Troubleshooting
PostgreSQL & databases

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.

Where the login failsDjango settingsUSER and PASSWORDlibpqsends credentialspg_hba.confpicks auth methodRole checkpassword matches?The client sees one generic message; the PostgreSQL server log names the actual reason.
  1. Read the PostgreSQL server log; the DETAIL line says whether the role is missing, has no password, or the password does not match.
  2. Test the exact credentials with psql from the app server.
  3. Percent-encode special characters in DATABASE_URL (@, :, /, #, %, ?).
  4. Make sure the running process actually has the current password (restart after changing environment files or secrets).
  5. With the Docker postgres image, remember POSTGRES_PASSWORD only applies when the data volume is first initialised.
  6. If needed, reset the password with ALTER ROLE ... PASSWORD and check password_encryption and pg_hba.conf methods.

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
the server log says whyFATAL:  password authentication failed for user "myapp"DETAIL:Password does not matchfor user "myapp".Connection matched pg_hba.conf line 98:"host all all 10.0.0.0/24scram-sha-256"1231What Django also sees, without the detail.2The real reason: only in the server log.3The rule and method that applied.The client error is deliberately vague; the server log is notdjango-deployment.com
Managed databases expose the same lines in their log viewer.

In Docker: docker compose logs db --tail 50. On managed services, open the database's log viewer. Look for the lines following the FATAL:

DETAILMeaning
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
encode these in DATABASE_URLEncodedOtherwise read as@%40✕end of credentials:%3Auser and password separator/%2Fstart of the database path#%23start of a fragment?%3Fstart of query options%%25start of an escapeurllib.parse.quote(password, safe="") produces the encoded formdjango-deployment.com
Or avoid the problem with separate DB_PASSWORD settings instead of a URL.
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 (a reload does not re-read EnvironmentFile).
  • Docker Compose: docker compose up -d recreates containers with new environment/env_file values; docker compose restart does 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';"
the volume remembers the first passwordFirst startempty volumeinitdb runsuses POSTGRES_PASSWORDPassword changed in.envlaterRestartvolume not empty: no initdbDjango uses new oneauthentication failsChange it inside the database with ALTER ROLE, then update .envdjango-deployment.com
Deleting the volume also works, but deletes the data with it.

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

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.

Keep going