2025, Sep 29 07:00
Resolve psycopg2 DNS failures: connecting a Python service to Postgres in Docker across build and runtime
Learn why psycopg2 can't resolve the Postgres host during Docker builds and how to fix it with host networking, healthchecks, and dual connection settings.
Connecting a Python service to Postgres inside Docker should be routine, yet it’s easy to get tripped up by where the code actually runs. A common symptom looks like a DNS problem from psycopg2: the service name works at runtime, but fails during image build and tests. Below is a concise walkthrough of the failure mode and a working pattern to fix it without guesswork.
Minimal failing setup
The application code makes a straightforward connection to Postgres using the service name as the host. The structure is fine when the container is running inside the Compose network.
import psycopg2
class PgSession:
    def __init__(self, db_label, login_user):
        self.db_label = db_label
        self.login_user = login_user
        self.link = None
    
    def __enter__(self):
        self.link = psycopg2.connect(
            user=self.login_user,
            database=self.db_label,
            password="data",
            host="db",
            port=5432
        )
        return self.link
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.link.close()
The observed error is explicit about DNS resolution. psycopg2 can’t translate the hostname into an address, even though the Compose service name looks correct.
psycopg2.OperationalError: could not translate host name "db" to address: Name or service not known
What’s actually happening
The name resolution for "db" works only inside the Docker network that Compose creates for your running services. When you try to run integration tests during the image build, that build phase is not attached to the Compose network. It won’t see the service name "db" unless you explicitly change the build networking mode. From a running container you can ping the service name successfully, which confirms the network and DNS wiring is fine at runtime. The failure happens during the build context, not after the containers are up.
The fix that works end to end
Two coordinated changes resolve the problem. First, ensure the database container is healthy before the other image is built. Second, attach the image build to the host network so that integration tests run against the host-mapped port instead of the internal Compose DNS name. With that in place, use two connection strings: one for tests during build (host network via localhost and the published port), and a different one for the running application (internal DNS name and the container port).
Updated docker-compose with a pre-created external network, healthcheck, and host network for the dataseeder build:
networks:
  backend:
    name: value_tracker_backend
    external: true
services:
  db:
    build:
      context: ./sql
      dockerfile: db.dockerfile
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: EntitiesAndValues
    ports:
      - "5431:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d EntitiesAndValues"]
      interval: 10s
      retries: 5
      start_period: 30s
      timeout: 10s
    networks:
      - backend
  dataseeder:
    build:
      context: .
      dockerfile: dataseeder.dockerfile
      network: host
    depends_on:
      db:
        condition: service_healthy
        restart: true
    networks:
      - backend
A two-stage Dockerfile runs integration tests during the build stage and prepares a slimmer release stage:
FROM python:latest AS base
RUN apt-get update
COPY packages/ packages/
COPY tests/ tests/
COPY localtest.txt .
COPY DataSeeder.py .
RUN pip install -r localtest.txt
RUN pytest -v tests/
FROM base AS release
RUN apt-get update
COPY packages/ packages/
COPY release.txt .
COPY DataSeeder.py .
RUN pip install -r release.txt
CMD ["python","DataSeeder.py"]
With the build attached to the host network and the DB published on 5431, tests connect to localhost and the published port. At runtime, the app connects to the service name and the container port. This split is the key to making both phases succeed.
# during tests (build stage uses host network)
import psycopg2
import pytest
class PgSessionWithHost:
    def __init__(self, db_label, login_user, db_host, db_port):
        self.db_label = db_label
        self.login_user = login_user
        self.db_host = db_host
        self.db_port = db_port
        self.link = None
    def __enter__(self):
        self.link = psycopg2.connect(
            user=self.login_user,
            database=self.db_label,
            password="data",
            host=self.db_host,
            port=self.db_port
        )
        return self.link
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.link.close()
@pytest.fixture(scope="session")
def bootstrap():
    with PgSessionWithHost("EntitiesAndValues", "data_seeder", "localhost", 5431) as cn:
        svc = EntitiesValuesFunctions(cn)
        with svc.conn.cursor() as cur:
            cur.execute("TRUNCATE TABLE public.entities, public.entity_values;")
            cn.commit()
        yield svc
        with svc.conn.cursor() as cur:
            cur.execute("TRUNCATE TABLE public.entities, public.entity_values;")
            cn.commit()
# at runtime inside the Compose network
if __name__ == "__main__":
    with PgSessionWithHost("EntitiesAndValues", "data_seeder", "db", 5432) as cn:
        svc = EntitiesValuesFunctions(cn)
        seeder = DataSeeder(svc)
        seeder.run()
Why this matters
Build-time and runtime are different network contexts. A Compose service name like "db" resolves inside the internal network once containers are running, but it is not guaranteed to resolve during the image build. Aligning the build with the host network unlocks integration tests that run before the final image is produced, provided the database is already up and healthy and its port is published. Alternatively, you can avoid running integration tests in the image build and execute them after the full system is deployed; the environment will then match the production-like topology. Both approaches are valid; the important part is to be explicit about which network your code targets at each phase.
One more practical note: documentation indicates it should be possible to specify a custom network for the build. In this setup, custom networks did not work as expected, while the host network mode did. If you run into similar behavior, switching the build to the host network is a pragmatic path forward.
Takeaways
Make the container lifecycle and network context first-class concerns in your connection strategy. Ensure the database is healthy before anything attempts to connect. Use host networking plus a published port for tests that run during the build, and the internal service name and container port at runtime. If you must start services separately, an external network helps keep them on the same logical bridge and prevents each Compose run from creating an isolated network.
The article is based on a question from StackOverflow by Rory Coble and an answer by Rory Coble.