Skip to content
Back to Blog

Infrastructure

Docker for VPS Deployments: The Developer's Playbook

ZentisLabs Team13 min read

Docker transforms VPS deployment from hours of manual configuration into repeatable, version-controlled infrastructure. This guide covers everything from basic Docker setup on a fresh VPS to production-ready deployments with reverse proxies, SSL, health checks, and monitoring.

Initial Docker Setup

bash
# Install Docker on Ubuntu/Debian
curl -fsSL https://get.docker.com | sh
# Add your user to the docker group
sudo usermod -aG docker $USER
# Install Docker Compose v2
sudo apt install docker-compose-plugin -y
# Verify installation
docker --version
docker compose version

Writing Production Dockerfiles

Key principles for production Dockerfiles:

dockerfile
# Use specific version tags, never :latest
FROM node:20-alpine AS builder
WORKDIR /app
# Copy dependency files first (cache layer)
COPY package.json package-lock.json ./
RUN npm ci --production=false
# Copy source and build
COPY . .
RUN npm run build
# Production stage - minimal image
FROM node:20-alpine
WORKDIR /app
# Don't run as root
RUN addgroup --system app && adduser --system --group app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER app
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]

Docker Compose for Multi-Service Apps

yaml
# docker-compose.yml
version: "3.8"
services:
app:
build: .
restart: unless-stopped
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://user:pass@db:5432/myapp
depends_on:
db:
condition: service_healthy
networks:
- app-network
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=securepassword
- POSTGRES_DB=myapp
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
networks:
- app-network
volumes:
postgres_data:
redis_data:
networks:
app-network:

Nginx Reverse Proxy with SSL

yaml
# Add to docker-compose.yml
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./certbot/conf:/etc/letsencrypt:ro
- ./certbot/www:/var/www/certbot:ro
depends_on:
- app
networks:
- app-network
certbot:
image: certbot/certbot
volumes:
- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait ${!}; done;'"
nginx
# nginx.conf
server {
listen 80;
server_name yourdomain.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
location / {
proxy_pass http://app:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

Zero-Downtime Deployment

bash
#!/bin/bash
# deploy.sh - Zero-downtime deployment script
set -e
echo "Pulling latest code..."
git pull origin main
echo "Building new image..."
docker compose build app
echo "Rolling update..."
docker compose up -d --no-deps app
echo "Waiting for health check..."
sleep 10
if docker compose ps app | grep -q "healthy"; then
echo "Deployment successful!"
docker image prune -f
else
echo "Health check failed! Rolling back..."
docker compose rollback app
exit 1
fi

Monitoring and Logs

bash
# View logs for all services
docker compose logs -f
# View logs for a specific service
docker compose logs -f app --tail 100
# Check resource usage
docker stats
# Set up log rotation
# Add to docker-compose.yml under each service:
# logging:
# driver: "json-file"
# options:
# max-size: "10m"
# max-file: "3"

Docker Security Checklist

  • Never run containers as root - always use USER in Dockerfile
  • Use specific image tags, never :latest
  • Don't expose the Docker socket to containers
  • Use --read-only filesystem where possible
  • Set memory and CPU limits per container
  • Scan images for vulnerabilities with docker scout
  • Use Docker secrets or environment files for credentials

Every ZentisLabs server gives you full root access, so this whole Docker playbook runs as-is on Ubuntu 24.04, Ubuntu 22.04, or Debian 12. Plans start at $47/mo in 19 locations.

Ready to get started?

Try ZentisLabs free - 100MB residential bandwidth included.