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).
- docker-compose.yml: bind client/server ports to 127.0.0.1 only — the
client container is the sole intended public entry point (it proxies
/api/* to the server itself), a host-level nginx sits in front of it in
production. Client__Origin now configurable via CLIENT_ORIGIN env var.
- docs/DEPLOY.md: step-by-step for a fresh Linux target (Docker install,
clone, .env secrets, nginx + certbot).
- docs/CI-CD.md: runner registration for that same target machine, in
"host" mode so the deploy step's `docker compose up -d` acts on the
actual running stack.
Full-stack F# (Domain/Server/Client via Fable+Elmish+Feliz), PostgreSQL
persistence via Dapper, Docker Compose deployment. Student quiz-taking flow
with time-limit enforcement and focus-loss tracking, Teacher question bank
and quiz builder with results analytics, Admin user management.