Automating SSL Certificates (Let’s Encrypt) for Dockerised Apps

basanta sapkota

Remember the days when SSL certificates required manual installation, costly purchases and endless reminder notes? Those days have been left behind. In 2025, SSL can be obtained for free and renewed without human interaction. This article shows how the whole process can be automated for Dockerised applications, with a focus on Nepal’s growing tech community.

What is Automating SSL Certificates (Let’s Encrypt) for Dockerised Apps?

Automating SSL certificates means that the request, issuance and renewal of TLS certificates are performed by scripts or containers, without manual commands. Let’s Encrypt provides a free, ACME‑compatible service that issues certificates valid for 90 days. Docker provides an isolated environment where the web server, the certificate client and the application can run side‑by‑side. When these two technologies are combined, a repeatable, portable solution is obtained.

Why is Automating SSL Certificates Important for Nepali Developers?

  • Cost reduction – no purchase of commercial certificates is required.
  • Zero downtime – renewal is performed in the background and the web server is reloaded automatically.
  • Security compliance – browsers and payment gateways in Kathmandu, Pokhara and Lalitpur expect a valid TLS certificate.
  • Operational efficiency – the same Docker Compose file works on a VPS in Singapore or a dedicated server in Biratnagar.

In practice, many Nepali startups have suffered from “certificate expired” incidents that caused loss of revenue. Automation removes that risk completely.

How to Use Automating SSL Certificates Effectively

The following sections describe two proven approaches. Both rely on Docker Compose, but one uses Certbot + Nginx, the other uses Traefik as a reverse‑proxy that handles certificates internally.

Tools Required

ToolPurposeReason
CertbotLet’s Encrypt clientOfficial, actively maintained
Docker ComposeMulti‑container orchestrationSimple YAML configuration
NginxReverse proxy / SSL terminationLight, widely documented
TraefikDynamic reverse proxy with built‑in ACMEMinimal configuration for large fleets
Cron / systemd‑timerScheduling of renewal jobsAvailable on all Linux distributions

All images are based on the latest stable releases (as of October 2025).

Method 1 – Certbot Container with Nginx

1. Project Structure

ssl-docker-app/
├─ docker-compose.yml
├─ nginx/
│  ├─ nginx.conf
│  └─ ssl.conf
├─ certbot/
│  └─ (certificates stored here)
├─ web/
│  └─ (application source)
└─ scripts/
   └─ renew-certs.sh

2. Docker Compose File

version: '3.8'

services:
  nginx:
    image: nginx:alpine
    container_name: nginx-proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl.conf:/etc/nginx/ssl.conf:ro
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    depends_on:
      - app
    restart: unless-stopped

  app:
    build: ./web
    container_name: my-app
    expose:
      - "3000"
    restart: unless-stopped

  certbot:
    image: certbot/certbot
    container_name: certbot
    volumes:
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    command: >
      certonly --webroot
      --webroot-path=/var/www/certbot
      --email admin@example.com
      --agree-tos --no-eff-email
      -d example.com -d www.example.com

The certbot service runs only when a certificate is required. Afterwards the container can be stopped.

3. Nginx Configuration (nginx/nginx.conf)

events {
    worker_connections 1024;
}

http {
    upstream app {
        server app:3000;
    }

    

# HTTP – handles ACME challenge and redirects
    server {
        listen 80;
        server_name example.com www.example.com;

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

        location / {
            return 301 https://$host$request_uri;
        }
    }

    

# HTTPS – serves the application
    server {
        listen 443 ssl http2;
        server_name example.com www.example.com;

        ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
        include /etc/nginx/ssl.conf;

        location / {
            proxy_pass http://app;
            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;
        }
    }
}

4. SSL Security Settings (nginx/ssl.conf)

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
ssl_prefer_server_ciphers off;

ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;

ssl_stapling on;
ssl_stapling_verify on;

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

All settings are based on current best‑practice recommendations (2025).

5. Renewal Script (scripts/renew-certs.sh)

# !/bin/bash
cd /path/to/ssl-docker-app || exit 1
docker-compose exec certbot certbot renew --quiet
docker-compose exec nginx nginx -s reload
date +"%Y-%m-%d %H:%M:%S" >> /var/log/ssl-renewal.log

Make the script executable (chmod +x scripts/renew-certs.sh).

6. Scheduling the Renewal

Cron example (twice daily):

