Skip to content
🎉 Launch sale:50% off everything over $22 — automatically applied at checkout· ends Aug 2Shop the sale →
DevOps AI ToolKit
Newsletter
All guides
AI for Automation By James Joyner IV · · 7 min read Last reviewed Jul 2026

envsubst Error: 'envsubst: command not found' — Cause, Fix, and Troubleshooting Guide

Quick answer

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
Free toolkit

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 found in a container or CI job.
  • The same script runs fine locally, where gettext is already installed.
  • which envsubst returns 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 envsubst substitutes 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, not gettext. Installing the full gettext also works but is heavier; gettext-base is the minimal runtime that provides envsubst.
  • Multi-stage Docker builds are a classic trap: installing gettext in 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.
  • envsubst only understands $VAR and ${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.
Free download · 368-page PDF

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?

Free download · 368-page PDF

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.