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.
Quick answer
- Use the Compose service name as
HOST(for exampledb), neverlocalhostor127.0.0.1. - Use the container port
5432, not a host-mapped port such as5433. - Make sure both services share a network (the default project network, or the same custom network).
- Wait for readiness with a
pg_isreadyhealth check anddepends_on: condition: service_healthy. - 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. - Test from inside the web container with
psqlor 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.
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"
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 incompose.yaml - the services are in different Compose projects (started from different directories or with different
-pnames), 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".
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
showmigrationslists migrations without errorsdocker compose psshowsdbashealthybeforewebstarts- restarting the whole stack with
docker compose up -d --force-recreateworks 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.
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:15cannot be started bypostgres:16; the log says the data directory was initialized by an incompatible version. Upgrade with a dump and restore (orpg_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 appmust matchPOSTGRES_USERandPOSTGRES_DB; otherwise the check can fail even though the server is up.
Upgrading the Postgres image safely
- Dump the database from the running old container:
docker compose exec db pg_dump -U app -Fc app > app.dump. - Stop the stack, change the image tag, and create a new volume name (keep the old volume untouched as a fallback).
- Start the new database, restore with
pg_restore, and point Django at it. - Remove the old volume only after verifying the application and a fresh backup.
Common mistakes
- Reusing a laptop
.envwithDB_HOST=localhostinside containers. - Connecting to the published host port from another container.
- Changing
POSTGRES_PASSWORDand expecting an existing volume to update. - Running
docker compose down -von production to "reset" credentials.
Next steps
- Build the full stack with Deploy Django with Docker Compose in Production.
- If the container stops right after start, see Fix a Django Docker Container That Exits Immediately.
- For non-Docker database errors, see Fix Database Connection Errors in Django Production.
- The same networking rules apply to Redis: Fix Redis Connection Refused in Django.
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.