fairlane.systems

NGINX · TECH STACK

Nginx as reverse proxy: SSL, rate limits, and security headers for containerised apps

Nginx 1.28 fronts Docker backends as an edge layer. Certbot SSL, gzip/brotli, limit_req zones, Cloudflare IP allowlist, HSTS/CSP, websocket upgrade.

Researched & fact-checked by: · As of: 2026-05

What is an nginx reverse proxy?

A reverse proxy is the first server a browser sees when it visits a domain. It accepts TLS connections, validates requests, terminates SSL, forwards decrypted traffic to internal backend processes, and caches or compresses the response on the way back. Nginx has been the de-facto standard in this role for over 15 years because it holds thousands of open connections with a few MB of RAM and its configuration is declarative and well-documented.

For a Swiss SME running Docker containers, nginx covers exactly what Cloudflare or a managed cloud load balancer would do – only locally and without a monthly subscription. As of May 2026, version 1.28 is mainline; it ships native HTTP/3 (QUIC), improved OCSP-stapling stability, and a new `aio threads` for faster file IO. On the Fairlane Hetzner server, a single nginx instance fronts 58 sites – from static marketing pages through Next.js apps to n8n webhooks – all with Lets-Encrypt certificates via Certbot, all behind Cloudflare DNS.

Why it matters

Without a reverse proxy, every Docker container would have to manage its own TLS certificates, implement its own rate limits, and set its own security headers – none of which container images handle well out of the box. A central edge layer solves it in one place and offloads the application containers.

Concrete effects for an SME: certificates are managed centrally (Certbot renewal every 60 days, one cron job). Rate limiting on /api/login automatically blocks brute-force attempts against every backend container. The HTTP-to-HTTPS redirect is defined once, not per app. Static assets receive Cache-Control headers so that Cloudflare or the browser can cache them efficiently. During maintenance, a container can be set to 503 without affecting the rest of the domain. In a Swiss revDSG audit, it is provable where personal data leaves the perimeter – precisely in the nginx config, not scattered across 25 containers.

How it works

A production-ready nginx configuration has three layers: global server defaults, one server block per domain, and location blocks for path-specific behaviour.

SSL termination with Certbot. `certbot --nginx -d example.ch -d www.example.ch` drops the certificate into /etc/letsencrypt/live/ and patches the server block. A systemd timer (or cron `0 3 * * *`) auto-renews from 30 days remaining. Important: ssl_protocols TLSv1.2 TLSv1.3 – disable TLSv1.0 and TLSv1.1 to stay PCI-DSS and BSI-2026 compliant.

Gzip plus brotli. Gzip is out of the box; brotli arrives via the `libnginx-mod-http-brotli` package. Brotli compresses HTML/JSON about 20% better than gzip; enable both – nginx picks based on `Accept-Encoding`.

Rate limiting via limit_req_zone. `limit_req_zone $binary_remote_addr zone=login:10m rate=5r/s;` defines a zone. In the `/api/login` location: `limit_req zone=login burst=10 nodelay;`. That targets login endpoints specifically, not blanket – which would throttle static assets.

Cloudflare IP allowlist plus real-IP. When Cloudflare sits in front as DNS proxy, backends see only Cloudflare IPs. `set_real_ip_from <cloudflare-cidr>;` plus `real_ip_header CF-Connecting-IP;` restores the original visitor IP. Without it, every audit log shows only 173.x.x.x – useless for forensics. The Cloudflare CIDR list is stable enough to refresh monthly via cron.

Security headers. HSTS (`max-age=31536000; includeSubDomains; preload`), CSP tailored per app, X-Frame-Options DENY, Referrer-Policy strict-origin-when-cross-origin, restrictive Permissions-Policy. Mozilla Observatory grades it in 30 seconds – target A+.

Websocket upgrade. For n8n, Grafana, and Next.js HMR you need `proxy_http_version 1.1;`, `proxy_set_header Upgrade $http_upgrade;`, `proxy_set_header Connection upgrade;`. Without those three lines, live updates do not work.

Nginx edge in 6 steps

  1. 01Install nginx 1.28 plus libnginx-mod-http-brotli (mainline repo, not distro default).
  2. 02One /etc/nginx/sites-available/<domain>.conf per domain, symlink to sites-enabled/. Never dump everything into nginx.conf.
  3. 03Certbot with the nginx plugin: certbot --nginx -d <domain>. Verify the renewal cron.
  4. 04limit_req_zone in the http{} block; limit_req on login and upload locations. Set burst and nodelay.
  5. 05Cloudflare CIDR + real_ip_header CF-Connecting-IP. A monthly cron refreshes the list.
  6. 06Security headers in an includeable snippet (/etc/nginx/snippets/security.conf), pulled into every server block. Verify with Mozilla Observatory.

