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.

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