Does nginx Docker image envsubst preserve $host variable without explicit escaping?
0 reputation · 29 Dec 2024, 15:06 UTC
0 reputation · 29 Dec 2024, 15:06 UTC
A team wants a repeatable nginx development environment using the official Docker image's built-in templating: *.template files in /etc/nginx/templates/ are rendered to /etc/nginx/conf.d/ via envsubst at container start. The goal is to keep configuration identical across machines without hand-editing files.
nginx configuration uses its own variables such as $host and $uri. The entrypoint's envsubst substitutes environment variables by default, and the NGINX_ENVSUBST_FILTER variable controls which names are eligible. Documentation indicates that literal $ signs in templates can be clobbered unless escaped or excluded, but the exact escaping mechanism ($$host vs. filter exclusion) and its stability across image versions (e.g., 1.25 vs 1.27) are not clearly pinned in the image's own docs.
Teams must decide whether to rely on NGINX_ENVSUBST_FILTER to protect nginx variables, escape them in the template, or avoid templating for files that contain them. The image's changelog shows adjustments to the default filter and escaping logic, so behavior may differ between tags.
Which approach reliably preserves $host in the rendered config across pinned image versions? Does the default NGINX_ENVSUBST_FILTER exclude nginx variables automatically, or must they be explicitly listed? Is $$host the documented escape sequence for current 1.27.x images?
32775 reputation · 29 Dec 2024, 19:56 UTC
No — with the default configuration, the nginx Docker image's envsubst step does not reliably preserve $host. If an environment variable named host exists in the container, it gets substituted; if it doesn't, envsubst typically replaces $host with an empty string, silently breaking your config. The reliable, version-independent fix is to escape nginx variables as $$host in the template. NGINX_ENVSUBST_FILTER works too, but it is an allowlist of names to substitute, not a denylist — so it protects nginx variables only indirectly, and its default value has changed between image releases.
The image entrypoint runs envsubst over each /etc/nginx/templates/*.template file and writes the result to /etc/nginx/conf.d/. GNU gettext envsubst has no knowledge of nginx; every $name token looks like a variable reference to it. NGINX_ENVSUBST_FILTER restricts substitution to variable names matching a pattern — when set, only matching names are replaced, which incidentally leaves $host alone. When unset, substitution is effectively unfiltered, so nothing protects nginx-internal variables.
Because the entrypoint script and its defaults have been adjusted across image lines (the 1.25-era and 1.27-era entrypoints are not identical), do not assume the filter default on one tag applies to another. Treat any claim about "the default filter excludes nginx variables" as unverified until you check your pinned tag.
Use the double-dollar escape in templates. It is standard envsubst behavior ($$ renders a literal $) and does not depend on the image's filter default:
server {
listen 80;
server_name ${APP_DOMAIN};
location / {
proxy_pass ${UPSTREAM_URL};
proxy_set_header Host $$host;
proxy_set_header X-Original-URI $$request_uri;
}
}Here ${APP_DOMAIN} and ${UPSTREAM_URL} are substituted from the environment, while $$host renders as the literal $host nginx expects.
The alternative — setting NGINX_ENVSUBST_FILTER to list only your own variables (e.g. NGINX_ENVSUBST_FILTER=APP_DOMAIN|UPSTREAM_URL, depending on the tag's expected format) — also works, but couples correctness to that tag's entrypoint implementation and to you remembering to add every new variable to the filter.
Do not trust documentation summaries; check the actual entrypoint and render output:
# Inspect the filter default in your exact tag
docker run --rm nginx:1.27.2 \
sh -c 'grep -n ENVSUBST_FILTER /docker-entrypoint.d/20-envsubst-on-templates.sh'
# Render a test template and check the output
docker run --rm -e APP_DOMAIN=example.com \
-v "$PWD/templates:/etc/nginx/templates" \
nginx:1.27.2 \
sh -c '/docker-entrypoint.sh nginx -t && cat /etc/nginx/conf.d/*.conf'Confirm the rendered file contains literal $host and that nginx -t passes. Repeat against any second tag you deploy (e.g. 1.25.x) before assuming parity.
This guidance reflects well-established envsubst and entrypoint behavior, but exact filter defaults and escaping handling are version-specific — pin an immutable tag (or digest), verify as above, and re-verify on upgrades. If your rendered config still shows an empty value where $host should be, share the template snippet and the exact image tag; that combination determines which of the two mechanisms is misfiring.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.