Deploy Docker Compose to a VPS — Production-Ready Setup
Why Deploy to a VPS?
A Virtual Private Server (VPS) gives you full control over your infrastructure. It is more flexible than PaaS (Heroku, Render), more affordable than managed Kubernetes, and gives you the same Linux environment as production cloud servers.
Why this matters for your career:
- VPS deployment skills are essential for freelance developers
- Understanding server setup helps you debug production issues
- VPS is often the most cost-effective choice for early-stage projects
- The skills transfer directly to cloud servers (EC2, DigitalOcean Droplets)
Step-by-Step VPS Deployment
Step 1: Initial Server Setup
# SSH into your VPS
ssh root@your-server-ip
# Update system
apt update && apt upgrade -y
# Create a non-root user
adduser deploy
usermod -aG sudo deploy
# Disable root SSH login (edit /etc/ssh/sshd_config)
# PermitRootLogin no
systemctl restart sshd
# Exit and reconnect as deploy user
exit
ssh deploy@your-server-ip
Step 2: Install Docker and Docker Compose
# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
# Log out and back in for group changes to take effect
# Install Docker Compose (standalone, latest)
sudo curl -SL "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
# Verify
docker --version
docker-compose --version
Step 3: Set Up the Application
# Create application directory
mkdir -p ~/apps/myapp
cd ~/apps/myapp
# Copy your docker-compose files (from git or scp)
git clone https://github.com/yourorg/your-repo.git .
# Create production .env file
cat > .env << 'EOF'
NODE_ENV=production
DB_PASSWORD=your-strong-password-here
DB_USER=myapp
DB_NAME=myapp_production
EOF
chmod 600 .env # Restrict permissions on .env file
# Start services
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# Check status
docker-compose ps
docker-compose logs --tail=50
Step 4: Nginx Reverse Proxy
# Install Nginx
sudo apt install nginx -y
# Create site config
sudo cat > /etc/nginx/sites-available/myapp << 'EOF'
server {
listen 80;
server_name myapp.example.com;
# Redirect all HTTP to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name myapp.example.com;
ssl_certificate /etc/letsencrypt/live/myapp.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myapp.example.com/privkey.pem;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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;
proxy_cache_bypass $http_upgrade;
}
location /static/ {
alias /var/www/myapp/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
}
EOF
# Enable site
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t # Test configuration
sudo systemctl reload nginx
Step 5: SSL Certificate (Let's Encrypt)
# Install Certbot
sudo apt install certbot python3-certbot-nginx -y
# Obtain certificate
sudo certbot --nginx -d myapp.example.com
# Auto-renewal (certbot creates a systemd timer automatically)
sudo certbot renew --dry-run
Step 6: Firewall Configuration
# Install ufw
sudo apt install ufw -y
# Configure rules
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
# Enable firewall
sudo ufw --force enable
# Check status
sudo ufw status
Complete Production docker-compose.yml
version: "3.9"
services:
app:
build:
context: .
dockerfile: Dockerfile.prod
restart: always
env_file: .env
expose:
- "3000" # Only expose to internal network
networks:
- app_net
depends_on:
db:
condition: service_healthy
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
db:
image: postgres:16-alpine
restart: always
env_file: .env
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- app_net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-myapp}"]
interval: 10s
timeout: 5s
retries: 5
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
app_net:
volumes:
pgdata:
Monitoring and Maintenance
# View logs
docker-compose logs --tail=100 -f
# Check resource usage
docker stats
# Update the application
git pull
docker-compose build
docker-compose up -d
# View old containers (for cleanup)
docker container ls -a | grep weeks ago
# Clean up unused images and containers
docker system prune -a --volumes
Automated Deployments with GitHub Actions
# .github/workflows/deploy.yml
name: Deploy to VPS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
cd ~/apps/myapp
git pull
docker-compose build
docker-compose up -d
docker image prune -f # Remove old images
Security Checklist
| Item | Status | |------|--------| | Non-root user for daily operations | ✅ | | Root SSH login disabled | ✅ | | Firewall (ufw) enabled | ✅ | | SSH key authentication only | ✅ | | SSL/TLS (Let's Encrypt) | ✅ | | .env file permissions (600) | ✅ | | Regular security updates | ✅ (unattended-upgrades) | | Docker not exposed on public port | ✅ | | Database not exposed externally | ✅ | | Log rotation configured | ✅ | | Backup strategy | ⚠️ (needs setup) | | Monitoring (uptime, resource) | ⚠️ (needs setup) |
Summary
Deploying Docker Compose to a VPS gives you full control over your infrastructure at a fraction of the cost of managed services. With Nginx as a reverse proxy, Let's Encrypt for SSL, UFW for firewall, and GitHub Actions for CI/CD, you have a production-ready setup that rivals cloud platforms.
Key takeaways:
- Use a non-root user and disable root SSH login
- Install Docker and Docker Compose on the VPS
- Use Nginx as a reverse proxy with SSL termination
- Obtain SSL certificates from Let's Encrypt (free, auto-renewing)
- Configure UFW firewall to allow only SSH, HTTP, HTTPS
- Use .env files for secrets with restricted permissions
- Set up automated deployments via GitHub Actions
- Configure log rotation and regular maintenance
- Monitor with docker stats and logs
What's Next: DevOps — GitOps
The next course covers GitOps — using Git as the single source of truth for infrastructure and application deployment with ArgoCD.