Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

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.

Levels: Advanced ~30 min read

Advanced

What you will learn

  1. The four defaults a container revokes: PID 1, logs, temp paths, config location.

  2. The correct NGINX container contract, line by line.

  3. Signals, shutdown, and why worker_shutdown_timeout is the container's exit story.

  4. 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)

docker stop waits 10s then SIGKILLs — in-flight requests die

ensure nginx IS PID 1 (or the entrypoint execs it); see §3

Logs go nowhere

docker logs shows nothing; JSON file logs vanish with the container

symlink access/error logs to /dev/stdout, /dev/stderr

Temp dirs are ephemeral/missing

[emerg] mkdir() "/var/cache/nginx/proxy_temp" failed — the classic first-boot error in a minimal image!

writable temp paths, or the client_body_temp_path overrides pointed at a writable location

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):

dockerfile
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:

bash
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.0

Why each line earns its place:

  • daemon off is 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 is nginx -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

SIGTERM

docker stop / pod termination (unless the image overrides STOPSIGNAL)

fast shutdown: exit immediately — no drain, in-flight requests dropped

instant — the dropped-request risk, not a grace overrun

SIGQUIT

you, manually — or docker stop when the image sets STOPSIGNAL SIGQUIT (the official image does)

graceful: wait for ALL in-flight to complete

can outlive the grace period → SIGKILL mid-response

SIGHUP

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:

nginx
worker_shutdown_timeout  8s;   # after 8s of graceful draining, force close

That 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:

text
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 runs nginx -t against every PR — Lesson 14.4's gen-config.sh validating its render before writing it is the same gate, in miniature.

  • nginx -t in CI, not in prod — run nginx -t before 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=5 through 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 off as PID 1, logs symlinked to stdout/stderr, config and certs mounted read-only, secrets never baked.

  • worker_shutdown_timeout bounded 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.

Was this page helpful?

Mark this learning resource complete to track your learning progress.