When to use nginx

Nginx is the right choice when (a) at least one Docker container should be publicly reachable over HTTPS, (b) multiple domains land on one host, or (c) existing apps without TLS support need to be exposed.

Concrete use cases: an n8n instance that receives webhooks from Cobra CRM or Bexio – nginx terminates SSL and Cloudflare absorbs DDoS. A Grafana instance restricted to the office IP – `allow 192.0.2.0/24; deny all;` directly in the location block. A marketing site and a RAG chat backend on the same domain – `/` to Next.js, `/api/chat` to a Python container.

At Fairlane, one nginx instance fronts 58 sites with roughly 240 locations and emits about 15 MB of logs per day. RAM usage stays below 80 MB, CPU in single-digit percent. That scales comfortably on a server between 64 and 128 GB of RAM.

When not to use

Nginx is the wrong choice when (a) a managed edge like Cloudflare Tunnels or Vercel already covers TLS plus CDN plus WAF and the host never needs to be on the public internet directly, (b) a high-throughput L4 load balancer (databases, MQTT) is needed – HAProxy or Envoy fits better, or (c) a full application firewall with regex-based attack rules is required – for that, ModSecurity plus OWASP CRS is the right stack, ideally as a module in front of nginx.

More pitfalls: using nginx as a Tomcat replacement for Java servlets by `proxy_pass`ing to random internal JBoss ports without testing session affinity. Copy-pasting CSP headers from internet tutorials without a CSP-Report-Only phase – the app breaks immediately. And `client_max_body_size 0;` means unlimited, which on a public upload endpoint is an open door for disk-fill attacks.

Trade-offs

STRENGTHS

  • One config file per domain, versionable in Git
  • TLS and rate limiting centralised, not per app
  • Low resource footprint – 80 MB of RAM for 58 sites
  • 15 years of documentation; every question has a known answer

WEAKNESSES

  • No auto-discovery – new containers need a manual conf file
  • CSP and security headers are non-trivial to get right
  • A reload error kills the whole nginx instance: nginx -t before every reload is mandatory
  • OpenResty/Lua modules tempt you into too much custom logic at the edge

FAQ

Why nginx if Cloudflare already terminates TLS?

Cloudflare terminates TLS to the browser, but the connection to the origin server needs its own TLS – otherwise a plaintext hop sits between Cloudflare and Hetzner over the public internet. Nginx with a valid origin cert (Lets-Encrypt or Cloudflare Origin Cert) closes that gap. Nginx also routes locally between multiple backend containers – Cloudflare does not.

Should I use Caddy or Traefik instead of nginx?

Caddy is the simplest pick for one to five domains with auto-SSL and no certbot. Traefik integrates straight into Docker labels and configures itself when containers start. Both are solid. Nginx wins on many-domain setups (>20), fine-grained rate limiting, lots of custom locations, and long-lived documentation. With 58 sites at Fairlane, nginx beats both on performance and doc availability.

How do I debug 502 Bad Gateway?

Three causes in 95% of cases: (1) backend container unreachable – `docker ps` shows whether the container runs and what port is published. (2) Wrong proxy_pass URL – use the container name plus container port, not the host port. (3) Timeout – `proxy_read_timeout 60s;` is enough for normal apps, raise to 300s for LLM calls. error.log in /var/log/nginx/ gives the exact line.

Related topics

DOCKER · TECH STACKDocker orchestration for SMEs: docker-compose without Kubernetes overkillCLOUDFLARE · TECH STACKCloudflare as DNS, reverse proxy, and WAF: SSL modes, cache rules, origin certificatesSERVER & INFRASTRUCTURE · SERVICEServer & Infrastructure: Ubuntu, Docker, monitoring – set up, hardened, handed overHETZNER · TECHHetzner as EU hosting for Swiss fiduciaries and SMEs: data centres, contracts, costMANAGED · SERVICEManaged Service & Monitoring: we keep it running, you use it

Sources

  1. nginx.org – Configuring HTTPS servers and rate limiting · 2026-04
  2. Certbot – User Guide (EFF) · 2026-03
  3. Mozilla SSL Configuration Generator – Intermediate profile · 2026-05
  4. OWASP Secure Headers Project · 2026-02

FITS YOUR STACK?

What this looks like in your business – a 30-minute intro call.

Book a call