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_PASSWORD after the first start does nothing. The log says Skipping initialization. Change the password with ALTER ROLE ... PASSWORD through docker compose exec, or delete the volume if the data is disposable.
  • role "root" does not exist means psql or pg_isready ran without -U. Pass -U with the user from POSTGRES_USER. If you set POSTGRES_USER, there is no postgres role at all.
  • superuser password is not specified means the password reached the container empty. Check for an unset .env variable 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 with condition: 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.

Diagram: when the postgres container starts, the entrypoint checks if the data directory is empty. If yes, initdb creates POSTGRES_USER with its password, creates POSTGRES_DB and runs the initdb.d scripts. If no, it prints Skipping initialization and ignores the POSTGRES variables and scripts

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.

Terminal capture: after changing DB_PASSWORD in .env, the postgres log says Skipping initialization, the container environment shows the new password, the login fails with password authentication failed for user postgres and a pg_hba.conf scram-sha-256 detail line, and ALTER ROLE fixes it

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.

Terminal capture: with POSTGRES_USER set to app, psql without -U fails with role root does not exist, psql -U postgres fails with role postgres does not exist, a TCP login as postgres reports password authentication failed with a server detail that the role does not exist, and a pg_isready healthcheck without -U logs FATAL lines until -U app -d app is added

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.

Terminal capture: with DB_PASSWORD unset Compose warns and the postgres container exits with Database is uninitialized and superuser password is not specified, a dollar sign in the password is cut to pa until it is written as two dollar signs, and the colon question mark syntax makes Compose stop with an error before starting anything

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.

Terminal capture: the app gets ECONNREFUSED while the database runs its init script, localhost inside the app container is refused, an unencoded password is parsed as host ss, the encodeURIComponent value logs in, and a custom pg_hba.conf returns no pg_hba.conf entry for host until a samenet line is added

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.