Deploying a Dockerized Node.js App on a DigitalOcean Droplet
Many teams reach for managed services but sometimes you want direct control, predictable cost, and the chance to run your containers on your own VM. DigitalOcean Droplets and Docker provide a simple, robust path. Below, a guide with real-world adjustments.
Core Tooling
- DigitalOcean Droplet (Ubuntu 22.04 LTS recommended)
- Docker 24.x+ (for Compose plugin support, faster image pulls)
- SSH access (key-based authentication)
- Local Docker for build/test
Reference Application: Node.js HTTP Server
Directory layout:
my-docker-app/
├── app.js
├── package.json
└── Dockerfile
app.js
Basic HTTP server, stateless:
const http = require('http');
http.createServer((req, res) => {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello from Docker on DigitalOcean!\n');
}).listen(3000, () => {
console.log('Server bound on :3000');
});
Dockerfile
Explicit pinning can prevent surprises:
FROM node:18.19-alpine
WORKDIR /app
COPY package.json ./
RUN npm install --production
COPY app.js ./
EXPOSE 3000
CMD ["node", "app.js"]
package.json (minimalist, no unnecessary deps):
{ "name": "digitalocean-docker-app", "version": "1.0.0", "main": "app.js", "scripts": { "start": "node app.js" } }
Local Build & Validation
Before shipping to the cloud, confirm the artifact is viable:
docker build -t my-digitalocean-app:latest .
docker run --rm -p 3000:3000 my-digitalocean-app:latest
Expected output in browser:
Hello from Docker on DigitalOcean!
Log output:
Server bound on :3000
Infrastructure Setup: DigitalOcean Droplet
- OS Image: Ubuntu 22.04 LTS (minimal install preferred; fewer default services)
- Plan: First-generation, 1 vCPU, 1GB RAM suffices for test/staging
- SSH key: never use passwords for root
- Networking: No floating IP required; firewall rules can be managed via DigitalOcean UI
Once provisioned, take note of the Droplet's public IPv4.
Critical: Docker Installation on Ubuntu 22.04
Do not use Ubuntu's apt install docker.io
(often outdated, missing new features).
apt update && apt upgrade -y
# Install prerequisites
apt install -y ca-certificates curl gnupg lsb-release
# Official Docker GPG key
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
Check installation, avoid a common gotcha (daemon not started):
docker --version
systemctl is-active --quiet docker || systemctl restart docker
Post-install: add your user to group (recommended, not mandatory for root):
usermod -aG docker $USER
# Log out and SSH back in to refresh groups
Code Deployment: Secure File Transfer or Git
Options:
scp
/rsync
from your local machine:scp -r ./my-docker-app root@your.DROPLET.IP:/root/
- Git (if public or token-auth required):
git clone https://github.com/your/repo.git ~/my-docker-app
Note: For larger teams or production, use a CI/CD system. Manual sync is error-prone for frequent releases.
Build and Run Container Remotely
Assuming folder now present at ~/my-docker-app
:
cd ~/my-docker-app
docker build -t my-digitalocean-app:latest .
# To run as a typical web workload:
docker run -d --restart unless-stopped \
-p 80:3000 \
--name myapp \
my-digitalocean-app:latest
--restart unless-stopped
covers most recovery cases.- Use
-p 80:3000
to expose on standard HTTP; adjust for HTTPS via reverse proxy in production.
Check logs:
docker logs myapp
If you see:
Server bound on :3000
the app is running.
Quick Sanity Check
Open browser:
http://your.DROPLET.IP/
Response should match local output.
Command-line check (network/firewall bypass):
curl -i http://localhost/
Known Issues and Notes
- Port binding conflicts: If port 80 is already in use by nginx or Apache, either stop/disable those services or use an alternate port (e.g.,
-p 8080:3000
). - Firewall: DigitalOcean Cloud Firewalls may block external HTTP requests. Verify inbound rules.
- SELinux: Not enabled by default on Ubuntu, but for users of Fedora-based downstreams, container networking may be impacted.
Production Considerations
- Use a reverse proxy (nginx, Caddy) for SSL termination, not shown here.
- For persistent data or stateful workloads, use Docker volumes—mandatory for DBs.
- For zero-downtime deploys, orchestrators (Kubernetes, Nomad) or at least Compose are preferable.
Side Note: Docker Compose
For a single container, Compose adds little. With multiple services, e.g. Redis or Postgres, define in docker-compose.yml
, but keep in mind:
Compose's lifecycle differs—containers restart policies and logs propagate differently.
Summary
The above approach runs a Dockerized Node.js app on a DO Droplet with minimal dependencies, full root control, and reproducible deployment. Trade-offs: manual steps, but full visibility.
For automated or team-centric deployments, wire a CI/CD pipeline (e.g., GitHub Actions to SCP or SSH). For scale, consider DigitalOcean App Platform or Kubernetes.
It’s not perfect—managing secrets, SSL, and blue/green deploys require further tooling—but for many projects, this gives the right balance of hands-on control and operational simplicity.