1. Check for Invisible Characters (CLI)
- Show Tabs and Ends of Lines:
- Look for
^I:- This represents a Tab. YAML only allows spaces for indentation.
- Look for
^M:- This indicates Windows-style line endings (
CRLF), which can sometimes confuse older parsers on Linux.
- This indicates Windows-style line endings (
- Look for
cat -A docker-compose.yml
- Search for Non-ASCII Characters:
- If you copy-pasted the file, you might have “smart quotes” or hidden Unicode characters.
<code>grep -P '[^\x00-\x7f]' docker-compose.yml
- Look for “Hidden Header”
- Even if
cat -Alooks clean,- files moved from Windows to Unix often have a UTF-8 BOM (Byte Order Mark).
- Docker’s Go-parser treats a file with a BOM as “not a key-value file” and ignores it.
- Run this to create a 100% binary-clean version:
- Even if
tail -c +1 .env > .env.fresh && mv .env.fresh .env
(If that doesn’t work, try tail -c +4 to specifically strip a 3-byte BOM).
tail -c +4 .env > .env.fresh && mv .env.fresh .env
- Try this specific check:
- If you see
ef bb bf, you have a BOM that must be deleted.
- If you see
<code>head -n 1 .env | od -t x1
- Check .env file permissions.
You may need to run:
chmod 644 .env
2. Validate with Docker’s Built-in Tool
Use the config command to get a more detailed breakdown of the structural error.
Use the modern docker<strong> </strong>compose command to avoid the versioning issues discussed earlier:
docker compose config
If the file is valid, this command will print the “resolved” version of your configuration.
If it’s invalid, it will often point to the specific key that is misaligned.
- Use “Shell Injection”
- Use the shell to “pipe” the
.envfile into the command.- This bypasses Docker’s internal file parser entirely:
- Use the shell to “pipe” the
set -a; source .env; set +a; docker compose config
alias dcup='set -a; [ -f .env ] && source .env; set +a; docker compose up -d'
export $(grep -v '^#' .env | xargs) && docker compose config
export $(grep -v '^#' .env | x_args=() ; while read -r line; do [[ -z "$line" ]] && continue; x_args+=("$line"); done < .env; printf '%s\n' "${x_args[@]}") && docker compose config
3. Use an Online Validator
If you have access to a browser, copy and paste your content into a tool like YAMLlint or YAML Checker.
These tools provide high-contrast highlighting of exactly where a mapping (key-value pair) is placed where it shouldn’t be.
1. The “Substitution” vs “Injection” Trap
There is a critical distinction in how Docker uses .env:
- Substitution (Default):
Docker reads.envto fill in the${VAR}blanks inside yourdocker-compose.yml.
The Fix:
You must either reference the variables in the environment section or use the env_file directive:
services:
web:
<em># Option A: Explicitly map them (Best for clarity)</em>
environment:
- SQLALCHEMY_DATABASE_URI=postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
<em># Option B: Inject the whole file into the container</em>
env_file:
- .env
2. Common “Silent” Failure Reasons
If your docker-compose.yml still shows empty values (check this with docker compose config), check these three things:
- File Name: It must be exactly
.env(no prefix, all lowercase) and in the same directory as yourdocker-compose.yml. - Syntax: Ensure there are no spaces around the equals sign (e.g.,
DB_USER=postgres✅,DB_USER = postgres❌). - Shell Overrides: If you have these variables exported in your current terminal session (e.g.,
export DB_USER=admin), the shell value will override whatever is in your.envfile.GitHub +4
3. How to Debug it
Run this command in your terminal:
docker compose config
This will print out the “fully resolved” YAML.
If you see SQLALCHEMY_DATABASE_URI: postgresql://:@db:5432/
(with missing usernames/passwords), then Docker is failing to read the .env file.
Quick fix if it’s still being stubborn:
You can explicitly pass the file in the command:
docker compose --env-file .env up
Look for Trailing Spaces
look at this line:LIC_REFRESH_INTERVAL=60 $
That space before the $ (the end-of-line marker) is a major red flag.
If your .env file has trailing spaces on lines
—or if it was saved with Windows-style line endings (CRLF) instead of Linux style (LF)
—Docker Compose V2 will often fail to parse the variables correctly,
resulting in the empty strings you see in your config output
(e.g., container_name: _db instead of licenses_db).
Clean the .env file
Run this command in your terminal to strip out any hidden carriage returns and trailing whitespace:
sed -i 's/[[:space:]]*$//' .env && sed -i 's/\r//' .env
After cleaning the .env file, verify it:
- Check Config again:
docker compose config
Look forcontainer_name: licenses_db.
If you see the word “licenses”, it worked! - Explicitly point to the file (The “Fail-safe”):
If it still shows empty values,
your Docker version might be looking for the file in a different context.
Force it with:
<code>docker compose --env-file .env config
Here is the industry-standard “Three-File” approach:
1. The Strategy
.env.example: (Check into Git) Contains keys but no secrets.
Serves as a template for other devs..env.dev: (Git Ignore) Local database credentials,FLASK_DEBUG=1, andSQL_ECHO=True..env.prod: (Git Ignore) Production secrets,
restricted ports, and your specific IP strings.
2. Implementation
You can tell Docker which file to use without renaming them constantly.
Option A: The “Explicit” Command (Recommended)
Keep both files in your folder.
When you want to run one or the other,
use the --env-file flag:
For Development:
docker compose --env-file .env.dev up
For Production:
docker compose --env-file .env.prod up
Option B: The “Automatic” Way (Override)
Create a base docker-compose.yml and a docker-compose.override.yml.
docker-compose.yml: Defines the services.docker-compose.override.yml:
Docker automatically merges this file if it exists.
You can put yourenv_file: .env.devhere.
3. Best Practices for Your Unraid Setup
Since you’re running this on Unraid, here is how to keep it clean:
- Strip the Whitespace:
As we saw earlier, Unraid/Linux is picky.
Always ensure your.env.prodis “Clean” (no trailing spaces). - External DBs:
In your.env.prod, you have192.168.0.51:5417.
In Dev, you’ll likely wantdb:5432(the internal Docker name).
Keeping these in separate files prevents you from accidentally overwriting your Prod DB with Dev migrations. - The
.gitignoreRule:
Never commit.env.devor.env.prodto yourfeature/testingbranch.
Only commit.env.example.
Pro-Tip: The “Shell Injection”
If you want to keep it even simpler, you can set a shell variable before running the command:
<em># In your terminal</em>
ENV_STATE=prod docker compose up
And in your docker-compose.yml:
services:
web:
env_file:
- .env.${ENV_STATE:-dev}
