A Spring Boot application was crash-looping over 5,000 times on an EC2 instance managed by systemd. The logs showed nothing obvious — just status=1/FAILURE every 30 seconds. No connection errors, no missing JARs, no OOM kills.
The culprit? Hyphens in environment variable names.
PAYMENT-API-PROTOCOL=https
PAYMENT-API-HOST=10.0.1.50:8080
PAYMENT-CRED-USERNAME=svc-account
These were pulled from AWS Secrets Manager and written to a .env file. The systemd service used EnvironmentFile to load them. And systemd was silently ignoring every single one:
Ignoring invalid environment assignment 'PAYMENT-API-PROTOCOL=https'
Ignoring invalid environment assignment 'PAYMENT-CRED-USERNAME=svc-account'
The kicker? The exact same .env file worked perfectly in Docker.
The Rules Are Different Everywhere
Environment variable naming isn't universal. Each runtime has its own rules, and what works in one will silently break in another.
POSIX Standard (What Most Things Follow)
The POSIX specification for environment variables says names must match:
[a-zA-Z_][a-zA-Z0-9_]*
That's letters, digits, and underscores only. No hyphens, no dots, no special characters. The name must start with a letter or underscore.
Most Linux tooling follows this — bash, sh, env, export, systemd, and core utilities all enforce or expect POSIX-compliant names.
Docker: The Permissive One
Docker's --env-file parser is lenient. It accepts hyphens, dots, and other characters in variable names that POSIX wouldn't allow:
# docker-compose.yml or --env-file
PAYMENT-API-PROTOCOL=https # ✅ works in Docker
my.app.config=value # ✅ works in Docker
Docker passes these directly to the container's process environment without validation. The container runtime doesn't care — it just injects whatever key-value pairs you give it.
This is why teams that develop exclusively in Docker containers often don't realize they're using non-standard variable names — until they deploy to bare metal or systemd-managed services.
systemd: Strict POSIX
systemd's EnvironmentFile directive follows POSIX rules strictly. If a variable name contains a hyphen, systemd silently drops it with a log warning:
Ignoring invalid environment assignment 'PAYMENT-API-PROTOCOL=https'
The word "silently" is doing a lot of heavy lifting here. The service still starts. The variable just isn't there. Your application boots, can't find the config it needs, and crashes — often with a confusing error that points at dependency injection or missing beans, not at the actual missing variable.
# /etc/systemd/system/myapp.service
[Service]
EnvironmentFile=/opt/app/.env
ExecStart=/usr/bin/java -jar /opt/app/myapp.jar
# .env file
PAYMENT_API_PROTOCOL=https # ✅ loaded
PAYMENT-API-PROTOCOL=https # ❌ silently ignored
Kubernetes: POSIX with DNS Flexibility
Kubernetes ConfigMaps and Secrets follow POSIX for environment variable names injected via envFrom or env. But there's a nuance — ConfigMap keys can contain hyphens and dots because they can also be mounted as files:
# ConfigMap keys can have hyphens
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
PAYMENT_API_PROTOCOL: "https" # ✅ works as env var
payment-api-protocol: "https" # ⚠️ works as file mount, NOT as env var
When you mount a ConfigMap as a volume, each key becomes a filename — and filenames can have hyphens. But if you use envFrom to inject them as environment variables, Kubernetes will skip keys that aren't valid POSIX variable names:
# This will silently skip non-POSIX keys
envFrom:
- configMapRef:
name: app-config
Kubernetes logs a warning event on the pod, but unless you're watching events closely, you'll miss it — just like systemd.
The Comparison Table
| Feature | Docker | systemd | Kubernetes (env) | Kubernetes (volume) |
|---|---|---|---|---|
| Underscores | ✅ | ✅ | ✅ | ✅ |
| Hyphens | ✅ | ❌ Silent drop | ❌ Silent skip | ✅ (as filename) |
| Dots | ✅ | ❌ Silent drop | ❌ Silent skip | ✅ (as filename) |
| Error on invalid | No | Warning in journal | Event on pod | N/A |
| POSIX compliant | No | Yes | Yes | N/A |
The Real Danger: Silent Failures
The common thread across systemd and Kubernetes is silent failure. The variable isn't there, but nothing crashes at the system level — the process starts, tries to read the missing config, and fails with an application-level error.
In the incident that prompted this article, the Spring Boot error was:
Caused by: java.lang.IllegalArgumentException:
Could not resolve placeholder 'PAYMENT_API_HOST' in value "${PAYMENT_API_HOST}"
This looks like a missing property issue. It is — but the root cause was three layers deep: Secrets Manager had hyphens → systemd dropped the variables → Spring couldn't find them.
Best Practices
1. Use Underscores Everywhere
Just use SCREAMING_SNAKE_CASE for all environment variables. It's POSIX-compliant and works everywhere — Docker, systemd, Kubernetes, bash, CI/CD pipelines, Lambda, ECS, everything.
# Do this
PAYMENT_API_PROTOCOL=https
PAYMENT_CRED_USERNAME=myuser
# Not this
PAYMENT-API-PROTOCOL=https
payment.api.protocol=https
2. Validate at Deploy Time
Add a preflight check in your deployment scripts that verifies all required variables are present before restarting the service:
REQUIRED_VARS="DB_HOST DB_PORT API_KEY SECRET_TOKEN"
for var in $REQUIRED_VARS; do
grep -q "^${var}=" .env || { echo "FATAL: Missing $var in .env"; exit 1; }
done
systemctl restart myapp
This takes 5 lines and prevents crash loops at 3 AM.
3. Transform at the Boundary
If you can't control the source (like a third-party Secrets Manager with legacy keys), transform at the boundary — the point where secrets are pulled into the runtime:
# In deploy.sh — transform hyphens to underscores when pulling from Secrets Manager
aws secretsmanager get-secret-value \
--secret-id myapp.prod \
--query SecretString --output text \
| jq -r 'to_entries|map("\(.key | gsub("-";"_"))=\(.value|tostring)")|.[]' > .env
4. Spring Boot Property Naming
Spring Boot has its own relaxed binding that maps environment variables to properties. Use this to your advantage:
# application.properties
# Spring property with dots — reads from PAYMENT_API_PROTOCOL env var automatically
payment.api.protocol=${PAYMENT_API_PROTOCOL}
But be careful with circular references. If your property name and placeholder are identical:
# ❌ Circular reference — Spring resolves against itself
PAYMENT_API_HOST=${PAYMENT_API_HOST}
# ✅ Different names — env var resolves correctly
payment.api.host=${PAYMENT_API_HOST}
5. Use systemd-analyze to Verify
Before deploying, verify your environment file is valid:
systemd-analyze verify /etc/systemd/system/myapp.service
Or manually test what systemd will actually load:
systemd-run --property=EnvironmentFile=/opt/app/.env /usr/bin/env
Wrapping Up
Environment variable naming is one of those things that "just works" until it doesn't. Docker's permissiveness masks issues that surface the moment you move to systemd or Kubernetes. The fix is simple — stick to POSIX-compliant names (letters, digits, underscores) and validate at deploy time.
The 5,000+ restarts and hours of debugging could have been avoided with two things: underscores instead of hyphens, and a 5-line validation script in the deployment pipeline.
Sometimes the smallest characters cause the biggest outages.
Have you run into similar environment variable gotchas? Drop your war stories in the comments 👇
``
Top comments (0)