Fix nginx crash-loop on a fully fresh docker compose up
Some checks failed
CI/CD / build-test-deploy (push) Has been cancelled

nginx resolves a plain `proxy_pass http://server:8080;` hostname once at
config-load time. On a cold `docker compose up` that creates every
container at once, the `server` container isn't always registered in
Docker's embedded DNS yet by the time the client's nginx starts — nginx
then fails immediately ("host not found in upstream") and doesn't retry,
so it just crash-loops.

Route the target through a variable instead (`set $upstream_server ...;
proxy_pass $upstream_server;`) with an explicit `resolver`, which makes
nginx resolve the hostname lazily per-request via Docker's DNS rather than
once at startup. Also add `depends_on: [server]` on the client service so
it at least doesn't start before the server container exists at all.

Verified with repeated `docker compose down && docker compose up` cycles
locally — this only reliably reproduced starting every container from
nothing simultaneously, which prior local testing hadn't actually done
(the server container had usually stayed running across rebuilds).
This commit is contained in:
danamir
2026-08-06 14:44:52 +03:00
parent 773dbb493e
commit 154411e7a4
2 changed files with 21 additions and 4 deletions

View File

@@ -37,6 +37,11 @@ services:
build: build:
context: . context: .
dockerfile: src/Client/Dockerfile dockerfile: src/Client/Dockerfile
# Doesn't fix the DNS race on its own (nginx.conf's lazy resolver does
# that) — just avoids nginx crashing into a restart loop for no reason
# by not starting client before `server` exists at all.
depends_on:
- server
# Loopback-only in production, where a host-level nginx (TLS + the real # Loopback-only in production, where a host-level nginx (TLS + the real
# domain) is the actual public entry point and proxies here — see # domain) is the actual public entry point and proxies here — see
# docs/DEPLOY.md. For local `docker compose up`, still reachable at # docs/DEPLOY.md. For local `docker compose up`, still reachable at

View File

@@ -2,11 +2,23 @@ server {
listen 80; listen 80;
root /usr/share/nginx/html; root /usr/share/nginx/html;
# No trailing slash on proxy_pass here — the full matched URI (including # `server` is resolved as a variable (not a plain hostname in proxy_pass)
# the /api/ prefix) is forwarded unchanged, matching Giraffe's route # so nginx uses Docker's embedded DNS resolver *lazily, per request*
# definitions in Server/Routes.fs, which all start with /api/. # instead of resolving it once at config-load time. A static
# `proxy_pass http://server:8080;` fails permanently at nginx startup
# ("host not found in upstream") if the `server` container isn't already
# registered in Docker's DNS yet — a real race on a fully fresh
# `docker compose up` that starts every container at once, since nginx
# doesn't retry once it's already crashed.
resolver 127.0.0.11 valid=10s;
# No trailing slash on the proxy_pass target — the full matched URI
# (including the /api/ prefix) is forwarded unchanged, matching
# Giraffe's route definitions in Server/Routes.fs, which all start
# with /api/.
location /api/ { location /api/ {
proxy_pass http://server:8080; set $upstream_server http://server:8080;
proxy_pass $upstream_server;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;