Web servers
Nginx
add_header replaces, it does not merge
The rule, from the ngx_http_headers_module documentation:
add_header directives are inherited from the previous configuration level if and only if there
are no add_header directives defined on the current level. One add_header in a location
wipes out every add_header set in the enclosing server and http blocks, silently.
An if block inside a location is a configuration level of its own. So as soon as it contains an
add_header, requests matching it get only the headers defined inside the block, and anything set
outside is dropped for those requests. A header that must apply in both cases has to be repeated
inside and outside the if.
Reproduced on nginx 1.31.7, 1.30.6 and 1.24.0, with one header set at each level. A location that defines no
add_header of its own returns the server level header and not the http one, because the
server level already replaced it: the rule applies at every level, not just the deepest. A
location that defines one returns only its own. And in a location whose if block defines one, a
request matching the if gets only the if block header, while a request that does not match
still gets the location’s.
location @backend {
internal;
if ($request_filename ~* \.(jpg|jpeg|gif|png|bmp|ico|pdf|flv|swf|txt|css|js|otf|eot|svg|ttf|woff|woff2|map)$) {
expires 7d;
add_header "Cache-Control" "public";
add_header "Access-Control-Allow-Origin" "*";
access_log off;
}
include rdbp/proxy.conf;
proxy_pass http://ngxps-backend:3080;
proxy_pass_header Cache-Control;
}
Since nginx 1.29.3 the duplication is avoidable: add_header_inherit changes the inheritance rule
for the level it sits on.
add_header_inherit merge; # keep the parent level's headers, appended after this level's
add_header_inherit off; # inherit nothing from the parent level
add_header_inherit on; # default: replace as soon as this level defines one add_header
Put add_header_inherit merge; at the http level and it applies recursively to every nested
level until something redefines it. add_trailer_inherit does the same for add_trailer, and
appeared in the same version. Available in the current stable branch (1.30.x) and mainline
(1.31.x), not in the older long-lived branches, so check what your distribution ships before
relying on it.
Distributions lag well behind: the nginx that ships with Ubuntu 24.04 is 1.24.0, and it rejects
the directive outright with unknown directive "add_header_inherit", at configuration test time
rather than at runtime.
add_header is skipped on most error responses
By default add_header only fires when the response code is one of 200, 201, 204, 206, 301, 302,
303, 304, 307 or 308. Anything else, a 404 or a 502 coming back from the backend, goes out without
the header. The always parameter lifts that condition.
add_header X-Cache-Note "set" always;
Verified on 1.31.7, 1.30.6 and 1.24.0: without always, the header is on the 200 and absent from the 404 of the same
location; with always, it is on both. This is the one to remember for security headers, which are
usually wanted on error pages too.
expires and add_header both write Cache-Control
expires 7d emits an Expires header and a Cache-Control: max-age=604800. An add_header
for Cache-Control next to it does not replace that value, it appends a second header line:
Expires: Mon, 12 Oct 2026 04:14:05 GMT
Cache-Control: max-age=604800
Cache-Control: public
Two field lines with the same name read as one comma separated list, so public landing next to
max-age is harmless, and it is what the configuration above means. A contradictory value is not:
add_header "Cache-Control" "no-store" beside the same expires sends max-age=604800 and
no-store side by side, so the caches on the path get two conflicting directives instead of the
one the config appears to set; RFC 9111 tells them to honour the most restrictive, so no-store
usually wins, but that is the cache’s call, not yours. Both outputs verified on 1.31.7, 1.30.6 and
1.24.0. To own the whole value, drop expires and write the single header.
Tuning for production
- NGINX tuning for best performance, a third party checklist (worker processes, buffers, timeouts, TLS), also kept as a repository at denji/nginx-tuning. Read it as a list of knobs to look up, not as a config to paste.
Reverse proxies and gateways
- Traefik, written in Go. It is a Kubernetes ingress controller among other things: it watches Ingress objects and derives its routing from them (Kubernetes Ingress provider).
- Kong Gateway, API gateway, also available as an ingress controller.
- Application Servers
Kubernetes ingress
The two nginx based ingress controllers are not interchangeable, and one of them is retired:
- kubernetes/ingress-nginx, the community controller, is retired. Best effort maintenance stopped in March 2026 and the repository was archived on 2026-03-24: no more releases, no bugfixes, no security fixes. Existing deployments keep running and the published charts and images stay available, but new clusters should pick a Gateway API implementation instead. Traefik ships a dedicated provider that reads the same annotations, which makes it a migration path.
- nginx/kubernetes-ingress is the one maintained
by nginx itself (F5), for both NGINX and NGINX Plus. It moved from the
nginxincorganisation tonginx. Its templates are worth reading even outside Kubernetes, as a source of production configuration.
See also Kubernetes.
Checked against the official nginx documentation (stable 1.30.6, mainline 1.31.7), and the
inheritance, always and Cache-Control behaviour reproduced on nginx 1.31.7, 1.30.6 and 1.24.0.