Deploying Node.js Apps with Docker and Nginx on a VPS
This is the exact workflow I use on every project. No fancy orchestration, just Docker Compose, Nginx, and Let's Encrypt running on a plain Ubuntu VPS.
Prerequisites
- A VPS running Ubuntu 22.04 (I use Linode or DigitalOcean)
- A domain pointed at your server's IP
- Docker and Docker Compose installed
1. Containerise your app
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Build and test locally:
docker build -t myapp .
docker run -p 3000:3000 myapp
2. Docker Compose setup
version: '3.8'
services:
app:
image: myapp:latest
restart: unless-stopped
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
networks:
- web
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./certs:/etc/letsencrypt
depends_on:
- app
networks:
- web
networks:
web:
Database Performance Considerations
If your Node.js app connects to PostgreSQL, Docker adds another layer to optimize. Connection pooling becomes critical when containers restart, and default Postgres settings rarely match production load.
For a deep dive into optimizing PostgreSQL in Docker, including connection pool configuration, query optimization, and memory tuning for containerized databases, see the complete guide.
3. Nginx configuration
server {
listen 80;
server_name yourdomain.com;
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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
4. SSL with Let's Encrypt
apt install certbot
certbot certonly --standalone -d yourdomain.com
Set up auto-renewal:
crontab -e
# Add: 0 3 * * * certbot renew --quiet && docker compose restart nginx
5. Zero-downtime deploy script
#!/bin/bash
docker pull myapp:latest
docker compose up -d --no-deps app
echo "Deployed at $(date)"
That's it. Simple, reliable, and you own the whole stack.