</>CodeWithKarani

FATAL: role "postgres" does not exist in Docker: the POSTGRES_USER volume trap

Karani GeoffreyKarani Geoffrey7 min read

You wrote a clean docker-compose.yml. You set POSTGRES_USER: appuser because hardcoding postgres everywhere felt sloppy. It worked on your laptop. Then a colleague pulled the branch, or you redeployed to the VPS, and the container started throwing this at every connection attempt:

FATAL:  role "postgres" does not exist

So you check your permissions. You check the password. You check that the network between your app and the database is fine. All of it is fine. The role genuinely does not exist, and no amount of GRANT is going to conjure it, because this is not a permissions problem at all. It is a lifecycle problem, and the official Postgres image is behaving exactly as documented. The trap is that the documentation for this behaviour is one sentence you have never had a reason to read.

POSTGRES_USER, POSTGRES_PASSWORD and POSTGRES_DB are read only on the first startup against an empty data volume. After that they do nothing.

  • If you changed POSTGRES_USER after the volume already had data, the old role is still the only one that exists.
  • If postgres does not exist, it is because your first-ever run set POSTGRES_USER to something else, and that value is now baked into the volume.
  • To keep your data: connect as the role that does exist and CREATE ROLE postgres ... or rename the one you have.
  • To start clean and lose the data: docker compose down -v so initdb re-runs with the current variables.
  • Fix any hardcoded pg_isready -U postgres healthcheck to use -U "$POSTGRES_USER".

The exact error: FATAL: role "postgres" does not exist

It shows up in three slightly different places, and knowing which one you have tells you where to look.

# From your application's driver
FATAL:  role "postgres" does not exist

# From a psql attempt inside the container
docker compose exec db psql -U postgres
psql: error: connection to server failed: FATAL:  role "postgres" does not exist

# From a healthcheck that hardcodes the user, in docker compose ps
db  Restarting (1)   pg_isready -U postgres  ->  role "postgres" does not exist

In all three, the message is telling you the literal truth. There is no role named postgres in this cluster. The question is why, and the answer is always the same.

Why this happens: the entrypoint runs initdb exactly once

The official postgres image has an entrypoint script that runs every time the container starts. Near the top it does one check: is PGDATA already an initialized Postgres data directory? Concretely, does PG_VERSION exist inside it?

  • If the directory is empty (a brand new volume), the entrypoint runs initdb, creates the superuser named by POSTGRES_USER (defaulting to postgres), sets its password from POSTGRES_PASSWORD, creates POSTGRES_DB, and runs anything in /docker-entrypoint-initdb.d.
  • If the directory already contains a cluster, it skips all of that and simply starts the existing server. Your environment variables are never consulted. They are initialization inputs, not runtime configuration.

That single branch is the whole bug. A Docker named volume survives docker compose down, docker compose up, image upgrades, and reboots. So the first time you ran the stack, whatever POSTGRES_USER was at that moment created the only superuser this cluster will ever have by default. Every later edit to the variable writes to a value the entrypoint has already decided to ignore.

Container starts: is PGDATA already a cluster? Empty volume (first run) Run initdb Create role = POSTGRES_USER Set password, create POSTGRES_DB Volume already has data Skip initdb entirely Start the existing server POSTGRES_USER is ignored here The mismatch Volume was initialized as "appuser", app or healthcheck now asks for "postgres" -> role does not exist The variables describe what to build. Once it is built, they describe nothing. This is why "it worked on my machine" and "it fails on the server" can both be true.
The entrypoint reads your environment variables on exactly one code path. The other path is where most people end up.

How you actually got here

Four routes lead to this exact error, and the fix differs slightly for each.

What happenedWhich role exists now
First run set POSTGRES_USER=appuser, app connects as postgresappuser
You changed POSTGRES_USER from postgres to something else after data existedThe original value, still postgres
Healthcheck hardcodes pg_isready -U postgres, but POSTGRES_USER is customYour custom user, and the database is actually healthy
A Postgres also runs on the host and shadows the container on localhost:5432Whatever the host Postgres has, not the container

That last row deserves a flag. If you have a native Postgres installed on the host and also publish the container on 5432, a client connecting to localhost:5432 may be hitting the host instance, not the container. That is a close cousin of the problem in connection refused on localhost with docker-compose Postgres, and the diagnostic is the same: prove which server you are actually talking to before you trust any error it returns.

The fix, in numbered steps

Step 1: Find out which role actually exists

Do not guess. Connect to the container and list the roles. You need to connect as a role that exists, so try the value you think was used at first init:

docker compose exec db psql -U appuser -d appuser -c "\du"

Expected output is the role table. Whatever superuser is listed is the one initdb created:

                             List of roles
 Role name |                         Attributes
-----------+------------------------------------------------------------
 appuser   | Superuser, Create role, Create DB, Replication, Bypass RLS

If even that fails with the same role error, try the default psql -U postgres. One of them will work, because some superuser exists or the cluster could not have started.

Step 2 (keep your data): create or rename the role you need

If your application is hardcoded to expect postgres and you would rather not change it, add that role from inside the working session:

