Web servers

nginxkuberneteshttp

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

Reverse proxies and gateways

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 nginxinc organisation to nginx. 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.

Related

Keyboard shortcuts

/Open search
CtrlKOpen search (Cmd+K on macOS)
?Show this cheat sheet
Ctrl/Show this cheat sheet (Cmd+/ on macOS)
EscClose search or this cheat sheet

Turns off the single-key shortcuts (/, ?). Ctrl/Cmd+K and Ctrl/Cmd+/ stay.