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
# Install Docker on Ubuntu/Debiancurl -fsSL https://get.docker.com | sh
# Add your user to the docker groupsudo usermod -aG docker $USER
# Install Docker Compose v2sudo apt install docker-compose-plugin -y
# Verify installationdocker --versiondocker compose versionWriting Production Dockerfiles
Key principles for production Dockerfiles:
# Use specific version tags, never :latestFROM 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 buildCOPY . .RUN npm run build
# Production stage - minimal imageFROM node:20-alpineWORKDIR /app
# Don't run as rootRUN addgroup --system app && adduser --system --group app
COPY --from=builder /app/dist ./distCOPY --from=builder /app/node_modules ./node_modulesCOPY --from=builder /app/package.json ./
USER appEXPOSE 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
# docker-compose.ymlversion: "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
# 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.confserver { 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
#!/bin/bash# deploy.sh - Zero-downtime deployment scriptset -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 -felse echo "Health check failed! Rolling back..." docker compose rollback app exit 1fiMonitoring and Logs
# View logs for all servicesdocker compose logs -f
# View logs for a specific servicedocker compose logs -f app --tail 100
# Check resource usagedocker 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
USERin Dockerfile - Use specific image tags, never
:latest - Don't expose the Docker socket to containers
- Use
--read-onlyfilesystem where possible - Set memory and CPU limits per container
- Scan images for vulnerabilities with
docker scout - Use Docker secrets or environment files for credentials
🐳 ZentisLabs VPS instances come with Docker pre-installed. Deploy your stack in minutes with our one-click Docker setup from the dashboard.
