Fix Django Cannot Connect to Postgres in Docker Compose

Fix Django connection errors to PostgreSQL in Docker Compose: localhost versus service names, readiness health checks, credentials in volumes, networks, and ports.

Problem statement

Django in a Docker Compose stack cannot reach the PostgreSQL container:

django.db.utils.OperationalError: connection to server at "localhost" (127.0.0.1), port 5432 failed:
Connection refused
django.db.utils.OperationalError: could not translate host name "db" to address: Name or service not known
django.db.utils.OperationalError: connection to server at "db" (172.20.0.2), port 5432 failed:
FATAL:  password authentication failed for user "app"

Each message points to a different cause. Compose gives every service its own container with its own network stack, a DNS name, and a lifecycle. Django settings that worked with a local PostgreSQL installation (host localhost) or a first run of the stack (credentials that later changed) stop working here.

Container networking for Postgresweb containerDATABASE_URLCompose networkservice name DNSdb containerport 5432Volumepersistent dataInside a container localhost is that container; use the service name instead.

Quick answer

  1. Use the Compose service name as HOST (for example db), never localhost or 127.0.0.1.
  2. Use the container port 5432, not a host-mapped port such as 5433.
  3. Make sure both services share a network (the default project network, or the same custom network).
  4. Wait for readiness with a pg_isready health check and depends_on: condition: service_healthy.
  5. If authentication fails after changing POSTGRES_PASSWORD, remember the variable only applies when the data volume is first initialized; change the password inside the database or recreate the volume in non-production environments.
  6. Test from inside the web container with psql or a Python connection.

Step-by-step solution

1. A working baseline

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 5s
      timeout: 3s
      retries: 10

  web:
    build: .
    environment:
      DATABASE_URL: postgres://app:${POSTGRES_PASSWORD}@db:5432/app
    depends_on:
      db:
        condition: service_healthy

volumes:
  pgdata:

${POSTGRES_PASSWORD} is substituted by Compose from your shell environment or the project .env file. The Django side reads DATABASE_URL, for example with dj-database-url.

2. "Connection refused" on localhost

Inside the web container, localhost is the web container. PostgreSQL runs in the db container, reachable through Compose's internal DNS at the service name.

localhost is the container itselfHOST = "localhost"✕points at the web container✕no PostgreSQL there✕Connection refusedHOST = "db"✓the Compose service name✓resolved by Docker DNS✓works on the default networkEach container has its own localhostdjango-deployment.com
The same settings file can read HOST from the environment for both cases.

Wrong:

DATABASE_URL=postgres://app:secret@localhost:5432/app

Right:

DATABASE_URL=postgres://app:secret@db:5432/app

The same applies to DB_HOST settings. A local .env file created for running Django directly on your laptop often contains localhost; do not reuse it inside containers.

3. Host ports versus container ports

  db:
    ports:
      - "5433:5432"
host and port depend on where you connect fromHostPortFrom the web container✓db✓5432 (container port)From your laptoplocalhost5433 (published, if any)From another host✕not at alldo not publish in productionPublished ports only matter from outside Dockerdjango-deployment.com
A ports: mapping like 5433:5432 changes nothing for other containers.

This publishes the container's port 5432 on host port 5433, for tools on your laptop. Containers on the same network still connect to db:5432. Using db:5433 from web fails.

In production, do not publish the database port at all unless something outside Docker needs it; the web container does not need published ports to reach db.

4. "Could not translate host name"

The web container cannot resolve db. Causes:

  • the service is named differently (postgres, database); the hostname must match the service key in compose.yaml
  • the services are in different Compose projects (started from different directories or with different -p names), so they are on different default networks
  • custom networks: are defined and the two services do not share one

Check:

docker compose ps
docker network ls
docker inspect --format '{{json .NetworkSettings.Networks}}' $(docker compose ps -q web) | python3 -m json.tool
docker compose exec web getent hosts db

getent hosts db prints the database container's IP when DNS works. To connect services from different projects, attach both to a shared external network:

networks:
  shared:
    external: true

5. Race at startup

Without a health check, depends_on only starts db before web. PostgreSQL may need several seconds to initialize, especially on first run with an empty volume, so Django's first connection attempt (for example migrations in the entrypoint) fails with "connection refused".

started is not the same as readydb container startsinitialisingweb startsdepends_on: startedmigrate runsconnection refuseddb readypg_isready passesFixcondition: service_healthyA healthcheck on db plus service_healthy removes the racedjango-deployment.com
The first start of a new volume takes longest, which is when this bites.

Use the health check shown in the baseline. pg_isready reports ready once the server accepts connections. For platforms that ignore depends_on conditions, add a small retry loop in the entrypoint:

#!/bin/sh
set -e

python - <<'PY'
import os, time
import psycopg

url = os.environ["DATABASE_URL"]
for attempt in range(30):
    try:
        psycopg.connect(url, connect_timeout=3).close()
        break
    except psycopg.OperationalError:
        time.sleep(2)
else:
    raise SystemExit("database not reachable")
PY

python manage.py migrate --noinput
exec "$@"

6. "Password authentication failed" after changing credentials

The official PostgreSQL image uses POSTGRES_USER, POSTGRES_PASSWORD, and POSTGRES_DB only when it initializes an empty data directory. If the pgdata volume already exists from an earlier run, changing these variables has no effect; the database keeps the old password.

Options:

  • Keep data: change the password inside the database, then update DATABASE_URL:
