Secure web traffic with HTTPS is no longer optional — it’s expected. This guide shows how to automate certificate issuance and renewal using Docker, Nginx and Certbot, packaged so deployment is repeatable and low-maintenance. Target audience: developers and ops engineers comfortable with Docker and basic Linux commands.
What you'll achieve
- Deploy Nginx as a reverse proxy inside Docker.
- Automatically obtain and renew Let's Encrypt TLS certificates using Certbot.
- Keep configuration simple, reproducible, and safe for production.
Prerequisites
- Docker and Docker Compose installed on a host with a public domain name pointing to its IP.
- Port 80 and 443 reachable from the internet.
- Basic familiarity with Nginx configuration and Docker Compose.
Project layout
Create a project directory; inside it, you’ll have:
- docker-compose.yml
- nginx/
- conf.d/
- default.conf (or site-specific conf)
- certbot/
- www/ (for HTTP challenge files)
- data/ (certificate storage)
Step 1 — Write the Docker Compose file
Compose will run two services: nginx and certbot. Nginx serves traffic and proxies ACME HTTP-01 challenges to Certbot’s challenge directory.
Example docker-compose.yml:
version: "3.8"
services:
nginx:
image: nginx:stable-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./certbot/www:/var/www/certbot:ro
- ./data:/etc/letsencrypt
depends_on:
- certbot
restart: unless-stopped
certbot:
image: certbot/certbot
volumes:
- ./certbot/www:/var/www/certbot
- ./data:/etc/letsencrypt
entrypoint: /bin/sh -c "trap exit TERM; while :; do sleep 12h & wait $${!}; certbot renew --webroot -w /var/www/certbot --deploy-hook 'nginx -s reload' || true; done"
restart: unless-stopped
Notes:
- Certificates live in ./data so they persist across container restarts.
- Certbot is configured to periodically run renew and reload Nginx on success.
Step 2 — Configure Nginx to handle ACME challenges and proxy traffic
Create an Nginx server block that:
- Serves /.well-known/acme-challenge from the certbot/www directory.
- Proxies other requests to your upstream app.
Example nginx/conf.d/default.conf:
server {
listen 80;
server_name example.com www.example.com;
location /.well-known/acme-challenge/ {
alias /var/www/certbot/.well-known/acme-challenge/;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
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;
# Basic SSL hardening (tune for your needs)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://your_upstream:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Replace example.com and your_upstream accordingly.
Step 3 — Obtain the initial certificate
Before HTTPS works, Certbot needs initial issuance. Run a one-off certbot command to request certificates using the webroot plugin.
Example command:
docker-compose run --rm certbot certonly \
--webroot -w /var/www/certbot \
-d example.com -d www.example.com \
--email you@example.com \
--agree-tos \
--no-eff-email
If successful, certificates are written under ./data/live/example.com. Then start or restart the stack:
docker-compose up -d
Certbot will handle renewals automatically via the background loop in docker-compose.
Step 4 — Validation & testing
- Visit https://example.com and confirm the site loads with a valid certificate. Certificate should be issued by Let's Encrypt.
- Test renewal simulation:
docker-compose run --rm certbot renew --dry-run --webroot -w /var/www/certbot
Troubleshooting checklist
- DNS: Ensure domain points to the host IP.
- Ports: Confirm 80 and 443 are open and not blocked by firewall.
- File permissions: Nginx must be able to read the certbot/www path.
- Nginx config test: inside container run
nginx -t
to check syntax.
Security & operational tips
- Store the data directory on encrypted disk if it contains sensitive keys.
- Monitor certificate expiry with alerts (letsencrypt certs expire every 90 days).
- Consider using a dedicated ACME container like dehydrated or an ACME client with webhook/notifications for more advanced flows.
- For high-availability, move certificate issuance into a single central manager and distribute certs via secure sync.
Optional enhancements
- Use Docker labels + Traefik to automate routing and certs (alternative to this stack).
- Add HSTS, OCSP stapling and stronger cipher suites in Nginx for production hardening.
- Automate backups of ./data with rotation.
Top comments (0)