Docker Compose Is Leaking Your Passwords
Docker Compose Is Leaking Your Passwords
Open-source projects have a dirty secret: their example configurations ship with real-looking credentials, and developers copy-paste them straight to production.
This isn't a hypothetical problem. We've seen it in some of the most popular open-source projects on GitHub — projects with thousands of stars, active maintainers, and real companies running them in production.
Three projects, three different leaks
Hoppscotch (64k+ GitHub stars) ships a docker-compose.yml with hardcoded database credentials:
# hoppscotch docker-compose.yml
services:
db:
environment:
POSTGRES_PASSWORD: testpass
POSTGRES_DB: hoppscotch
That's testpass as the PostgreSQL password. In the docker-compose file that developers use as their starting point for self-hosted deployments. The file isn't labeled "development only" or "do not use in production." It's the primary deployment method in their docs.
Trigger.dev includes session secrets in their .env.example:
# trigger.dev .env.example
SESSION_SECRET=abcdef1234
MAGIC_LINK_SECRET=abcdef1234
ENCRYPTION_KEY=abcdef1234
These are the values developers copy when they run cp .env.example .env — the first step in every setup guide. The values are short, obvious, and identical across all three fields. But they're not obviously fake enough to trigger a "wait, I should change this" reaction in a developer who's rushing through setup.
Typebot puts a real-looking encryption key in their .env.example:
# typebot .env.example
ENCRYPTION_SECRET=b24a04c3611d1e2...
This one is worse than the others. It looks like a real key. It has the right length and format. A developer copying this file won't think "placeholder" — they'll think "the encryption key is already configured." And now every Typebot instance that used the default setup shares the same encryption key.
Why this keeps happening
The root cause is a conflict between two goals:
- Make setup easy. New contributors and self-hosters need to get running fast. The fewer manual steps, the better.
- Keep secrets secret. Production credentials must be unique, strong, and never committed to version control.
Most projects optimize entirely for goal 1 and ignore goal 2. They put working values in example files so that docker compose up works on the first try. The problem is that "first try" setup becomes production for a disturbingly large number of deployments.
The copy-paste pipeline to production
Here's what actually happens:
- Developer finds a cool open-source project
- Follows the self-hosting guide:
git clone,cp .env.example .env,docker compose up - Everything works. The app is running.
- Developer points a domain at it, sets up a reverse proxy, and considers it deployed
- The
.envfile still has every default value from.env.example
Step 5 is where the vulnerability lives. Nobody goes back to regenerate secrets after the initial setup. The app works. Changing secrets means restarting services and potentially breaking things. So the defaults stay.
And those defaults are:
- Shared across every installation that followed the same docs
- Committed to a public GitHub repository
- Indexed by search engines and archived by the Wayback Machine
How to structure example files safely
There are two approaches that actually work.
Approach 1: Fail loudly with placeholder values
Use values that are obviously fake AND will cause the application to fail at startup if not replaced:
# .env.example
DATABASE_URL=postgresql://user:CHANGE_ME_BEFORE_RUNNING@localhost:5432/myapp
SESSION_SECRET=GENERATE_WITH_openssl_rand_-hex_32
ENCRYPTION_KEY=REPLACE_THIS_OR_APP_WILL_NOT_START
Then add a startup check:
// config.ts
const FORBIDDEN_VALUES = [
'CHANGE_ME', 'REPLACE_THIS', 'GENERATE_WITH', 'YOUR_', 'TODO',
'testpass', 'password', 'secret', 'abcdef', '12345',
];
function validateEnv(key: string, value: string | undefined): string {
if (!value) throw new Error(`Missing required env var: ${key}`);
if (FORBIDDEN_VALUES.some(f => value.includes(f))) {
throw new Error(`${key} contains a placeholder value. Generate a real secret.`);
}
return value;
}
export const config = {
sessionSecret: validateEnv('SESSION_SECRET', process.env.SESSION_SECRET),
encryptionKey: validateEnv('ENCRYPTION_KEY', process.env.ENCRYPTION_KEY),
databaseUrl: validateEnv('DATABASE_URL', process.env.DATABASE_URL),
};
The app refuses to start with placeholder values. No ambiguity. No "I'll change it later."
Approach 2: Generate secrets at setup time
Ship a setup script instead of a static example file:
#!/bin/bash
# setup.sh — generates .env with random secrets
if [ -f .env ]; then
echo ".env already exists. Delete it to regenerate."
exit 1
fi
cat > .env << EOF
DATABASE_URL=postgresql://user:$(openssl rand -hex 16)@localhost:5432/myapp
SESSION_SECRET=$(openssl rand -hex 32)
ENCRYPTION_KEY=$(openssl rand -hex 32)
MAGIC_LINK_SECRET=$(openssl rand -hex 32)
EOF
echo "Generated .env with random secrets."
echo "Review the file and update DATABASE_URL with your actual database credentials."
Every installation gets unique secrets. No copy-paste. No shared defaults.
For Docker Compose specifically
Never put passwords in docker-compose.yml. Use Docker secrets or environment variable references:
# docker-compose.yml — no hardcoded secrets
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
app:
env_file:
- .env # Generated by setup.sh, never committed
secrets:
db_password:
file: ./secrets/db_password.txt # gitignored, generated at setup
Add a .gitignore entry:
.env
.env.local
secrets/
How this affects audit scores
Our Environment Security audit has two checks directly related to this pattern. The no-hardcoded-secrets check is critical severity (weight: 10) — the highest possible weight. The no-default-passwords check catches placeholder credentials in example files.
The Security Headers & Basics audit also has a no-hardcoded-secrets check at critical severity. Between the two audits, shipping default credentials can cost you 20+ weighted points across your audit portfolio.
Hoppscotch is classified as a mature-tier project in our benchmarks — high code quality, strong community, actively maintained. But hardcoded secrets in Docker Compose are a critical-severity finding regardless of how good the rest of your codebase is. A single critical failure at weight 10 affects your score more than five passing info-level checks.
What to do right now
If you maintain an open-source project:
- Search your repo for hardcoded passwords:
grep -rn "PASSWORD=\|SECRET=\|_KEY=" docker-compose* .env.example - Replace hardcoded values with obvious placeholders that include generation instructions
- Add a startup validation that rejects placeholder values
- Ship a
setup.shthat generates random secrets
If you've deployed a self-hosted app using defaults:
- Check your
.envagainst the project's.env.example. If the values match, you're running shared secrets. - Generate new values:
openssl rand -hex 32for each secret - Rotate your database password and update the connection string
- Restart all services
This takes 10 minutes. The alternative is sharing your encryption keys with every other installation of the same software.