14.2 NGINX in Containers
A containerized NGINX is the same binary and same config inside an environment that quietly removes four things the package install gave you. This is the checklist that keeps it production-safe.
Advanced
What you will learn
The four defaults a container revokes: PID 1, logs, temp paths, config location.
The correct NGINX container contract, line by line.
Signals, shutdown, and why
worker_shutdown_timeoutis the container's exit story.Immutability and configuration injection — config as a mounted artifact.
1. What the container revoked
The
nginx -V receipt (Lesson 1.3) shows compiled defaults —
/var/log/nginx,
/var/cache/nginx, prefix paths — all of which the package install created, permissioned, and wired to logrotate. The container gives you none of it:
Revoked | Symptom when forgotten | Fix |
|---|---|---|
PID 1 is nginx (maybe) |
| ensure nginx IS PID 1 (or the entrypoint execs it); see §3 |
Logs go nowhere |
| symlink access/error logs to
|
Temp dirs are ephemeral/missing |
| writable temp paths, or the
|
Config is baked or mounted | "fix it in the container" anti-pattern | mount config; treat image as immutable |
That fix generalizes: redirect temp paths into a writable prefix (
tmp/client_body,
tmp/proxy, …) — a minimal container has no writable
/var/cache/nginx. An entrypoint that runs
mkdir -p on
tmp/ and
logs/ before launch
is the container entrypoint's whole job, in one line.
2. The contract, line by line
A production-shaped Dockerfile (the same mechanics, the industry's spelling):
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends nginx \
&& rm -rf /var/lib/apt/lists/*
# 1) logs to the container's stdout/stderr — the runtime collects them
RUN ln -sf /dev/stdout /var/log/nginx/access.log \
&& ln -sf /dev/stderr /var/log/nginx/error.log
# 2) entrypoint execs nginx as PID 1, in the foreground
ENTRYPOINT ["/usr/sbin/nginx", "-g", "daemon off;"]Then at run time:
docker run -d --name edge \
-v $PWD/conf:/etc/nginx/conf.d:ro \ # config: mounted, read-only
-v $PWD/certs:/etc/nginx/certs:ro \ # secrets: mounted, never baked
-p 443:443 \
nginx-edge:1.0Why each line earns its place:
daemon offis what makes nginx a foreground process — which is what makes it PID 1 — which is what makes signals arrive (§3). Without a container, the equivalent isnginx -g 'daemon off;'— the binary in the foreground, owning its pid file.stdout/stderr symlinks: Module 12's log-as-data story flows into
kubectl logs/docker logs/ your platform's collector, with zero file tailers. (The trade: you lose per-file separation on disk — split by format at the shipper, which the JSON lines make trivial.)Read-only mounts: the running instance cannot modify its own config — immutability as a security property, not a slogan. Log volumes (if any) are the only writable paths.
No baked secrets: certificates via mounted volumes or platform secret stores; an image layer containing a private key is an incident-in-waiting (any registry pull extracts it).
3. Signals: the exit story
Module 1's signal table, containerized:
Signal | Who sends it | NGINX does | The container race |
|---|---|---|---|
| docker stop / pod termination (unless the image overrides
| fast shutdown: exit immediately — no drain, in-flight requests dropped | instant — the dropped-request risk, not a grace overrun |
| you, manually — or
| graceful: wait for ALL in-flight to complete | can outlive the grace period → SIGKILL mid-response |
| you, manually | reload workers | not part of the platform lifecycle — your controller sends it |
The trap: on the
STOPSIGNAL SIGQUIT path, a slow upstream (2s responses, Lesson 4.6’s timeout tours) plus a 10s stop grace usually fits — a 30s one plus streaming responses might not. The fix is bounding nginx’s own patience so
it exits before the runtime kills it:
worker_shutdown_timeout 8s; # after 8s of graceful draining, force closeThat single directive converts "sometimes SIGKILLed mid-flight" into "bounded drain, then clean exit" — set it a few seconds inside your platform's termination grace. (It's the same philosophy as Module 10's
proxy_timeout: pick your own deadline before the environment picks it for you.)
The other half of the story is
readiness: the container should only receive traffic when nginx can serve it. The
/healthz location the mini-controller renders (Lesson 14.4) is exactly what platform probes hit —
access_log off on it, cheap, dependency-free.
4. Config as a mounted artifact
The image ships nginx the program; everything else arrives by mount. That separation is what makes the image reusable across environments and what makes Lesson 14.4's controller pattern work in containers:
image = program + contract (entrypoint, paths, log wiring)
mount = config + certs (+ any writable state)Production corollaries:
Version configs like code — they are code (Module 11 proved it:
if-shaped landmines included). Git, review, CI that runsnginx -tagainst every PR — Lesson 14.4'sgen-config.shvalidating its render before writing it is the same gate, in miniature.nginx -tin CI, not in prod — runnginx -tbefore every launch, because a broken config should fail a pipeline, not a rollout.Environment differences belong in variables and templates, not in divergent files — the manifest→render pipeline from Lesson 14.4 is the scalable answer; hand-maintained per-env configs are how "just one small prod-only change" incidents are born.
Try it yourself
A non-root, prefix-isolated install is a container rehearsal: writable
tmp/+logs/, config tested before launch,daemon off(the binary runs in the foreground of a controlled shell).Meet the revoked defaults on purpose: comment out the temp-path stanza where the compiled-in cache path isn't writable (a container, or any locked-down host), then
systemctl restart nginx— the[emerg] mkdir()error is the container lesson, pre-installed.Practice the drain: set
worker_shutdown_timeout 2s;, start a slow request (/slow/?delay=5through any proxying module),nginx -s quit— watch the bounded drain in the error log.
Q&A
Q: Root or non-root container? A: Non-root (ports >1024, or
cap_net_bind_service). NGINX runs fine unprivileged — every config in this series is a non-root config already.
Q: One container, many vhosts? A: Yes — it's just many
server{} blocks (Module 2). The container boundary and the vhost boundary are independent axes; conflate them and you'll spawn containers to solve a config problem.
Q: How do I reload config in a container? A:
docker exec edge nginx -s reload works (signal to PID 1's children), but the
controller-shaped answer is: new config lands via mount/secret update, and something (controller, sidecar, you) fires the reload — Lesson 14.4's loop, in a container. Or roll new pods with the new config and let the platform drain the old — often
cleaner, because it reuses the health-based rollout machinery.
Q: Where do uploaded bodies go in a container? A:
client_body_temp_path — if the container filesystem is read-only, that must be a writable volume (emptyDir), or big uploads meet Module 9's spill receipts with nowhere to spill to. Size the volume for your worst-case concurrent body spill.
Takeaways
A container revokes four defaults — PID-1 signals, log files, temp paths, config location — and each revocation has a one-line fix, covered one by one above.
The contract:
daemon offas PID 1, logs symlinked to stdout/stderr, config and certs mounted read-only, secrets never baked.worker_shutdown_timeoutbounded inside the platform's termination grace is the difference between graceful drain and SIGKILL mid-response.Image = program; mount = policy. Version configs like code, test them in CI with
nginx -t, and let the 14.4 render pipeline own per-environment differences.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.