2025, Sep 29 07:17

Как подключить Python‑сервис к Postgres в Docker: рабочий шаблон

Разбираем, почему psycopg2 не видит хост db на сборке, и даём схему: healthcheck Postgres, сеть хоста для тестов, два коннекта для сборки и рантайма сервиса.

Подключение Python‑сервиса к Postgres внутри Docker — задача вроде бы обыденная, но легко ошибиться из‑за того, где именно выполняется код. Характерный симптом напоминает проблему с DNS в psycopg2: имя сервиса работает во время выполнения, но падает на этапе сборки образа и при тестах. Ниже — краткий разбор сценария сбоя и рабочий шаблон, который решает его без догадок.

Минимальная воспроизводимая конфигурация с ошибкой

Код приложения устанавливает прямое соединение с Postgres, используя имя сервиса в качестве хоста. Такая схема корректно работает, когда контейнер запущен внутри сети Compose.

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()

Ошибка прямо указывает на разрешение DNS. psycopg2 не может преобразовать имя хоста в адрес, хотя имя сервиса в Compose выглядит верным.

psycopg2.OperationalError: could not translate host name "db" to address: Name or service not known

Что на самом деле происходит

Разрешение имени "db" работает только внутри сети Docker, которую Compose создаёт для запущенных сервисов. Когда вы пытаетесь прогонять интеграционные тесты во время сборки образа, этот этап сборки не подключён к сети Compose. Он не увидит имя сервиса "db", если явно не изменить сетевой режим для сборки. Из запущенного контейнера имя сервиса пингуется успешно — это подтверждает, что сеть и DNS на этапе выполнения настроены верно. Сбой происходит в контексте сборки, а не после подъёма контейнеров.

Рабочее решение от начала до конца

Проблему решают две согласованные правки. Во‑первых, убедитесь, что контейнер с базой данных перешёл в состояние healthy до сборки другого образа. Во‑вторых, подключите сборку образа к сети хоста, чтобы интеграционные тесты обращались к проброшенному на хост порту, а не к внутреннему DNS‑имени Compose. После этого используйте две строки подключения: одну для тестов во время сборки (сеть хоста через localhost и опубликованный порт) и другую — для работающего приложения (внутреннее DNS‑имя и порт контейнера).

Обновлённый docker-compose с заранее созданной внешней сетью, проверкой готовности (healthcheck) и сетевым режимом host для сборки dataseeder:

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

Двухэтапный Dockerfile запускает интеграционные тесты на стадии сборки и формирует более лёгкий релизный этап:

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"]

Когда сборка подключена к сети хоста, а БД опубликована на порту 5431, тесты подключаются к localhost и опубликованному порту. Во время выполнения приложение соединяется по имени сервиса и порту контейнера. Это разделение — ключ к успешной работе обоих этапов.

# во время тестов (этап сборки использует сеть хоста)
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()
# во время выполнения внутри сети Compose
if __name__ == "__main__":
    with PgSessionWithHost("EntitiesAndValues", "data_seeder", "db", 5432) as cn:
        svc = EntitiesValuesFunctions(cn)
        seeder = DataSeeder(svc)
        seeder.run()

Почему это важно

Время сборки и время исполнения — это разные сетевые контексты. Имя сервиса Compose, например "db", резолвится во внутренней сети, когда контейнеры уже запущены, но нет гарантии, что оно будет резолвиться во время сборки образа. Привязка сборки к сети хоста позволяет запускать интеграционные тесты до получения финального образа — при условии, что база данных уже поднята, здорова и её порт опубликован. Либо можно вовсе не запускать интеграционные тесты внутри сборки, а выполнить их после развёртывания всей системы; тогда окружение будет соответствовать производственной топологии. Оба подхода рабочие; важно чётко задавать, к какой сети ваш код обращается на каждом этапе.

Ещё одно практическое замечание: в документации указано, что для сборки можно задавать пользовательскую сеть. В этой конфигурации такие сети не сработали как ожидалось, тогда как режим сети хоста — сработал. Если столкнётесь с похожим поведением, переключение сборки на сеть хоста — прагматичный выход.

Выводы

Закладывайте жизненный цикл контейнеров и сетевой контекст в стратегию подключения как первостепенные факторы. Убедитесь, что база данных здорова, прежде чем к ней что‑либо подключается. Для тестов, выполняемых во время сборки, используйте сеть хоста и опубликованный порт; во время выполнения — внутреннее имя сервиса и порт контейнера. Если службы приходится запускать раздельно, внешняя сеть помогает удерживать их на одном логическом мосту и не позволяет каждому запуску Compose создавать изолированную сеть.

Статья основана на вопросе на StackOverflow от Rory Coble и ответе от Rory Coble.