Streamlining Production: Deploying Docker Containers on DigitalOcean Droplets for Scalable Apps
Avoid unnecessary overhead. Not every application needs Kubernetes. For startups, small teams, or situations demanding simplicity and price predictability, Docker on DigitalOcean Droplets remains a direct, highly controllable approach.
A quick review of why:
- Immediate access to raw Linux hosts (no managed cluster).
- Costing starts as low as $5/month per droplet, billed hourly or monthly.
- Docker containers enable app-level portability and build reproducibility.
- Operational complexity is minimal: direct SSH, direct logs, simple networking.
Experienced teams use this pattern to ship services rapidly, bootstrap MVPs, or operate low-maintenance prod workloads.
Provision a DigitalOcean Droplet
Provisioning via the DigitalOcean CLI (doctl
) or dashboard is straightforward. Example via UI:
- Image: Ubuntu 22.04 LTS (tested, receives regular security updates).
- Plan: For a typical web app,
s-1vcpu-1gb
($5/mo) is workable; jump tos-2vcpu-2gb
if expecting CI-driven load or more containers. - Region: Minimize latency—NYC3 or FRA1 common picks.
- Authentication: Always upload your SSH public key. Password access is slow and discouraged.
- Hostname: Name according to your fleet’s convention (
api-prod-01
, etc). - Click Create Droplet.
- Note assigned IPv4 address.
Gotcha: By default, root login is enabled. Should be disabled later for production hardening.
SSH In and Install Docker
Minimum tested versions: Docker 20.10.x, Ubuntu 22.04. Confirm current latest; upgrade if needed.
Connect:
ssh root@<droplet_ip>
Update system and install dependencies:
apt update && apt upgrade -y
apt install -y apt-transport-https ca-certificates curl gnupg lsb-release
Install Docker’s official GPG key and repo (backports safer than Ubuntu’s default):
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Install Docker Engine and CLI:
apt update
apt install -y docker-ce docker-ce-cli containerd.io
Enable and start the daemon:
systemctl enable --now docker
Check install:
docker --version # e.g. Docker version 24.0.5, build 24.0.5-0ubuntu1~22.04.1
Non-obvious tip: Add a non-root user to the docker
group instead of running everything as root. For production, configure SSH to disable root login once your user is ready.
Containerizing Your Application
Assume a Node.js project for illustration, though the steps translate easily to Python, Go, or static binaries.
Example Dockerfile
:
FROM node:18.17-alpine AS base
WORKDIR /srv/app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]
Why npm ci
and --omit=dev
? Faster, deterministic installs in prod images.
Build and test locally:
docker build -t acme-node-app:1.0.0 .
docker run --rm -it -p 3000:3000 acme-node-app:1.0.0
Before pushing, fix .dockerignore
to prevent uploading big files (e.g. node_modules/
, .git/
).
Push to a Registry
For simplicity, demonstrate with Docker Hub. Alternatives: GHCR, private registries.
- Tag for push:
docker tag acme-node-app:1.0.0 mydockerhub/acme-node-app:1.0.0
- Login:
docker login
- Push:
docker push mydockerhub/acme-node-app:1.0.0
Known issue: Docker Hub’s free tier rate-limits unauthenticated pulls. Configure auth or use your org’s registry for critical workloads.
Deploy: Pull and Run on Droplet
Back on the droplet:
docker pull mydockerhub/acme-node-app:1.0.0
docker run -d \
--name=acme-app \
--restart unless-stopped \
-p 80:3000 \
mydockerhub/acme-node-app:1.0.0
--restart unless-stopped
: Service-style autostart.-p 80:3000
: Maps container port to droplet port 80.
Check container health:
docker ps
docker logs acme-app
If port 80 is occupied (Error starting userland proxy: listen tcp 0.0.0.0:80: bind: address already in use
), diagnose with ss -tlnp
.
Hardening: Basic Firewall and Updates
Enable UFW:
ufw allow OpenSSH
ufw allow 80/tcp
ufw --force enable
ufw status
Note: UFW rules persist reboots. For HTTPS, also allow 443/tcp
. For additional hardening, consider fail2ban and restrict SSH to your office IP.
Update Docker and system on a regular schedule:
apt update && apt upgrade -y
docker --version
Scaling and Extending
A single droplet can typically push a Node.js/Go service to 500–1000 req/sec before network or CPU bottlenecks. Scale horizontally by imaging droplets or scripting with Terraform/Ansible. DigitalOcean Load Balancers slot in front of droplets—note the $12/mo starting price.
For stateless apps, DNS-based cutover between droplets is trivial:
+---------------+ +--------------------+
| Load Balancer +---80/443+ droplet-01 (app) |
+---------------+ +--------------------+
| + droplet-02 (app) |
| + ... |
Users
Beware upgrade coordination and persistent state—there’s no orchestrator here to handle rolling restarts.
TL;DR Table
Feature | Droplets + Docker | Kubernetes (for comparison) |
---|---|---|
Setup time | <10 minutes | 1+ hours |
Pricing transparency | Fixed per-droplet | Variable (per-node, + mgmt) |
Scaling | Manual but simple | Automated, more knobs |
Deployment complexity | Low | Moderate to high |
App portability | High via Docker | High |
Non-obvious Tip
DigitalOcean offers prebuilt “Docker” one-click images—but regular Ubuntu + manual Docker installation gives you more control, completeness, and transparency over package versions and security updates.
Missed perfection: No built-in blue/green deployment. For that, integrate a load balancer with health checks, or script the cutover via DNS.
References
- DigitalOcean Documentation: Droplets
- Docker Official Docs
- DigitalOcean Load Balancers
- Terraform DigitalOcean Provider
Questions or edge cases not addressed here? Audit your firewall config and container restart policy before deploying at scale.