envsubst Error: 'envsubst: command not found' — Cause, Fix, and Troubleshooting Guide
Fix 'envsubst: command not found' in scripts and CI — envsubst ships in the gettext package; install gettext-base on Debian or gettext on Alpine, and mind PATH.
- #automation
- #troubleshooting
- #envsubst
Fixing errors like this? Get 500 free DevOps AI prompts
500 copy-paste AI prompts for the stack you actually run — one PDF, free.
Overview
envsubst substitutes environment variables into text — a common way to render config templates in deploy scripts and CI pipelines. The catch is that it is not a standalone tool: it ships as part of the GNU gettext package, and minimal base images (Alpine, debian:slim, distroless-adjacent CI runners) do not include it. So a script that works on your workstation dies inside a container with:
$ ./deploy.sh
Rendering config from template...
./deploy.sh: line 12: envsubst: command not found
Line 12 is where the script called envsubst. The binary simply is not on PATH because gettext (or its runtime subset) was never installed in that environment. This almost always surfaces first in CI or a freshly built container, not on the developer machine where gettext came in as a dependency of something else. The fix is a one-line package install, but the package name differs by distro — which is the part that trips people up.
Symptoms
./deploy.sh: line NN: envsubst: command not foundin a container or CI job.- The same script runs fine locally, where
gettextis already installed. which envsubstreturns nothing inside the failing environment.- The failure appears only after switching to a slim/Alpine base image or a minimal CI runner.
- A rendered config file is empty or missing because the substitution step never ran.
Common Root Causes
1. gettext is not installed
envsubst lives in gettext (Debian/Ubuntu split the runtime tools into gettext-base). If neither is installed, the command does not exist. This is the root cause in the large majority of cases.
2. Alpine images need the gettext package explicitly
Alpine’s BusyBox does not provide envsubst. You must apk add gettext (which installs the full envsubst), as the base image ships without it.
3. PATH does not include the install location in the container
gettext is installed but into a prefix not on the container’s PATH, or an early script layer reset PATH, so the shell cannot find envsubst.
4. Wrong build stage in a multi-stage Dockerfile
gettext is installed in a build stage but the final runtime stage — a fresh, minimal base — does not carry it forward, so the shipped image lacks envsubst.
How to Diagnose
Confirm whether the binary is present and where it resolves inside the failing environment:
which envsubst || echo "envsubst NOT on PATH"
command -v envsubst
envsubst NOT on PATH
Identify the distro so you pick the right package name — the fix differs between Debian-family, RHEL-family, and Alpine:
cat /etc/os-release | grep -E '^(ID|ID_LIKE)='
ID=alpine
ID_LIKE=
Check whether gettext provides the binary and where a working copy would live (run this on a machine where it works to see the expected path):
dpkg -L gettext-base 2>/dev/null | grep envsubst || rpm -ql gettext 2>/dev/null | grep envsubst || apk info -L gettext 2>/dev/null | grep envsubst
/usr/bin/envsubst
If envsubst exists but the script still cannot find it, the problem is PATH — print it and confirm /usr/bin is present:
echo "$PATH"
ls -l /usr/bin/envsubst 2>/dev/null
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
-rwxr-xr-x 1 root root 39208 May 10 12:00 /usr/bin/envsubst
Fixes
Install the package for your distro. The name is the part that varies:
# Debian / Ubuntu — the runtime subset is gettext-base
apt-get update && apt-get install -y gettext-base
# Alpine
apk add --no-cache gettext
# RHEL / Alma / Rocky / Fedora
dnf install -y gettext
Confirm the binary now resolves and renders a template:
command -v envsubst
export APP_ENV=prod APP_PORT=8080
echo 'env=${APP_ENV} port=${APP_PORT}' | envsubst
/usr/bin/envsubst
env=prod port=8080
In a Dockerfile, install it in the final stage so the shipped image actually contains it:
FROM alpine:3.20
RUN apk add --no-cache gettext
COPY deploy.sh config.tmpl /opt/jobs/
Limit which variables are substituted. By default envsubst replaces every ${VAR} it recognizes, which will clobber shell-like syntax in your template (for example ${nginx_variable} in an nginx config). Pass an explicit list to substitute only those:
# Only replace $APP_ENV and $APP_PORT; leave every other ${...} untouched
envsubst '$APP_ENV $APP_PORT' < config.tmpl > config.rendered
# config.rendered
environment = prod
listen 8080;
# $DATABASE_URL left literal on purpose
If you genuinely cannot add a package (locked-down base image), a pure-shell fallback works for simple cases, though it is less robust than envsubst:
# Minimal fallback: read template on stdin, expand with the shell
while IFS= read -r line; do eval "echo \"$line\""; done < config.tmpl > config.rendered
Use the fallback with care — eval will execute anything in the template, so only use it on templates you fully control.
What to Watch Out For
- Default
envsubstsubstitutes all environment variables, not just the ones you intend. Templates that legitimately contain${...}(nginx, systemd unit specifiers, shell snippets) get mangled unless you pass the explicit'$VAR1 $VAR2'allowlist. - The Debian package is
gettext-base, notgettext. Installing the fullgettextalso works but is heavier;gettext-baseis the minimal runtime that providesenvsubst. - Multi-stage Docker builds are a classic trap: installing
gettextin a builder stage does nothing for the runtime image. Install it where the script actually runs. - The
eval-based shell fallback executes template contents. Never point it at untrusted or user-supplied templates — a$(rm -rf ...)in the template would run. envsubstonly understands$VARand${VAR}— it does not support default values (${VAR:-default}) or other shell parameter expansions. If you need those, render with the shell or a dedicated templating tool.
Related Guides
- flock Error: failed to execute — No such file or directory
- Writing Safe sed and awk Bulk Edits With AI Review
- GitHub Actions: Process completed with exit code 1
Fixed it? Get 500 Automation & DevOps AI prompts — free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.