Your app should not be listening on 443
On Unix, ports below 1024 require root, and you do not want your application code running as root. So the shape almost everybody ends up with is this: your app listens on a high port (3000, 8000, 8080) as an ordinary user, and something else listens on 80 and 443 and forwards to it.
That something is a reverse proxy. On a virtual machine it is nginx, Caddy or Traefik, all free. On a managed platform it is the platform’s own edge, and you never configure it, but it is still there and it still has opinions.
What it buys you:
- TLS terminates in one place, so certificates are one component’s problem.
- Static files are served without ever waking your runtime.
- Compression, request size limits and connection handling happen before your code.
- Several applications share one machine and one IP.
- You can restart your app without dropping the connection the browser already holds.
127.0.0.1 versus 0.0.0.0
127.0.0.1 means “accept connections that originate on this machine only”. 0.0.0.0 means “accept on every network interface”.
Bind to 127.0.0.1 when a proxy on the same machine is the only thing that will ever connect. It is a free firewall.
Bind to 0.0.0.0 inside a container. This is the single most common “it works on my laptop and times out in Docker” bug: the container publishes port 3000, the request arrives on the container’s network interface, and your server is listening only on the container’s own loopback. Nothing is wrong with your code, your networking or the image. One address is wrong.
You have just lost the client’s IP address
Every request now arrives from the proxy, so your logs fill with 127.0.0.1. The proxy is supposed to tell you who it was, in headers:
X-Forwarded-For: 203.0.113.7, 198.51.100.4
X-Forwarded-Proto: httpsX-Forwarded-For is a list, appended to at each hop. Here is the part people get wrong in both directions. It is an ordinary header: anyone can send X-Forwarded-For: 1.2.3.4 in their first request. Trust only the entry that a proxy you control appended: in practice, the rightmost one, not the leftmost. Behind Cloudflare, use CF-Connecting-IP, which Cloudflare overwrites rather than appends.
Get this wrong one way and every visitor looks like the same client, so your rate limiter throttles the whole internet as one person. Get it wrong the other way and the limiter is bypassed by typing a fake header.
Most frameworks have a “trust proxy” setting. It is off by default for exactly this reason, and turning it on blindly is how the bypass appears.
The defaults that will bite you
These are nginx’s, and every proxy has an equivalent:
client_max_body_sizedefaults to 1 MB. A larger upload gets a 413 and your application never sees the request, so there is nothing in your logs to find.proxy_read_timeoutdefaults to 60 seconds. A slow report returns a 504 to the user while your app finishes happily and logs a 200. Two logs disagreeing about the same request is the signature of a proxy timeout.- Response buffering is on. Server-sent events and streamed responses arrive all at once at the end, or never. You need
proxy_buffering offand oftenX-Accel-Buffering: no. - WebSockets need explicit permission to upgrade (
UpgradeandConnectionheaders passed through) or the handshake fails with a plain 400. - Header size limits produce a 431 once someone’s cookie jar gets fat.
The redirect loop you get exactly once
The proxy terminates TLS and speaks plain HTTP to your app. Your app therefore believes it is running on HTTP. It builds an absolute redirect: http://example.com/dashboard. The proxy sees an HTTP request and redirects it to HTTPS. Which reaches your app, which believes it is on HTTP, which redirects to http://.
ERR_TOO_MANY_REDIRECTS. The fix is to trust X-Forwarded-Proto when generating URLs, or to emit relative redirects and let the browser keep the scheme.
Two lines of Caddy
addaly.com {
reverse_proxy 127.0.0.1:3000
}That is a working configuration including a certificate, its renewal, HTTP-to-HTTPS redirection and modern TLS settings. nginx will ask for thirty lines and give you finer control, and far more of the internet’s accumulated knowledge is written about nginx. Both are free; start with whichever you can read.
Every lesson here is open. Your progress is recorded once you pass the Agents and Automation exam.
Check your understanding
You containerise a Node app that works perfectly on your laptop. The container starts with no errors, docker ps shows port 3000 published, and the log line says “listening on 3000”, but every request from the host hangs and then times out. What is the most likely cause?
Pick the one you would defend. Your answer stays private.
Stuck on something?Ask Tri, your AI tutor