Deploy Docker Containers to DigitalOcean: Practical, Fast, No-Nonsense
There’s a persistent misconception that container orchestration must mean Kubernetes. In reality, spinning up containers on DigitalOcean can be done quickly, without the overhead or the cognitive load of managing a Kubernetes cluster. For many production workloads, droplets plus Docker are more than enough—especially when you want predictable costs, direct control, and minimal moving parts.
Snapshot: Why Use Docker on DigitalOcean?
Docker offers application portability; DigitalOcean provides simplicity, consistency, and just enough abstraction. Combined, these enable direct deployment pipelines with fewer failure domains and shorter feedback loops. No managed control planes. Direct access to logs. The network is yours to debug.
Note: App Platform covers PaaS use cases (builds from source or container), but this guide targets the hands-on route: provisioning droplets, running containers directly, integrating with CI/CD.
Prerequisites
- Source code already containerized (e.g. for Node.js, Python, etc.)
- Valid DigitalOcean account with billing enabled
- doctl v1.103.0+ (DigitalOcean CLI) installed and authenticated (
doctl auth init
) - Docker 24.x or newer installed locally for building and pushing images
Reference Example: Minimal Node.js Service
The following Dockerfile
illustrates a best-practice build for a Node.js app—nothing fancy, but production-lean:
FROM node:18.20-alpine3.19
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]
Build and verify locally:
docker build -t my-node-app:202406 .
docker run --rm -p 3000:3000 my-node-app:202406
# Check http://localhost:3000
If you see an error such as Module not found: Error: Cannot find module 'express'
, revisit your package.json
or context.
Registry: Docker Hub vs DigitalOcean Container Registry
Images must be retrievable from your droplet. DOCR is preferred for private builds and faster region access.
A. Docker Hub
docker tag my-node-app:202406 yourdockerhub/my-node-app:202406
docker push yourdockerhub/my-node-app:202406
B. DigitalOcean Container Registry (DOCR)
- Create registry:
doctl registry create myregistry
- Authenticate:
doctl registry login
- Tag and push:
docker tag my-node-app:202406 registry.digitalocean.com/myregistry/my-node-app:202406 docker push registry.digitalocean.com/myregistry/my-node-app:202406
Gotcha: DOCR rate-limits requests from unauthenticated agents. Use doctl registry login
before any automated deployments.
Droplet Provisioning: Quick/Direct Route
Provision a base Ubuntu 22.04 LTS droplet via Control Panel or CLI:
doctl compute droplet create web01 \
--region nyc3 --image ubuntu-22-04-x64 --size s-1vcpu-1gb \
--ssh-keys <ssh-key-fingerprint> --tag-names docker --wait
- Default 1 vCPU/1GB RAM sufficient for test deployments; for higher concurrency, select s-2vcpu-2gb or above.
- SSH:
ssh root@<DROPLET_IP>
Install Docker on Droplet
On a new droplet, avoid snap packages (they break on headless/CI bootstraps). Use official repositories:
apt-get update
apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/docker.gpg
echo "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" \
| tee /etc/apt/sources.list.d/docker.list
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io
systemctl enable --now docker
docker --version
Deploy and Run Container
Login to Registry (on droplet):
- For DOCR:
doctl registry login docker login registry.digitalocean.com
Pull and Launch:
docker pull registry.digitalocean.com/myregistry/my-node-app:202406
docker run -d -p 80:3000 --restart unless-stopped --name node-web-app \
registry.digitalocean.com/myregistry/my-node-app:202406
# For Docker Hub:
# docker pull yourdockerhub/my-node-app:202406
# docker run -d -p 80:3000 --restart unless-stopped --name node-web-app \
# yourdockerhub/my-node-app:202406
Verify:
- Hit
http://<droplet_ip>
from browser or usecurl -i http://<droplet_ip>
for headers. - If the port conflicts due to fail2ban or Nginx, either choose a different port or stop the conflicting service.
(Optional) Systemd for Container Lifecycle Management
To guarantee automatic restarts (and proper logging), wrap your container in a systemd unit. Example file:
/etc/systemd/system/nodeweb.service
[Unit]
Description=Node.js App Container
After=network-online.target docker.service
Requires=docker.service
[Service]
Restart=always
RestartSec=3
ExecStart=/usr/bin/docker start -a node-web-app
ExecStop=/usr/bin/docker stop -t 10 node-web-app
[Install]
WantedBy=multi-user.target
Then:
systemctl daemon-reload
systemctl enable --now nodeweb
Known issue: Systemd restarts will not recreate the container if you alter command flags or update the image tag. Use docker rm -f ... && docker run ...
to change config.
(Alternative) DigitalOcean App Platform: PaaS, No SSH
Use cases: CI/CD pipeline from GitHub, multi-environment builds, integrated load balancing, HTTPS by default.
Steps:
- Push code or image to GitHub/DOCR.
- App Platform UI: "Create App", select repo or custom image.
- Specify exposed port.
- Configure horizontal/vertical scaling, add domains, set runtime variables.
- Deploy.
Trade-off: Debugging networking or layer 7 details is more opaque; direct SSH access is unavailable.
Production Hardening and Caveats
- Lock down ports via DigitalOcean cloud firewall, not just UFW.
- Tag droplets with environment (
prod
,staging
, etc) for betterdoctl
filtering. - For persistent data, mount DO Volumes (
/mnt/volume_nyc1_*
) and use Docker volumes:-v /mnt/volume_nyc1_xyz:/app/data
- Watch for orphaned containers:
docker ps -a
can reveal if containers exited unexpectedly (e.g., OOMKilled).
Wrapping Up
Direct Docker deployment on DigitalOcean offers a balance between transparency and manageability. Not every workload needs full orchestration; for stateless services or MVP staging, droplets suffice. For those requiring rapid rollback or canary deploys, invest time in templated systemd units and registry-based automation.
Further reading: study DO load balancer integration, renovate image tags with semantic versioning, or trial DO's managed Postgres for stateful backing.
Any blocking errors, logs, or improvement ideas? Open a comment below; real failure cases are the best source of learning.