Postgres "password authentication failed": the 6 real causes
⏱️ 2 min read
FATAL: password authentication failed for user "postgres" is Postgres's way of saying the credentials you sent don't match pg_hba.conf and the role. It's rarely a lying password — it's usually the wrong combination of user, host, and auth method. Work down this list.
1. You're connecting as the wrong role from a different host context
Inside the container, the default role is postgres; from outside, you may need a role you created. Check who you actually are: SELECT current_user; — apps frequently assume postgres while their connection string says app_user.
2. POSTGRES_PASSWORD vs a real superuser password
In the official Docker image, POSTGRES_PASSWORD only sets the initial superuser password on first initialization. If the data volume already existed, your env var was ignored — the old password is still live. docker volume rm (and losing the data) or ALTER ROLE ... PASSWORD fixes it.
3. pg_hba.conf says something different than you think
The auth method column decides what "password" means: md5, scram-sha-256, trust. A password stored as SCRAM fails against an md5 line (and upgrading Postgres switched the default to SCRAM in v10+). Modern libpq handles this, but ancient clients and some GUI tools don't.
4. Special characters in the password
If your password contains @, :, / or %, URL-encoded connection strings (postgresql://user:p%40ss@host) need percent-encoding. Raw JSON/env values are fine — the URL form is not. This causes "the password works in psql but not in the app".
5. Kubernetes secrets that didn't load
An env var referencing a missing Secret mounts empty — so the app sends an empty password against a real one. kubectl describe pod shows the env source; check the actual value with a debug pod.
6. The password really did change
Automation (a migration, a rotation script, a teammate) altered it. ALTER ROLE app_user WITH PASSWORD '...'; resets it deliberately.
For a sandbox where you can break auth setups without fear: a fresh managed or self-hosted instance on a droplet makes cause #2's volume surprises painless.