Production Reverse Proxying with Nginx and Automated SSL in Docker
Configuring hardened Nginx reverse proxy headers, rate-limiting rules, and certbot auto-renewals for containerised microservices.
Every service I deploy for clinical use goes through the same checklist before it touches real patient data: reverse proxy hardened, SSL automated, rate limits on, sensitive headers stripped. Here is the exact setup I use.
Why Nginx in Front of Docker
Docker Compose gives you inter-service networking for free, but it does not give you TLS termination, rate limiting, or the security headers that stop browsers screaming at your users. Nginx sits in front of everything, handles certificates, and forwards clean HTTP to the containers behind it.
The alternative — letting each service manage its own TLS — means separate certbot configs, separate renewal crons, and separate failure modes. One Nginx proxy means one renewal job.
The Stack
Three files do all the work.
Initial Certificate Issuance
Before the proxy can serve HTTPS, you need the certificate. Run this once with Docker down:
# 1. Start only nginx on port 80 (comment out the 443 server block first)
docker compose up -d nginx
# 2. Issue the certificate via the webroot challenge
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
# 3. Uncomment the 443 block and reload
docker compose exec nginx nginx -s reload
After that, the certbot container’s renewal loop handles everything. Certificates renew automatically when they have fewer than 30 days left.
Rate Limiting That Does Not Break Legitimate Users
The burst=10 nodelay combination is deliberate. It allows a burst of 10 requests to pass without queuing delay (useful for page loads that fire several parallel API calls), then enforces the 30 r/m zone for sustained traffic. Without nodelay, the first burst would queue and your SPA would feel slow on first load.
For the health services I build, the numbers are conservative by design — a clinic worker refreshing a donor dashboard should never hit a 429. Adjust the zone rate for your actual expected traffic.
What This Buys You
| Without this setup | With this setup |
|---|---|
| Self-signed certs or manual renewal | Automated 90-day Let’s Encrypt renewals |
| No rate limiting | 30 req/min per IP, burst-tolerant |
| Server version leaked in headers | server_tokens off + headers stripped |
| HTTP serving mixed content | Forced HTTPS redirect with HSTS preload |
| Each service manages its own TLS | One Nginx, one renewal job, one failure mode |
The Docker multi-stage build in the Dockerfile is equally important — the final image copies only the installed packages, not the build toolchain, keeping it under 150 MB and shrinking the attack surface.
Comments
Comments coming soon. Set up Giscus on the repo to enable them.