How to Deploy a Docker Container to DigitalOcean: Droplets, App Platform and DOKS
DigitalOcean gives you three ways to run a Docker image:
- A Droplet (a VM) with Docker installed. Full control, lowest cost, and you maintain the server.
- App Platform. A managed platform that runs your image or builds your Dockerfile. No servers, built-in HTTPS and rolling deploys.
- DigitalOcean Kubernetes (DOKS). For when you already use Kubernetes or have many services.
All three can pull from DigitalOcean Container Registry (DOCR), Docker Hub or GitHub Container Registry.
Step 1: push the image to DigitalOcean Container Registry
Install doctl, authenticate with an API token, create a registry and log Docker in:
doctl auth init
doctl registry create my-registry
doctl registry login
docker build --platform linux/amd64 -t registry.digitalocean.com/my-registry/my-app:1.0.0 .
docker push registry.digitalocean.com/my-registry/my-app:1.0.0
--platform linux/amd64 matters when you build on an Apple Silicon Mac, because Droplets and App Platform run x86_64. Use a version tag instead of latest, so you always know what is running and can roll back to an exact image.
Option A: a Droplet with Docker
Create an Ubuntu 24.04 Droplet with your SSH key, either in the control panel or with doctl:
doctl compute ssh-key list # find your key ID or fingerprint
doctl compute droplet create my-app \
--image ubuntu-24-04-x64 \
--size s-1vcpu-2gb \
--region fra1 \
--ssh-keys <key-id> \
--wait
The Marketplace also has a Docker 1-Click image with Docker preinstalled. On a plain Ubuntu Droplet, SSH in as root, create a non-root user and install Docker Engine from Docker's apt repository.
Firewall. Use a DigitalOcean Cloud Firewall (Networking > Firewalls). Allow SSH from your IP, ports 80 and 443 from anywhere, and attach it to the Droplet. Cloud Firewalls filter traffic before it reaches the Droplet. UFW on the Droplet itself does not reliably protect containers, because ports published with -p bypass UFW rules.
Registry access on the Droplet. Either install doctl there and run doctl registry login, or log in with an API token that only has read access to the registry:
echo "$DO_TOKEN" | docker login registry.digitalocean.com -u you@example.com --password-stdin
Run it with Compose and a TLS proxy. In /srv/my-app/compose.yaml:
services:
app:
image: registry.digitalocean.com/my-registry/my-app:1.0.0
restart: unless-stopped
env_file: app.env
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
volumes:
caddy-data:
And the Caddyfile:
app.example.com {
reverse_proxy app:8080
}
Point an A record for app.example.com at the Droplet's IP, then start it:
cd /srv/my-app
docker compose up -d
docker compose logs -f app
Only Caddy publishes ports. The app is reachable only through the Compose network, and Caddy gets and renews a Let's Encrypt certificate on its own. restart: unless-stopped brings both containers back after a crash or reboot, so no systemd unit is needed. Keep secrets in app.env with chmod 600, not in the image.
Updating. Change the tag in compose.yaml, then:
docker compose pull app && docker compose up -d
docker image prune -f
Compose replaces the container, which causes a few seconds of downtime. To roll back, set the previous tag and run the same commands. The Docker on a server guide covers deploy scripts and zero-downtime options in more depth.
Option B: App Platform
App Platform runs the image for you and handles HTTPS, health checks and rolling deploys. Describe the app in app.yaml:
name: my-app
region: fra
services:
- name: web
image:
registry_type: DOCR
repository: my-app
tag: "1.0.0"
deploy_on_push:
enabled: true
http_port: 8080
instance_count: 2
instance_size_slug: apps-s-1vcpu-1gb
health_check:
http_path: /healthz
envs:
- key: DB_PASSWORD
scope: RUN_TIME
type: SECRET
value: change-me
doctl apps create --spec app.yaml
doctl apps list # app ID and live URL
doctl apps update <app-id> --spec app.yaml # after changing the tag
For DOCR, leave the registry field out. For Docker Hub, set registry_type: DOCKER_HUB and registry to the Docker Hub namespace. With deploy_on_push, pushing a new image to the same tag triggers a redeploy. App Platform can also build straight from a GitHub repository that contains a Dockerfile, so you skip the registry step. You set http_port to whatever the container listens on, and the platform gives you an HTTPS URL plus optional custom domains.
App Platform costs more per unit of CPU than a Droplet, but you have no OS to patch and no proxy to run.
Option C: DigitalOcean Kubernetes
doctl kubernetes cluster create my-cluster --region fra1 \
--node-pool "name=pool-1;size=s-2vcpu-4gb;count=2"
doctl kubernetes cluster registry add my-cluster
kubectl create deployment my-app \
--image=registry.digitalocean.com/my-registry/my-app:1.0.0 --port=8080
kubectl expose deployment my-app --type=LoadBalancer --port=80 --target-port=8080
kubectl get service my-app --watch
doctl kubernetes cluster create also adds the cluster to your kubeconfig. cluster registry add sets up pull credentials for DOCR in the cluster, and a LoadBalancer Service creates a DigitalOcean Load Balancer. The Kubernetes deployment guide covers manifests, probes and rollouts.
Which one to choose
- One small app, tight budget, comfortable with Linux: a Droplet with Compose.
- No interest in servers, need HTTPS and scaling out of the box: App Platform.
- Many services or an existing Kubernetes setup: DOKS.
Quick reference
doctl registry login
docker build --platform linux/amd64 -t registry.digitalocean.com/my-registry/my-app:TAG .
docker push registry.digitalocean.com/my-registry/my-app:TAG
# Droplet
docker compose pull && docker compose up -d
# App Platform
doctl apps create --spec app.yaml
doctl apps update <app-id> --spec app.yaml
# DOKS
doctl kubernetes cluster registry add my-cluster
kubectl create deployment my-app --image=registry.digitalocean.com/my-registry/my-app:TAG