docker compose exec db psql -U app -d app -c "ALTER ROLE app WITH PASSWORD 'new-password';"
  • Development only: remove the volume and let the image initialize again, which deletes all data:
docker compose down -v
docker compose up -d

Never run down -v on a production stack without a verified backup.

7. Special characters in passwords

A password containing @, :, /, #, or % breaks URL parsing in DATABASE_URL. Either URL-encode the password or pass connection parameters separately:

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "HOST": os.environ.get("DB_HOST", "db"),
        "PORT": os.environ.get("DB_PORT", "5432"),
        "NAME": os.environ["DB_NAME"],
        "USER": os.environ["DB_USER"],
        "PASSWORD": os.environ["DB_PASSWORD"],
    }
}

Encode for URLs with Python:

python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1], safe=''))" 'p@ss:w/rd'

8. Connecting from the host instead

If you run python manage.py on your laptop against the database in Compose, the rules reverse: use localhost and the published host port (for example 5433). Keep separate environment files for "Django in a container" and "Django on the host" to avoid mixing them.

9. Test from inside the web container

docker compose exec web python -c "
import os, psycopg
with psycopg.connect(os.environ['DATABASE_URL'], connect_timeout=5) as conn:
    print(conn.execute('select version()').fetchone()[0])
"
docker compose exec web python manage.py showmigrations | head -5

If psql is available in the image or you use the database container:

docker compose exec db psql -U app -d app -c "select 1;"

Verification check:

  • the Python connection prints the PostgreSQL version
  • showmigrations lists migrations without errors
  • docker compose ps shows db as healthy before web starts
  • restarting the whole stack with docker compose up -d --force-recreate works without manual steps

Explanation

Docker Compose creates a private network per project and registers each service name in an internal DNS server. Containers talk to each other by name on the ports the processes listen on inside the containers. localhost and published host ports belong to a different network context. Most connection errors come from mixing those contexts, starting before the database accepts connections, or expecting image initialization variables to update an existing data volume.

the error names the fixErrorUsual causeConnection refusedlocalhost used, or db not ready yetcould not translate host name "db"different network orwrong service namepassword authentication failedvolume initialisedwith old credentialsdatabase "app" does not existPOSTGRES_DB changed after first startPOSTGRES_* variables only apply when the volume is emptydjango-deployment.com
Changing credentials later means ALTER USER inside the database, or a new volume.

Edge cases / notes

Database on the host, Django in Docker

Connecting from a container to a PostgreSQL service installed on the host requires the host's address as seen from the container (for example the Docker bridge gateway, or host.docker.internal where supported and configured) and PostgreSQL listening on that interface with a matching pg_hba.conf entry.

Managed databases

When using a managed database instead of a db service, the hostname is the provider's endpoint, TLS is usually required (sslmode=require), and outbound network rules must allow the connection.

Podman and rootless setups

Name resolution between containers depends on the network backend. Use a user-defined network so containers can resolve each other by name.

Separate settings for container and host workflows

Many teams run Django both inside Compose and directly on their laptops against the same database container. Keeping two small environment files avoids editing values back and forth:

# .env.docker  (used by compose.yaml env_file for the web service)
DB_HOST=db
DB_PORT=5432
# .env.local  (loaded by your shell or IDE when running manage.py on the host)
DB_HOST=127.0.0.1
DB_PORT=5433

With ports: ["127.0.0.1:5433:5432"] on the db service, the host workflow uses the published port, bound to localhost only, while containers use the internal name and port.

Inspecting the database container when it will not become healthy

docker compose logs --tail=50 db
docker compose exec db pg_isready -U app -d app
docker compose exec db ls -la /var/lib/postgresql/data | head

Common reasons PostgreSQL never becomes healthy:

  • Data directory from a different major version. A volume initialized by postgres:15 cannot be started by postgres:16; the log says the data directory was initialized by an incompatible version. Upgrade with a dump and restore (or pg_upgrade), or pin the old image until you migrate.
  • Bind-mounted directory with wrong ownership. The server refuses to start if it cannot own its data directory. Named volumes avoid this.
  • Health check using the wrong user or database. pg_isready -U app -d app must match POSTGRES_USER and POSTGRES_DB; otherwise the check can fail even though the server is up.

Upgrading the Postgres image safely

  1. Dump the database from the running old container: docker compose exec db pg_dump -U app -Fc app > app.dump.
  2. Stop the stack, change the image tag, and create a new volume name (keep the old volume untouched as a fallback).
  3. Start the new database, restore with pg_restore, and point Django at it.
  4. Remove the old volume only after verifying the application and a fresh backup.

Common mistakes

  • Reusing a laptop .env with DB_HOST=localhost inside containers.
  • Connecting to the published host port from another container.
  • Changing POSTGRES_PASSWORD and expecting an existing volume to update.
  • Running docker compose down -v on production to "reset" credentials.

Next steps

FAQ

What host should Django use for Postgres in Docker Compose?

The service name of the database in compose.yaml, such as db, with the container port 5432. localhost inside the web container refers to the web container itself.

Why does depends_on not fix "connection refused" to Postgres?

Plain depends_on only controls start order. Add a pg_isready health check to the database and use condition: service_healthy.

Why doesn't changing POSTGRES_PASSWORD work?

The postgres image only applies it when initializing an empty data directory. With an existing volume, change the password with ALTER ROLE inside the database.

Should I publish the Postgres port in production Compose files?

Usually not. Containers on the same network connect without published ports, and publishing exposes the database on the host's interfaces.

Keep going