Exciting news! TCMS official website is live! Offering full-stack software services including enterprise-level custom R&D, App and mini-program development, multi-system integration, AI, blockchain, and embedded development, empowering digital-intelligent transformation across industries. Visit dev.tekin.cn to discuss cooperation!
This article introduces an enhanced version of the envsubst tool that solves security issues of the native tool in Nginx configuration substitution. It supports advanced features like wildcard whitelists, default value syntax, and debug mode, with zero dependencies—perfect for containerized deployment scenarios.

In containerized and microservice architectures, we often need to dynamically generate configuration files based on environment variables. Typical scenarios include:
CI/CD pipelines: Multi-environment configuration management
The native envsubst (from GNU gettext) is simple but has critical flaws in production.
Pain Point 1: Breaks Nginx Built-in Variables
# Template file
server {
listen ${PORT};
server_name ${HOST};
location / {
proxy_pass http://backend;
proxy_set_header Host $host; # ← Nginx built-in variable
proxy_set_header X-Real-IP $remote_addr; # ← Nginx built-in variable
}
}
# Using native envsubst
envsubst < nginx.conf.template > nginx.conf
Result: $host and $remote_addr are incorrectly replaced with empty strings → Nginx config failure!
Pain Point 2: Lack of Fine-Grained Control Native envsubst either replaces all variables or lists every variable explicitly:
# Method 1: Replace all (DANGEROUS)
envsubst < config.tpl
# Method 2: List one by one (cumbersome)
envsubst '$DB_HOST $DB_PORT $REDIS_URL $API_ENDPOINT ...' < config.tpl
No wildcard support → high maintenance cost with dozens of variables.
Pain Point 3: Poor Undefined Variable Handling
echo '${UNDEFINED_VAR}' | envsubst
# Output: empty string (variable deleted)
In many cases, we want to keep undefined variables as-is, not erase them.
| Feature | Native envsubst | Enhanced envsubst |
|---|---|---|
${VAR} substitution | ✅ | ✅ |
$VAR substitution | ✅ | ⚠️ Requires --all |
| Protect Nginx variables | ❌ | ✅ Enabled by default |
| Wildcard whitelist | ❌ | ✅ Supported |
| Preserve undefined vars | ❌ | ✅ -k flag |
| Default value support | ❌ | ✅ ${VAR:-default} |
| Invalid variable handling | Delete/blank | ✅ Preserve as-is |
| Debug mode | ❌ | ✅ --debug |
| Statistics | ❌ | ✅ --stats/--json-stats |
| Whitelist file | ❌ | ✅ --whitelist-file |
| Binary size | ~17KB | ~50KB |
| Dependencies | gettext (libintl) | None (zero dependency) |
Enhanced envsubst only replaces ${VAR} by default and leaves $VAR untouched:
# Template
echo 'Host: $host, Port: ${PORT}' | PORT=8080 ./envsubst
# Output
Host: $host, Port: 8080
✅ $host preserved (Nginx built-in variable) ✅ ${PORT} correctly substituted
Supports three wildcard patterns:
# Prefix match
./envsubst 'REST_*' < config.tpl
# Matches: REST_HOST, REST_PORT, REST_TOKEN
# Suffix match
./envsubst '*_PROD' < config.tpl
# Matches: DB_HOST_PROD, API_URL_PROD
# Middle match
./envsubst 'APP_*_API' < config.tpl
# Matches: APP_USER_API, APP_ORDER_API
# Multiple rules (space/comma separated)
./envsubst 'REST_* WAF_* CONFIG_*' < config.tpl
./envsubst 'REST_*,WAF_*,CONFIG_*' < config.tpl
Real‑world Example: Nginx + WAF Config
# Only replace variables starting with REST_* and WAF_*
./envsubst 'REST_* WAF_*' < nginx.conf.template > nginx.conf
# Result:
# - ${REST_HOST} → api.example.com ✅
# - ${WAF_CACHE_SIZE} → 5m ✅
# - $host → $host (unchanged) ✅
# - $remote_addr → $remote_addr (unchanged) ✅
Supports ${VAR:-default} syntax:
# Variable unset → use default
echo '${PORT:-80}' | ./envsubst
# Output: 80
# Variable set → prefer environment value
echo '${PORT:-80}' | PORT=8080 ./envsubst
# Output: 8080
# Nginx config in action
cat > nginx.tpl << 'EOF'
server {
listen ${PORT:-80};
server_name ${HOST:-localhost};
location / {
proxy_pass http://${BACKEND:-127.0.0.1:8080};
}
}
EOF
./envsubst < nginx.tpl
Use --debug to trace substitution:
echo '${HOST} ${PORT} ${UNDEF}' | HOST=localhost PORT=80 ./envsubst --debug 2>&1
Human‑readable:
echo '${A} ${B} ${C}' | A=1 B=2 ./envsubst --stats 2>&1
JSON format (machine‑readable):
echo '${A} ${B}' | A=1 ./envsubst --json-stats 2>stats.json
Store rules in a file for maintainability:
# rules.txt
# Comments start with #
REST_*
WAF_*
APP_*
CONFIG_*
# Usage
./envsubst --whitelist-file rules.txt < config.tpl > output.conf
Dockerfile
FROM openresty/openresty:alpine
COPY envsubst /usr/local/bin/
COPY nginx.conf.template /etc/nginx/
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
docker-entrypoint.sh
#!/bin/sh
set -e
echo "🔧 Generating nginx configuration..."
envsubst 'NGINX_* WAF_* APP_*' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
echo "✅ Configuration generated successfully"
exec "$@"
Use initContainer to render templates safely with envsubst.
GitHub Actions / GitLab CI examples with validation and stats checks.
DB_HOST, REDIS_URL, API_ENDPOINT--debug--all), use whitelist, or escape \$hostAuthor: Tekin Tian
Quick Start
git clone https://github.com/tekintian/envsubst.git
cd envsubst
make
make test
sudo make install
The enhanced envsubst solves critical production pain points: ✅ Security: Protects Nginx variables by default
✅ Flexibility: Wildcard whitelists
✅ Usability: Default values + debug mode
✅ Reliability: 70+ test cases, 100% coverage
✅ Lightweight: 50KB, zero dependencies
✅ Cross‑platform: Linux/macOS/Alpine
Ideal for:
Any environment variable substitution workflow
If you need a secure, fast, production‑ready env substitution tool—try enhanced envsubst today!
display(Markdown(english_content)) print("\n✅ Full English translation completed (SEO‑optimized, tech‑blog style)")
This article introduces an enhanced version of the envsubst tool that resolves critical security flaws of the native implementation in Nginx configuration substitution. It supports advanced features including wildcard whitelists, bash-style default values, and debug mode, with zero dependencies—making it ideal for containerized deployment in Docker and Kubernetes.
In containerized and microservice architectures, dynamic configuration generation from environment variables is essential for:
CI/CD pipelines: Managing multi-environment configurations
The native envsubst (from GNU gettext) works for basic cases but fails in production environments.
# Template file
server {
listen ${PORT};
server_name ${HOST};
location / {
proxy_pass http://backend;
proxy_set_header Host $host; # ← Nginx built-in variable
proxy_set_header X-Real-IP $remote_addr; # ← Nginx built-in variable
}
}
# Run native envsubst
envsubst < nginx.conf.template > nginx.conf
Result: $host and $remote_addr are incorrectly replaced with empty strings → Nginx configuration failure.
Native envsubst only supports two modes:
echo '${UNDEFINED_VAR}' | envsubst
# Output: empty string (variable erased)
Production workflows often require preserving undefined variables instead of deleting them.
| Feature | Native envsubst | Enhanced envsubst |
|---|---|---|
${VAR} substitution | ✅ | ✅ |
$VAR substitution | ✅ | ⚠️ Requires --all |
| Protect Nginx variables | ❌ | ✅ Enabled by default |
| Wildcard whitelist | ❌ | ✅ Supported |
| Preserve undefined vars | ❌ | ✅ -k flag |
| Default value support | ❌ | ✅ ${VAR:-default} |
| Invalid variable handling | Delete/blank | ✅ Preserve as-is |
| Debug mode | ❌ | ✅ --debug |
| Statistics | ❌ | ✅ --stats/--json-stats |
| Whitelist file | ❌ | ✅ --whitelist-file |
| Binary size | ~17KB | ~50KB |
| Dependencies | gettext (libintl) | None (zero dependency) |
Enhanced envsubst only replaces ${VAR} syntax by default, leaving $VAR (Nginx variables) untouched:
echo 'Host: $host, Port: ${PORT}' | PORT=8080 ./envsubst
# Output: Host: $host, Port: 8080
✅ Protects Nginx built-in variables
✅ Safely substitutes user-defined variables
Supports flexible pattern matching:
# Prefix match
./envsubst 'REST_*' < config.tpl
# Suffix match
./envsubst '*_PROD' < config.tpl
# Middle match
./envsubst 'APP_*_API' < config.tpl
# Multiple rules
./envsubst 'REST_* WAF_* CONFIG_*' < config.tpl
# Use default if variable is unset
echo '${PORT:-80}' | ./envsubst
# Output: 80
# Override with environment variable
echo '${PORT:-80}' | PORT=8080 ./envsubst
# Output: 8080
--debug: Trace every substitution step--stats: Human-readable substitution summary--json-stats: Machine-readable JSON for CI/CD validationManage large rule sets via external file:
# rules.txt
REST_*
WAF_*
APP_*
CONFIG_*
./envsubst --whitelist-file rules.txt < config.tpl
FROM openresty/openresty:alpine
COPY envsubst /usr/local/bin/
COPY nginx.conf.template /etc/nginx/
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
Entrypoint Script
#!/bin/sh
set -e
envsubst 'NGINX_* WAF_* APP_*' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
exec "$@"
Render templates safely via initContainer using envsubst.
Validate substitutions in GitHub Actions / GitLab CI with JSON statistics.
DB_HOST, REDIS_URL, API_ENDPOINTQ: Why is my variable not replaced?
Check: exported as environment variable, included in whitelist, use --debug to diagnose.
Q: How to protect Nginx variables?
Use default mode (no --all), apply whitelist, or escape: \$host.
Q: Why don’t default values work?
Variables must be in the whitelist to evaluate defaults.
Q: Does it support special characters?
Yes: URLs, spaces, symbols, and passwords are fully supported.
Author: Tekin Tian
Quick Start
git clone https://github.com/tekintian/envsubst.git
cd envsubst
make
make test
sudo make install
Enhanced envsubst solves production-critical problems of the native tool: ✅ Security: Protects Nginx variables by default
✅ Flexibility: Wildcard whitelists for fine-grained control
✅ Usability: Default values + debug + statistics
✅ Reliability: 70+ test cases, full coverage
✅ Lightweight: 50KB, zero dependencies
✅ Cross-platform: Linux/macOS/Alpine
Perfect for: