Render Safe Shell Templates with envsubst
One unset variable and envsubst quietly writes an empty string into your config, so learn to render templates without that surprise. You will render a text template from environment variables, limit substitution to an explicit allow-list, and check the result before using it. This guide uses GNU gettext-runtime 0.21, provided here by Ubuntu package gettext-base version 0.21-14ubuntu2. Allow about ten minutes. You need a shell and a template file. No elevated privileges are required.
The route
Jump straight to the step you need, or tick off Done means at the end.
1. Check the installed command
Start with read-only checks. They confirm the executable and the version this guide describes:
$ command -v envsubst
/usr/bin/envsubst
$ envsubst --version
envsubst (GNU gettext-runtime) 0.21
$ dpkg-query -W -f='${Package} ${Version}\n' gettext-base
gettext-base 0.21-14ubuntu2
envsubst reads standard input and writes standard output. In normal mode it replaces references written as $VARIABLE or ${VARIABLE}. It does not edit a file in place, and it does not evaluate the input as shell code.
2. Create a small template
Keep the template as ordinary text. This example uses a temporary working directory so the original content is easy to spot:
$ mkdir -p /tmp/envsubst-demo
$ cat > /tmp/envsubst-demo/app.conf.in <<'EOF'
service_name=$SERVICE_NAME
listen_address=$LISTEN_ADDRESS
listen_port=${LISTEN_PORT}
EOF
The quoted heredoc marker is a shell detail. It stops your current shell expanding the variables while the template is created, so the file holds the literal placeholders. Check that before rendering:
$ sed -n '1,5p' /tmp/envsubst-demo/app.conf.in
service_name=$SERVICE_NAME
listen_address=$LISTEN_ADDRESS
listen_port=${LISTEN_PORT}
Warning
Do not put passwords or tokens into a template you plan to display, log or commit. The rendered output contains the actual values, so treat it with the same care as the environment that supplied them.
3. Set non-sensitive example values
Export values in the shell that will run envsubst. These are harmless local settings:
$ export SERVICE_NAME='demo-api'
$ export LISTEN_ADDRESS='127.0.0.1'
$ export LISTEN_PORT='8080'
An exported variable is visible to child processes. Quote the assignment if a value contains spaces. A variable that exists in the shell but is not exported is invisible to envsubst.
Checkpoint
Verify only the values you mean to use. Avoid printing a whole real environment, because it may contain credentials:
$ printf '%s\n' "$SERVICE_NAME" "$LISTEN_ADDRESS" "$LISTEN_PORT"
demo-api
127.0.0.1
8080
4. Render all recognised references
Pipe the template into envsubst and redirect its standard output to a new file:
$ envsubst < /tmp/envsubst-demo/app.conf.in > /tmp/envsubst-demo/app.conf
$ sed -n '1,5p' /tmp/envsubst-demo/app.conf
service_name=demo-api
listen_address=127.0.0.1
listen_port=8080
Without a SHELL-FORMAT argument, every recognised variable reference in the input gets replaced. An unset variable becomes an empty string. That default is easy to miss: a typo can give you a plausible-looking but incomplete configuration.
Check the result before you hand it to another program:
$ test -s /tmp/envsubst-demo/app.conf && echo 'rendered file is non-empty'
rendered file is non-empty
$ grep -F 'listen_port=8080' /tmp/envsubst-demo/app.conf
listen_port=8080
5. Restrict substitution to an allow-list
Use the optional shell-format argument when the input may contain dollar signs that must stay literal, or when you want the substitution contract to be obvious in a script:
$ cat > /tmp/envsubst-demo/mixed.txt <<'EOF'
name=$SERVICE_NAME
port=${LISTEN_PORT}
literal=$DO_NOT_RENDER
EOF
$ envsubst '$SERVICE_NAME ${LISTEN_PORT}' < /tmp/envsubst-demo/mixed.txt
name=demo-api
port=8080
literal=$DO_NOT_RENDER
Only the variables named in that format are substituted. The single quotes are essential: they stop the current shell expanding the format before envsubst receives it. The format itself is not output. It only selects names for the input stream.
Tip
In a script, keep the allow-list next to the template so a later edit cannot silently expose a newly added variable. Do not use an unreviewed, broad environment as a transport for secrets.
6. List variable names without rendering
--variables ignores standard input and prints the variable names found in the shell format you supply, one per line:
$ envsubst --variables '$SERVICE_NAME ${LISTEN_PORT} $SERVICE_NAME'
SERVICE_NAME
LISTEN_PORT
SERVICE_NAME
Duplicates are kept. This suits a review step, but it does not check a template for completeness unless you deliberately give it the same format. A format is required for --variables, and standard input is ignored in this mode.
7. Handle missing values and output files safely
envsubst has no required-variable or default-value syntax. It does not turn ${NAME:-fallback} into shell default-value behaviour, because only shell-format variable references are substituted. If a value must exist, check it before rendering:
$ : "${SERVICE_NAME:?SERVICE_NAME must be set}"
$ : "${LISTEN_PORT:?LISTEN_PORT must be set}"
$ envsubst '$SERVICE_NAME ${LISTEN_PORT}' < /tmp/envsubst-demo/app.conf.in > /tmp/envsubst-demo/app.conf
These are ordinary shell checks, not an envsubst option. They stop the command line before rendering when a value is missing. They do not check that a port is numeric or an address is usable.
Warning
Shell redirection truncates its destination before the command starts. Before replacing a real configuration, render to a separate file and inspect it. If the destination matters, back it up first:
$ cp --preserve=all /path/to/app.conf /path/to/app.conf.bak
$ envsubst '$SERVICE_NAME ${LISTEN_PORT}' < /path/to/app.conf.in > /path/to/app.conf.new
$ sed -n '1,20p' /path/to/app.conf.new
$ mv /path/to/app.conf.new /path/to/app.conf
The mv is the state-changing step.
Recovery
If the rendered file is wrong, restore the backup with cp --preserve=all /path/to/app.conf.bak /path/to/app.conf. Do not remove the backup until the consuming service has been checked, because deleting it is irreversible.
Done means
- Version confirmed.
envsubstis GNU gettext-runtime 0.21, or you have checked the differences in your installed version. - Template clean. It contains only the variable references you intend to render.
- Inputs guarded. Values are exported, missing required values are rejected before rendering, and secrets are not printed or committed.
- Allow-list used. An explicit shell format is in place wherever literal dollar references must survive.
- Output reviewed. You read the rendered file before any service reload or configuration replacement.