Fix PostgreSQL "password authentication failed" for Django
Fix FATAL: password authentication failed for user in Django: read the server-side reason, URL-encode special characters, reload environment variables, fix Docker volume passwords, and pg_hba methods.
Problem statement
Django fails to start, migrate fails, or every request returns 500, with:
django.db.utils.OperationalError: connection to server at "10.0.0.5", port 5432 failed:
FATAL: password authentication failed for user "myapp"
The network path to PostgreSQL works (otherwise you would see "connection refused" or a timeout), but the server rejected the credentials. The message is deliberately vague: PostgreSQL reports the same error to the client whether the password is wrong, the role does not exist, or the role has no password. The server log states the real reason, and it is the fastest route to a fix.
- Read the PostgreSQL server log; the
DETAILline says whether the role is missing, has no password, or the password does not match. - Test the exact credentials with
psqlfrom the app server. - Percent-encode special characters in
DATABASE_URL(@,:,/,#,%,?). - Make sure the running process actually has the current password (restart after changing environment files or secrets).
- With the Docker
postgresimage, rememberPOSTGRES_PASSWORDonly applies when the data volume is first initialised. - If needed, reset the password with
ALTER ROLE ... PASSWORDand checkpassword_encryptionandpg_hba.confmethods.
Step-by-step solution
1. Read the server-side reason
On a self-hosted server:
sudo tail -n 50 /var/log/postgresql/postgresql-16-main.log
In Docker: docker compose logs db --tail 50. On managed services, open the database's log viewer. Look for the lines following the FATAL:
| DETAIL | Meaning |
|---|---|
Role "myapp" does not exist. | wrong user name, or the role was never created on this server |
Password does not match for user "myapp". | wrong password reached the server |
User "myapp" has no password assigned. | the role exists without a password |
User "myapp" has an expired password. | VALID UNTIL passed |
The log line also includes the Connection matched pg_hba.conf line N information, which shows which rule and authentication method applied.
2. Test the credentials directly
From the app server, with the same values Django uses:
psql "postgresql://myapp@10.0.0.5:5432/myapp?sslmode=require" -c "select current_user;"
psql prompts for the password. If it works in psql but not in Django, the difference is in how Django receives the credentials (steps 3 and 4). If it fails in psql too, the problem is on the database side (steps 5 and 6).
3. Encode special characters in DATABASE_URL
A password like p@ss:w0rd/#1 breaks URL parsing: the @ ends the credentials early, so the wrong user or password is sent. Percent-encode it:
python3 -c "from urllib.parse import quote; print(quote('p@ss:w0rd/#1', safe=''))"
# p%40ss%3Aw0rd%2F%231
DATABASE_URL=postgres://myapp:p%40ss%3Aw0rd%2F%231@10.0.0.5:5432/myapp
dj-database-url decodes it again. Alternatively, pass the password in a separate variable and build DATABASES from individual settings, which avoids URL encoding entirely:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ["DB_NAME"],
"USER": os.environ["DB_USER"],
"PASSWORD": os.environ["DB_PASSWORD"],
"HOST": os.environ["DB_HOST"],
"PORT": os.environ.get("DB_PORT", "5432"),
}
}
Also watch for trailing spaces or quotes. In a systemd EnvironmentFile, quotes are stripped, but a value copied with a trailing space keeps it. Docker Compose env_file values keep quotes in some versions. Print the length of the value the app sees to catch invisible characters:
python -c "import os; print(len(os.environ['DB_PASSWORD']))"
4. Make sure the process has the new password
Environment variables are read at process start. After rotating a password:
- systemd:
sudo systemctl restart gunicorn celery-worker(areloaddoes not re-readEnvironmentFile). - Docker Compose:
docker compose up -drecreates containers with newenvironment/env_filevalues;docker compose restartdoes not. - Kubernetes: Secrets exposed as environment variables are not updated in running pods; roll the Deployment.
Check what a running process actually has:
sudo cat /proc/$(pgrep -f "gunicorn.*master" | head -1)/environ | tr '\0' '\n' | grep -E "^DB_|^DATABASE_URL" | sed 's/=.*/=<set>/'
5. Docker volumes keep the old password
The official postgres image uses POSTGRES_USER, POSTGRES_PASSWORD, and POSTGRES_DB only when it initialises an empty data directory. If the volume already exists, changing POSTGRES_PASSWORD in Compose has no effect; the old password remains. Either change it inside the database:
docker compose exec db psql -U postgres -c "ALTER ROLE myapp WITH PASSWORD 'new-password';"
or, only for disposable development data, remove the volume (docker compose down -v) so it is initialised again.
6. Reset the password and check the method
As a superuser or the role's owner:
SHOW password_encryption; -- scram-sha-256 on modern versions
ALTER ROLE myapp WITH LOGIN PASSWORD 'new-long-random-password';
SELECT rolname, rolvaliduntil FROM pg_roles WHERE rolname = 'myapp';
In psql, prefer \password myapp, which avoids writing the password into the shell history and server logs.
If pg_hba.conf requires scram-sha-256 but the stored password was hashed with MD5 (common after upgrading from old versions), authentication fails until the password is set again with password_encryption = scram-sha-256. Check the matching rule:
sudo grep -vE "^\s*(#|$)" /etc/postgresql/16/main/pg_hba.conf
After editing pg_hba.conf, reload PostgreSQL: sudo systemctl reload postgresql.
Verification check:
psql "postgresql://myapp@10.0.0.5:5432/myapp?sslmode=require" -c "select 1;"
.venv/bin/python manage.py check --database default
.venv/bin/python manage.py showmigrations | head -3
The last command connects to the database, so it succeeding confirms Django can authenticate.
Explanation
When a client connects, PostgreSQL chooses an authentication method from the first matching pg_hba.conf rule, then verifies the supplied password against the role's stored hash. To avoid telling attackers which user names exist, the client receives the same message for every failure, while the server logs the detail. In Django deployments, the password is usually correct somewhere and wrong in transit: mangled by URL parsing, stale in a running process, or never applied because of how a container volume was initialised.
Edge cases / notes
Managed databases
Some providers require user name formats or IAM-based authentication tokens instead of static passwords (for example RDS IAM authentication). Tokens expire, so they must be refreshed; static passwords are simpler for most Django apps.
Connection poolers
With PgBouncer in the path, the error may come from PgBouncer's own user list (auth_file or auth_query) rather than PostgreSQL. Check PgBouncer's log too.
When to automate this
Store database credentials in one secret source and inject them into every process type (web, workers, migrations) the same way. Rotate passwords with a runbook or script that updates the role, updates the secret, and restarts or rolls all consumers in one step.
Next steps
- For network and other connection errors, see Fix Database Connection Errors in Django Production.
- Review a clean connection setup in How to Connect Django to PostgreSQL in Production.
- For container networking problems, see Fix Django Cannot Connect to Postgres in Docker Compose.
- If environment variables do not reach Gunicorn, see Fix systemd EnvironmentFile Variables Not Reaching Django.
FAQ
Why does PostgreSQL say password authentication failed when the user does not exist?
PostgreSQL returns the same client error for missing roles and wrong passwords. The server log's DETAIL line shows which one it is.
Why did changing POSTGRES_PASSWORD in Docker Compose not work?
The variable is only used to initialise an empty data volume. Change the password with ALTER ROLE or recreate the volume.
Do I need to encode special characters in DATABASE_URL?
Yes. Characters such as @, :, /, #, and % must be percent-encoded in the URL's password part.
Why does psql work but Django fails with the same password?
Django received a different value: an unencoded URL, a stale environment in a running process, or invisible characters in the environment file.