Deploying Docker on a VPS: A Practical Engineering Walkthrough
Abstracting away all container management to cloud platforms isn’t always optimal. In regulated environments, or just where cost and granular infrastructure control matter, rolling your own Docker deployment on a VPS is still the pragmatic move. Explored below: setting up Docker engine (≥24.0), streaming containers, persistent data strategies, and common pain points—without the abstraction layer.
Why Push Containers to a VPS?
- Environment Parity: Achieve tight alignment between development and production—same OS, kernel version, and network layout.
- Performance Profiling: No noisy neighbors unless self-inflicted. Latency and IOPS are predictable relative to hardware, not cloud hypervisor scheduling.
- Cost Model: $6/month for a 2GB RAM, 1 vCPU instance from the usual suspects (DigitalOcean, Vultr, Hetzner Cloud), vs. managed container price hikes post-free-tier.
- Root-Level Troubleshooting: Strace, tcpdump, cgroups inspection—available without support tickets.
- Escape Hatches: Container snapshots, raw backup, or bare-metal migration is viable.
Setup: From Provider Console to Shell
Provider: Any VPS host with KVM or similar (avoid oversold OpenVZ for namespaces!).
Provision:
- Image: Ubuntu Server 22.04 LTS (
jammy
) - Minimum: 1 vCPU, 2GB RAM, 20GB disk (SSD preferred)
- SSH key injection (skip password auth if possible):
# Replace $IP and $USER appropriately
ssh $USER@$IP
Immediate patch:
sudo apt update && sudo apt upgrade -y
Gotcha: Some kernel upgrades require a reboot. Double-check
uname -a
andapt list --upgradable | grep linux-image
.
Lock privilege escalation:
adduser dockerops
usermod -aG sudo dockerops
su - dockerops
Docker Engine: Installation & Verification
Docker’s convenience installer script suffices for most VPS use. For airgapped or audited environments, use the official repo and verify GPG signatures.
curl -fsSL https://get.docker.com | sudo sh
docker --version # e.g., Docker version 24.0.7, build cb74dfc
Permission edge-case: Add user to docker group, then refresh session context.
sudo usermod -aG docker $USER
# Re-login or: exec su -l $USER
If docker ps
throws “permission denied”, group membership hasn't propagated.
Sanity Test: Minimal Container Launch
docker run --rm hello-world
Should see:
Hello from Docker! This message shows that your installation appears to be working correctly.
If instead:
Cannot connect to the Docker daemon at unix:///var/run/docker.sock
—verify service is running:
sudo systemctl status docker
Deploying a Real Container: Nginx Web
The basics matter. Here’s Nginx served stateless.
docker run -d --name mynginx -p 80:80 nginx:1.25-alpine
Access via curl http://<your_vps_ip>
or browser. Default welcome page confirms working publish of port 80.
To persist configuration or serve static assets, mount local volumes (see below).
Key Runtime Operations
Command | Action |
---|---|
docker ps | List running containers |
docker ps -a | All containers (stopped/running) |
docker logs mynginx | Print logs for named container |
docker stop mynginx | Graceful shutdown |
docker rm mynginx | Destroy (after stopping) |
docker images | List available images |
Known issue: Zombie containers—cleanup with docker system prune -f
occasionally to reclaim orphaned volumes/layers.
Data Persistence: Bind-Mount Volumes
Container ephemeral storage isn’t enough; use host-mounted directories.
mkdir -p ~/webroot/html
echo "<h2>VPS-backed Nginx</h2>" > ~/webroot/html/index.html
docker run -d \
--name mynginx \
-p 80:80 \
-v ~/webroot/html:/usr/share/nginx/html:ro \
nginx:1.25-alpine
Now curl http://<your_vps_ip>
serves your custom content.
Note: File mode mismatches on bind mounts cause 403 errors. Chown as needed.
Orchestrating Multi-Container Apps with Compose
Example use case: Nginx + static HTML.
- Install compose plugin (Debian-based):
sudo apt install docker-compose-plugin -y
docker-compose.yml
:version: '3.8' services: web: image: nginx:1.25-alpine ports: - "80:80" volumes: - ./html:/usr/share/nginx/html:ro
- Launch:
mkdir ~/compose-demo && cd ~/compose-demo mkdir html && echo 'Testing Compose' > html/index.html docker compose up -d
Failure mode:
If you see ERROR: for web Cannot start service web: Ports are not available
, ensure nothing else is bound to 80. Check via sudo lsof -i :80
.
Security, Resource, and Recovery Practices
- Patch: Regularly update engine/plugins:
sudo apt upgrade docker-ce docker-compose-plugin
- Monitor: Live stats:
docker stats
or host-level withhtop
,iftop
,iotop
- Restrict: Harden with UFW or firewalld. Allow only required ingress:
sudo ufw allow 22/tcp sudo ufw allow 80/tcp sudo ufw enable
- Restart policies:
For production:docker run ... --restart unless-stopped ...
- Backup strategy:
Script volume/tar snapshots, or use rsync/RESTIC to remote storage.
Gotcha: Live-mounts may yield inconsistent db backups. Use DB-native dumps for data containers.
Troubleshooting: Real-World Faults
-
Out of Memory Kills (OOM):
dmesg will show msgs likeKilled process 1234 (nginx) total-vm:39328kB
.
Solution: monitor withdocker stats
; raise swap or adjust container memory limits with-m
. -
Disk Full:
docker: Error response from daemon: no space left on device
Clean up unused volumes/images:
docker system df
anddocker system prune -af
Further Steps
Consider moving toward Docker Compose for complex stacks, failover-ready storage with bind mounts, or even managed orchestration (K3s, Nomad). For declarative setups, check out Ansible automation.
Tip: For fully-scripted deployments, integrating with CI/CD tools (GitHub Actions, GitLab CI) cuts manual setup overhead and improves rollback plans.
Reference: Docker Official Documentation
Open Issues: Some VPS providers apply aggressive conntrack tuning, causing dropped connections on bursty TCP workloads—monitor /proc/net/nf_conntrack
if relevant.
This walkthrough isn’t exhaustive, but it hits the operational cornerstones for Docker-on-VPS in a real-world scenario. Any specifics out of scope? Drop to the Docker issues or seek tailored advice from infrastructure peers.