All posts

Deploying Node.js Apps with Docker and Nginx on a VPS

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:

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.