docker compose exec db psql -U appuser -d appuser -c \
  "CREATE ROLE postgres LOGIN SUPERUSER PASSWORD 'your-strong-password';"

Or, cleaner, rename the existing superuser so there is only one:

ALTER ROLE appuser RENAME TO postgres;
-- a role rename does not change its password, so set it explicitly if needed
ALTER ROLE postgres WITH PASSWORD 'your-strong-password';

This keeps every table, index and row exactly where it is. You are only adjusting the login roles, not the data.

Step 3 (disposable data only): remove the volume so initdb re-runs

In development, the honest reset is to throw the cluster away and let the entrypoint rebuild it with your current variables:

# -v removes named volumes declared in the compose file
docker compose down -v
docker compose up -d

If your volume is external or named explicitly, remove it directly:

docker compose down
docker volume ls | grep pg
docker volume rm myproject_pgdata
docker compose up -d

This deletes the database. Never run it against production data you have not backed up and verified you can restore.

Step 4: fix the healthcheck so it follows POSTGRES_USER

A hardcoded user in the healthcheck is the sneakiest version of this bug, because the database is healthy and Docker keeps restarting it anyway. Reference the variable:

services:
  db:
    image: postgres:17
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: your-strong-password
      POSTGRES_DB: appuser
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      # shell form so $POSTGRES_USER is expanded inside the container
      test: ["CMD-SHELL", "pg_isready -U \"$POSTGRES_USER\" -d \"$POSTGRES_DB\""]
      interval: 5s
      timeout: 5s
      retries: 5
volumes:
  pgdata:

Now the check asks for whatever user the container was built with, so it cannot drift out of sync with POSTGRES_USER.

Verification

Two checks prove you are out of it. First, the role you expect now exists:

docker compose exec db psql -U postgres -c "SELECT current_user, current_database();"
 current_user | current_database
--------------+------------------
 postgres     | appuser
(1 row)

Second, the container reports healthy rather than restarting:

docker compose ps
# STATUS should read "Up (healthy)", not "Restarting" or "Up (unhealthy)"

What people get wrong

Adding GRANT statements. You cannot grant privileges to a role that does not exist. The error is not about privileges. Every minute spent on GRANT ALL is a minute not spent creating the role.

Editing POSTGRES_USER and restarting, expecting it to take. This is the core misunderstanding. Restarting re-runs the entrypoint, the entrypoint sees an initialized volume, and it skips the code that reads your variable. You can edit and restart a hundred times; nothing changes until the volume is empty or you change the role inside SQL.

chmod or chown on the data directory. Sometimes suggested because the message vaguely smells like access. It is not a filesystem permission issue, and changing ownership of PGDATA under a running server can actually break it. Leave the data directory alone.

Reaching for docker compose down -v in production. On a laptop it is fine. On a server it is the command that turns a five-minute role fix into a restore from backup. The -v is not harmless; it is the whole database.

When it is still broken

  1. Even the default postgres user fails. Your first init used a custom user and you have forgotten it. Your original compose file or its git history will name it: search old commits for POSTGRES_USER. A Postgres cluster always has at least one superuser, so the value is recoverable.
  2. The role appears in \du but login still fails. Now it really is authentication. Check the password and the pg_hba.conf method, and see too many clients already if the failure is intermittent rather than constant.
  3. It works from inside the container but not from your app. You are probably hitting a host Postgres. Stop the container and re-run your app's connection; if it still connects, you were never talking to the container.
  4. You are using Kubernetes, not Compose. Same rule, different object: the PersistentVolumeClaim is the thing that persists. Deleting the pod does nothing; you have to delete the PVC to force re-initialization.

The durable lesson is worth stating plainly: in the Postgres image, environment variables build the cluster once and then go quiet. The named volume, not the compose file, is the source of truth for who the database thinks you are.

Frequently asked questions

Why does changing POSTGRES_USER in my docker-compose.yml do nothing?
Because POSTGRES_USER, POSTGRES_PASSWORD and POSTGRES_DB are only read the first time the container initializes an empty data directory. If a named volume already holds an initialized cluster, the entrypoint skips initdb entirely and every one of those variables is ignored. The role that was created on the very first run is the only role that exists, regardless of what the variables say now.
How do I fix 'role postgres does not exist' without losing my data?
Connect as the role that actually exists (the value POSTGRES_USER had on first init) and create or rename the role you need: CREATE ROLE postgres LOGIN SUPERUSER PASSWORD '...'. If you no longer know the original user, check the first-run compose file or its git history. Only remove the volume if the data is disposable, because that destroys the database.
Should my healthcheck use pg_isready -U postgres?
Not if you customized POSTGRES_USER. A hardcoded pg_isready -U postgres queries a role that does not exist and the healthcheck fails forever while the database is actually fine. Use the variable instead, pg_isready -U "$POSTGRES_USER", so the check follows whatever user the container was initialized with.
Does removing the volume delete my database?
Yes, completely. docker compose down -v or docker volume rm deletes the entire cluster and lets initdb run again from scratch with your current environment variables. Only do this in development or after you have a verified backup. In production, add or rename the role inside the running database instead.
#PostgreSQL#Docker#Docker Compose#Volumes#Environment Variables
Keep reading

Related articles