hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Deploying Node.js Apps with Docker

Why Docker

  • The "Works on My Machine" Problem
  • Docker Concepts

Your First Dockerfile

  • Writing a Dockerfile
  • Multi-Stage Builds
  • The .dockerignore File

Running Containers

  • Running and Managing Containers
  • Environment Variables and Secrets
  • Health Checks

Multi-Container Apps

  • Docker Compose
  • Adding a Reverse Proxy
  • Persistent Data

Deploying to Production

  • Deploying to a VPS
  • HTTPS with Let's Encrypt
  • Zero-Downtime Deploys

CI/CD

  • Building Images in CI
  • Automated Deployment

Production Hardening

  • Container Security
  • Logging and Monitoring
  • Deployment Checklist and Capstone

HTTPS with Let's Encrypt

No excuses for no HTTPS

Every production app needs HTTPS. Without it, all traffic (including passwords, tokens, cookies) is sent in cleartext. Anyone on the network can read it.

Let’s Encrypt provides free SSL certificates. Certbot automates the process. There is no cost and no reason to skip this.

Setting up Certbot with Docker

Add a Certbot service to your compose file:

# docker-compose.yml
services:
  app:
    build: .
    expose:
      - "3000"
    env_file:
      - .env.production
    volumes:
      - app-data:/data
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    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
    restart: unless-stopped

  certbot:
    image: certbot/certbot
    # The official Certbot Docker image. Certbot is the tool that
    # requests and renews SSL certificates from Let's Encrypt.
    # It runs as a one-off command (not a long-running service).
    volumes:
      - certbot-conf:/etc/letsencrypt
      - certbot-www:/var/www/certbot

volumes:
  app-data:
  certbot-conf:
  certbot-www:

Step 1: HTTP-only Nginx config

Before getting a certificate, Nginx must serve HTTP so Certbot can verify your domain:

# nginx.conf (initial — HTTP only)
server {
    listen 80;
    server_name yourdomain.com;

    # Certbot verification
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    # Proxy to app
    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;
    }
}

Step 2: Get the certificate

# Start Nginx (HTTP only)
docker compose up -d nginx

# Request a certificate
docker compose run --rm certbot certonly \
  --webroot \
  --webroot-path /var/www/certbot \
  -d yourdomain.com \
  --email [email protected] \
  --agree-tos \
  --no-eff-email

Certbot places a verification file in /var/www/certbot/.well-known/acme-challenge/. Let’s Encrypt’s server accesses it via HTTP to verify you control the domain. On success, the certificate is stored in the certbot-conf volume.

Step 3: Update Nginx for HTTPS

# nginx.conf (with SSL)
server {
    listen 80;
    server_name yourdomain.com;

    # Redirect all HTTP to HTTPS
    return 301 https://$server_name$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;

    # Modern SSL config
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;

    # Certbot renewal endpoint
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    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_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;
    }
}

Restart Nginx to pick up the new config:

docker compose restart nginx

Your app is now at https://yourdomain.com. HTTP redirects to HTTPS.

Auto-renewal

Let’s Encrypt certificates expire after 90 days. Set up a cron job to renew automatically:

# Add to crontab (crontab -e)
0 3 * * * cd /app && docker compose run --rm certbot renew && docker compose exec nginx nginx -s reload

This runs daily at 3 AM. Certbot only renews if the certificate is within 30 days of expiry. After renewal, Nginx is reloaded to pick up the new certificate.

Exercises

Exercise 1: Point a domain to your VPS IP (A record in DNS). Set up Certbot and get a certificate.

Exercise 2: Verify HTTPS works. Open https://yourdomain.com in a browser. Check the certificate details.

Exercise 3: Test HTTP-to-HTTPS redirect. Open http://yourdomain.com. It should redirect to HTTPS.

Why do Let's Encrypt certificates expire after 90 days instead of a year?

← Deploying to a VPS Zero-Downtime Deploys →

© 2026 hectoday. All rights reserved.