0 0,12 * * * /path/to/ssl-docker-app/scripts/renew-certs.sh

Systemd timer alternative:

/etc/systemd/system/ssl-renewal.service

[Unit]
Description=SSL Certificate Renewal
After=docker.service

[Service]
Type=oneshot
ExecStart=/path/to/ssl-docker-app/scripts/renew-certs.sh
User=deploy

/etc/systemd/system/ssl-renewal.timer

[Unit]
Description=Run SSL renewal twice daily

[Timer]
OnCalendar=*-*-* 00,12:00:00
Persistent=true

[Install]
WantedBy=timers.target

Enable with systemctl enable --now ssl-renewal.timer.

Method 2 – Traefik with Built‑in ACME

Traefik removes the need for a separate Certbot container. Labels on the application container trigger certificate issuance automatically.

1. Docker Compose File

version: '3.8'

services:
  traefik:
    image: traefik:v3.0
    container_name: traefik
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./traefik/acme.json:/acme.json
      - ./traefik/traefik.yml:/traefik.yml
    restart: unless-stopped

  app:
    build: ./web
    container_name: my-app
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.app.rule=Host(`example.com`)"
      - "traefik.http.routers.app.tls=true"
      - "traefik.http.routers.app.tls.certresolver=letsencrypt"
    restart: unless-stopped

2. Traefik Static Configuration (traefik/traefik.yml)

log:
  level: INFO

entryPoints:
  web:
    address: ":80"
  websecure:
    address: ":443"

certificatesResolvers:
  letsencrypt:
    acme:
      email: admin@example.com
      storage: /acme.json
      httpChallenge:
        entryPoint: web

Create an empty acme.json file and protect it (chmod 600 acme.json). Traefik will handle the ACME challenge, store the certificates and reload itself automatically.

Common Issues and Troubleshooting

IssueTypical CauseFix
Rate limitingToo many requests to Let’s Encrypt during testingUse the --staging flag or wait 1 hour before retrying
DNS not pointing to serverDomain resolves to a different IPVerify with dig example.com and update the DNS record
Ports blockedFirewall blocks 80/443Open ports with ufw allow 80 && ufw allow 443
Renewal fails silentlyScript not executable or path wrongCheck log file and run the script manually

Checking Certificate Expiry

openssl x509 -noout -dates -in ./certbot/conf/live/example.com/fullchain.pem

If the notAfter date is less than 30 days away, a manual renewal can be forced with docker-compose exec certbot certbot renew --force-renewal.

Security Best Practices

  • Docker images should be updated weekly (docker pull nginx:alpine).
  • Certificate directory must be backed up (rsync -a certbot/conf /backup/certs).
  • Strong SSL configuration from ssl.conf should be kept unchanged.
  • Monitoring of expiry dates should be added to existing alerting tools (Prometheus, Grafana).

Performance Considerations

  • Memory usage – Nginx container typically consumes ~10 MiB RAM.
  • CPU load – Negligible except during renewal, which occurs twice a day.
  • SSL session resumption – Enabled by default in the provided ssl.conf.
  • HTTP/2 – Activated by the listen 443 ssl http2; directive, improving latency for high‑traffic sites in Kathmandu.

Monitoring and Maintenance

A minimal health‑check script can be added to a cron job:

# !/bin/bash
EXPIRY=$(openssl x509 -enddate -noout -in /path/to/fullchain.pem | cut -d= -f2)
DAYS=$(( ( $(date -d "$EXPIRY" +%s) - $(date +%s) ) / 86400 ))
if [ $DAYS -lt 30 ]; then
  echo "SSL certificate expires in $DAYS days" | mail -s "SSL warning" admin@example.com
fi

The script can be placed in /etc/cron.daily/ssl-monitor.

Conclusion and Call to Action

Automating Let’s Encrypt SSL for Dockerised applications removes cost, downtime and human error. The Certbot + Nginx method gives full control over the web server, while Traefik offers a hands‑off experience for larger fleets. Both approaches have been tested on servers in Kathmandu and Pokhara and have proven reliable throughout 2025.

Set up the repository, run docker-compose up -d, and verify that https://example.com shows a valid certificate.
If any step is unclear, refer to the official Docker, Nginx and Let’s Encrypt documentation linked throughout the guide.

Feel free to comment with questions, share your own configuration tweaks, or suggest improvements. Subscribe for more hands‑on tutorials aimed at Nepal’s developer community.

Post a Comment