Your Postgres container runs, but every login fails with password authentication failed for user "postgres". Or psql inside the container says role "root" does not exist. Or the container exits right away with Database is uninitialized and superuser password is not specified. The password in your .env file looks right, and that is the trap.
This guide reproduces each of these errors with the official postgres:18.6 image in Docker Compose and fixes them. Every error message below comes from that lab run on November 3, 2026, with a small Node.js client (pg 8.23.1) standing in for your app. Most of them have one root cause: the image reads its POSTGRES_* variables only once, the first time it creates the database.
These questions get a lot of traffic. On Stack Overflow, Connecting to Postgresql in a docker container from outside had 986,699 views on November 3, 2026, and postgres with docker compose gives FATAL: role "root" does not exist error had 188,086.
The short version:
- Changing
POSTGRES_PASSWORDafter the first start does nothing. The log saysSkipping initialization. Change the password withALTER ROLE ... PASSWORDthroughdocker compose exec, or delete the volume if the data is disposable. -
role "root" does not existmeanspsqlorpg_isreadyran without-U. Pass-Uwith the user fromPOSTGRES_USER. If you setPOSTGRES_USER, there is nopostgresrole at all. -
superuser password is not specifiedmeans the password reached the container empty. Check for an unset.envvariable or a$in the password, and use${DB_PASSWORD:?}so Compose stops before it starts anything. - From another container, connect to the service name, not
localhost, and wait for a healthcheck withcondition: service_healthy. URL-encode special characters in connection strings.
Fix password authentication failed after changing POSTGRES_PASSWORD
You started the stack once, then changed the password in .env or compose.yaml, recreated the container, and now nothing can log in with the new password.
Why the new password never applied
The official image runs initdb only when the data directory is empty. The image documentation says it plainly: "the Docker specific variables will only have an effect if you start the container with a data directory that is empty." An existing database is left as it is.
In the lab, the volume was first created with DB_PASSWORD=first-secret. After changing .env to second-secret and running docker compose up -d --force-recreate, the log showed:
PostgreSQL Database directory appears to contain a database; Skipping initialization The container's environment had the new value, POSTGRES_PASSWORD=second-secret. The database still had the old one. A login with the new password failed with password authentication failed for user "postgres" (SQLSTATE 28P01), and the server log named the pg_hba.conf rule the connection matched:
FATAL: password authentication failed for user "postgres" DETAIL: Connection matched file "/var/lib/postgresql/18/docker/pg_hba.conf" line 128: "host all all all scram-sha-256" The same rule explains why init scripts seem to be ignored. A file mounted into /docker-entrypoint-initdb.d after the first start never ran, and select count(*) from notes returned relation "notes" does not exist.
Change the password inside the database
The image's pg_hba.conf trusts connections over the local Unix socket, so docker compose exec gets you in without the password. Set the new password with SQL:
docker compose exec db psql -U postgres -c "ALTER ROLE postgres WITH PASSWORD 'second-secret'" It printed ALTER ROLE. The app logged in with second-secret right away, without a restart, and first-secret stopped working. Keep .env and the database in sync from now on, because the app reads the new value from .env while the database only changes through SQL.
If the data is disposable, delete the volume instead. docker compose down -v removes the project's volumes, and with them all data. On the next up, the log showed running /docker-entrypoint-initdb.d/01-notes.sql, the notes table existed, and the new password worked. The MySQL and MongoDB guides cover the same first-start rule for those images.
Fix role "root" does not exist and role "postgres" does not exist
The second family of errors comes from the user name, not the password.
psql and pg_isready inside the container
docker compose exec db psql runs as root inside the container. Without -U, psql uses that operating system user as the database user:
psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL: role "root" does not exist The top answer on the Stack Overflow question, by richyen, puts it this way: "If you don't specify the PGUSER environment variable, then psql will assume you want to use the current OS user as your database user name." Pass the user, as in psql -U postgres.
The same thing happens in healthchecks. With test: ["CMD", "pg_isready"], the container still showed (healthy), because pg_isready reported the server as up even though the role was rejected. But the server log got a new FATAL: role "root" does not exist line every 5 seconds, one per check. With ["CMD", "pg_isready", "-U", "app", "-d", "app"], the log had 0 FATAL lines over the next 20 seconds.
POSTGRES_USER replaces the postgres role
When you set POSTGRES_USER, the image creates that user as the superuser and does not create postgres. With POSTGRES_USER: app and no POSTGRES_DB, the lab cluster had one role, app (Superuser, Create role, Create DB, Replication, Bypass RLS), and a database also named app, matching the docs: the variable creates "the specified user with superuser power and a database with the same name."
So these failed:
$ docker compose exec db psql -U postgres ... FATAL: role "postgres" does not exist $ docker compose exec db psql -U app -d appdb ... FATAL: database "appdb" does not exist Over TCP, the error is less helpful. Logging in as postgres from the app with the app password returned password authentication failed for user "postgres", which sends you after the wrong problem. Only the server log told the truth: DETAIL: Role "postgres" does not exist. Check the server log before you reset any password.
Use the user and database you set, in psql, in the healthcheck and in the app's connection string. If an app insists on a postgres role or another database, create them with SQL: CREATE ROLE postgres WITH LOGIN SUPERUSER PASSWORD '...' and createdb -U app appdb both worked in the lab, and the app then logged in to appdb as postgres.
Fix Database is uninitialized and superuser password is not specified
This one stops the container on its first start. It exited with code 1 and printed:
Error: Database is uninitialized and superuser password is not specified. You must specify POSTGRES_PASSWORD to a non-empty value for the superuser. For example, "-e POSTGRES_PASSWORD=password" on "docker run". The message goes on to offer POSTGRES_HOST_AUTH_METHOD=trust, which lets anyone connect without a password. Don't use that to get past this error. If you did set a password, check how it gets into the container.
Unset .env variables and dollar signs
In the lab, the compose file had POSTGRES_PASSWORD: ${DB_PASSWORD} and there was no .env file next to it. Compose printed a warning and started the container anyway, with an empty password:
level=warning msg="The \"DB_PASSWORD\" variable is not set. Defaulting to a blank string." A $ inside a password does something similar. Compose treats it as the start of a variable. With POSTGRES_PASSWORD: pa$word42 written straight into compose.yaml, Compose warned that word42 was not set, and the container got POSTGRES_PASSWORD=pa. The cluster was created with the password pa, so pa$word42 failed with password authentication failed and pa worked. The Compose interpolation docs give the fix: "You can use a $$ (double-dollar sign) when your configuration needs a literal dollar sign." With pa$$word42, the container got pa$word42.
Make Compose fail before it starts anything
Replace ${DB_PASSWORD} with ${DB_PASSWORD:?set DB_PASSWORD in .env}. With no .env file, docker compose up -d stopped right away and started nothing:
error while interpolating services.db.environment.POSTGRES_PASSWORD: required variable DB_PASSWORD is missing a value: set DB_PASSWORD in .env This matters more than it looks. An empty password stops Postgres on the first start, but the pa case above did not stop anything. It created a database with the wrong password, and the variable rule from the first section then kept that password for good.
Fix connection refused and pg_hba errors from other containers
If the password and user are right and the app still can't connect, look at the address, the timing and the connection string.
localhost, published ports and startup order
Inside a container, localhost is that container. An app with @localhost:5432 in its URL got connect ECONNREFUSED ::1:5432, connect ECONNREFUSED 127.0.0.1:5432. Use the Compose service name, such as db, as the host. node-postgres defaults to PGHOST=localhost when you set no host, so always set it.
From the host machine, the container is only reachable if you publish the port. Without ports, psql -h 127.0.0.1 -p 5432 on the host got Connection refused. With ports: - "127.0.0.1:5433:5432", the host connected on port 5433 and read the data. Binding to 127.0.0.1 keeps the port off the host's other network interfaces.
Timing is the other trap. With depends_on: - db and no condition, the app started as soon as the database container did. On a fresh volume with an init script, it failed with connect ECONNREFUSED 172.28.0.2:5432 and exited. During the first start, the entrypoint runs a temporary server with listen_addresses='', so nothing listens on TCP until the init scripts are done. Starting the app again a few seconds later worked. The fix is a healthcheck plus condition: service_healthy, shown in the Compose file below. The Grafana and Prometheus guide and the Redis guide run into the same localhost mistake.
Passwords in URLs and pg_hba.conf entries
Connection strings are URLs, so @, #, / and : in a password break them. With the password p@ss#word/1 pasted into postgres://app:p@ss#word/1@db:5432/app, the parser read the password as p, the host as ss and no database at all. Encode the password first. node -p 'encodeURIComponent("p@ss#word/1")' printed p%40ss%23word%2F1, and the URL with that value logged in. Or skip the URL and pass PGHOST, PGUSER, PGPASSWORD and PGDATABASE as separate variables, as the Compose file below does.
You only see no pg_hba.conf entry if you replaced the image's default pg_hba.conf, which ends with host all all all scram-sha-256 and allows every address with a password. A custom file that only allowed 127.0.0.1/32 over TCP rejected the app container:
no pg_hba.conf entry for host "172.28.0.4", user "postgres", database "postgres", no encryption Add a line for the Docker network. The pg_hba.conf docs describe samenet as "any address in any subnet that the server is directly connected to", which covers the Compose network. After adding host all all samenet scram-sha-256 and running SELECT pg_reload_conf(), the same login worked without a restart. If you carried a custom file over from an upgrade, the PostgreSQL 17 to 18 upgrade guide covers the other places it can break.
A PostgreSQL Compose file that avoids these errors
This file puts the fixes together. It ran verbatim in the lab. The app service is the small Node.js test client from the lab. Replace its image, command and volume with your own app and keep the environment and depends_on parts.
The compose file
Create .env next to it with a random password. Hex characters need no escaping in Compose or in URLs:
printf 'DB_PASSWORD=%s\n' "$(openssl rand -hex 24)" > .env services: db: image: postgres:18.6 restart: unless-stopped environment: POSTGRES_USER: app POSTGRES_DB: app POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD in .env} volumes: - pgdata:/var/lib/postgresql - ./initdb:/docker-entrypoint-initdb.d:ro healthcheck: test: ["CMD", "pg_isready", "-U", "app", "-d", "app"] interval: 5s timeout: 3s retries: 10 start_period: 30s app: image: node:24.22.2-alpine working_dir: /app command: node pgtest.mjs environment: PGHOST: db PGUSER: app PGDATABASE: app PGPASSWORD: ${DB_PASSWORD:?set DB_PASSWORD in .env} volumes: - ./app:/app:ro depends_on: db: condition: service_healthy volumes: pgdata: The volume is mounted at /var/lib/postgresql, the path that the 18 images expect. Both services read the same DB_PASSWORD, so there is one place to change it. If you change it later, also run the ALTER ROLE command from the first section, or the app gets 28P01 again.
Check that it works
Run docker compose up -d. Compose waited for the database before it started the app:
Container pgfinal-db-1 Waiting Container pgfinal-db-1 Healthy Container pgfinal-app-1 Starting Container pgfinal-app-1 Started The app printed OK app app PostgreSQL 18.6 (Debian 18.6-1.pgdg13+2), and the log showed running /docker-entrypoint-initdb.d/01-schema.sql. After docker compose restart db and docker compose run --rm app, the app connected again, and the database log had 0 FATAL lines for the whole run. For a full app stack on this pattern, see the Umami with PostgreSQL guide.
FAQ
Passwords and users
Why does Postgres in Docker say password authentication failed when the password is right?
Either the database still has the password from its first start, or the user doesn't exist. Look at the server log with docker compose logs db. Skipping initialization at startup means your new POSTGRES_PASSWORD was ignored. DETAIL: Role "..." does not exist. means the user name is wrong.
How do I use a new password from .env without removing the volume?
Run docker compose exec db psql -U <user> -c "ALTER ROLE <user> WITH PASSWORD '<new>'". It works over the local socket without the old password and takes effect right away. Then keep the same value in .env for the app.
Why do I get role "root" does not exist?
Something ran psql or pg_isready inside the container without -U, so it used the operating system user root. Add -U with your POSTGRES_USER (and -d with your database) to the command or the healthcheck.
Setup and connections
How do I connect to Postgres in Docker from the host?
Publish the port, for example ports: - "127.0.0.1:5433:5432", then connect to 127.0.0.1 on port 5433 with the user and password from your Compose file. Other containers on the same network use the service name and port 5432 and need no published port.
Why didn't my docker-entrypoint-initdb.d script run?
Init scripts run only when the data directory is empty, the same as the POSTGRES_* variables. If the volume already has a database, the scripts are skipped. Run the SQL by hand with docker compose exec, or delete the volume if the data is disposable.
How do I fix Database is uninitialized and superuser password is not specified?
Give POSTGRES_PASSWORD a non-empty value. Check that your .env file sits next to compose.yaml and defines the variable, and run docker compose config to see the value Compose actually passes. Use ${DB_PASSWORD:?} so a missing value stops Compose with an error.







