Learning Platform
Глоссарий Troubleshooting
Урок 14.01 · 24 мин
Начальный
dockerpostgresdata-engineering

Postgres в контейнере

Postgres в контейнере — это первое, что Junior Data Engineer обычно поднимает локально. Контейнер позволяет за одну команду получить чистую базу нужной версии, без apt install postgresql-16, без правки pg_hba.conf, без чужих данных, оставленных предыдущим разработчиком. В этом уроке мы запустим Postgres 16, поговорим про обязательные переменные окружения, прикрутим volume для данных, проверим подключение через docker exec psql — и в конце разберём, почему в production-compose порт 5432 не торчит наружу.


Какой образ брать

Официальный образ называется postgres, лежит на Docker Hub. Стабильные теги: postgres:16, postgres:17, postgres:16-alpine, postgres:17-alpine. В мае 2026 года Postgres 17 — текущий stable, Postgres 16 — предыдущий LTS-like (поддерживается до 2028). Для Junior DE рекомендую брать 16: большинство production-баз на работе пока на 16, ты увидишь те же quirks.

Между postgres:16 (debian-based, около 430 МБ) и postgres:16-alpine (около 240 МБ) выбор зависит от задачи. Для локальной разработки можно alpine — он легче. Но если ты используешь расширения (pg_stat_statements, pgvector, postgis), стандартный debian-образ удобнее — там больше системных библиотек предустановлено, меньше вероятность словить «ld: library not found» при подключении расширения. Я обычно беру postgres:16 — лишние 200 МБ не стоят головной боли.

Слои образа postgres:16
debian:12-slimБазовый слой — урезанная Debian Bookworm, без apt-каша. Около 80 МБ
+
locales + tzdataЛокали и таймзоны — без них Postgres ругается при initdb. Около 30 МБ
postgresql-16 binariesСам сервер, psql, pg_dump, pg_basebackup и все утилиты. Около 200 МБ
+
docker-entrypoint.shСкрипт, который читает POSTGRES_* env, делает initdb, создаёт юзера/БД, выполняет init scripts из /docker-entrypoint-initdb.d, запускает postgres

Обязательные переменные окружения

Когда контейнер postgres стартует впервые (data dir пустой), entrypoint-скрипт хочет три вещи:

  • POSTGRES_PASSWORD — пароль суперпользователя. Обязательная переменная. Без неё контейнер падает с ошибкой Database is uninitialized and superuser password is not specified. Это сделано специально, чтобы случайно не оставить открытую базу с пустым паролем.
  • POSTGRES_USER — имя суперпользователя. Опциональная, по умолчанию postgres.
  • POSTGRES_DB — имя базы данных, которая создастся при первом запуске. Опциональная, по умолчанию = POSTGRES_USER.

Самый минимальный запуск:

docker run -d \
  --name pg \
  -e POSTGRES_PASSWORD=secret \
  postgres:16

Это запустит Postgres, создаст пользователя postgres с паролем secret, создаст базу postgres, начнёт слушать порт 5432 внутри контейнера. Но порт никуда не проброшен — снаружи в неё не достучаться. Поправим:

docker run -d \
  --name pg \
  -e POSTGRES_USER=de \
  -e POSTGRES_PASSWORD=secret \
  -e POSTGRES_DB=warehouse \
  -p 5432:5432 \
  postgres:16

Теперь localhost:5432 пробрасывается в контейнер, и из любой Python-программы или DBeaver можно подключиться. Стандартный URI:

postgresql://de:secret@localhost:5432/warehouse
TIP

Если порт 5432 на хосте уже занят (например, на macOS Homebrew поставил Postgres локально), запусти контейнер с -p 5433:5432 — внешний порт 5433 пробросится во внутренний 5432. URI станет localhost:5433/warehouse.


Зачем нужен volume

Если запустить контейнер как выше, данные базы будут лежать внутри контейнера в /var/lib/postgresql/data. Это внутри файловой системы контейнера — слой поверх образа. Как только мы сделаем docker rm pg, эта файловая система исчезает. Данные тоже.

Чтобы данные пережили рестарт и пересоздание контейнера, монтируем volume:

docker volume create pg-data

docker run -d \
  --name pg \
  -e POSTGRES_USER=de \
  -e POSTGRES_PASSWORD=secret \
  -e POSTGRES_DB=warehouse \
  -p 5432:5432 \
  -v pg-data:/var/lib/postgresql/data \
  postgres:16

Теперь /var/lib/postgresql/data внутри контейнера = volume pg-data на хосте. docker rm pg уберёт контейнер, но pg-data останется. Запустим заново с тем же -v pg-data:/var/lib/postgresql/data — entrypoint увидит непустую data dir и не будет запускать initdb, просто стартанёт сервер с существующими данными. POSTGRES_PASSWORD во втором запуске уже игнорируется — пароль остался с первого initdb.

Что происходит при docker run postgres
docker runCLI собирает параметры, шлёт в Docker daemon через UNIX-сокет
контейнер стартуетcontainerd создаёт namespaces, монтирует volume, запускает PID 1 = docker-entrypoint.sh
data dir пустой?Entrypoint проверяет /var/lib/postgresql/data: если пусто — initdb, если есть PG_VERSION — пропускает
да
initdb + создание user/dbinitdb создаёт структуру кластера, postgres-bootstrap читает POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB и выполняет CREATE ROLE + CREATE DATABASE
init scriptsЕсли есть /docker-entrypoint-initdb.d/*.sql или *.sh — выполняются в алфавитном порядке. Только при initdb!
exec postgresEntrypoint делает exec postgres -c config_file=... — теперь PID 1 = сам postgres-сервер, готовый принимать соединения

Альтернатива volume — bind mount: -v /Users/me/pgdata:/var/lib/postgresql/data. Это работает, но на macOS и Windows bind mount медленнее volume (синхронизация через виртуальную ФС). Для локальной dev-базы это не критично, но я обычно использую volume — портативнее, не путает Docker Desktop.


Запуск + проверка

Запустим Postgres и подключимся:

docker run -d \
  --name pg \
  -e POSTGRES_USER=de \
  -e POSTGRES_PASSWORD=secret \
  -e POSTGRES_DB=warehouse \
  -p 5432:5432 \
  -v pg-data:/var/lib/postgresql/data \
  postgres:16

docker logs pg --tail 5

Видим в логах что-то вроде:

2026-05-15 09:12:33.456 UTC [1] LOG:  database system is ready to accept connections

Подключимся к базе через psql внутри самого контейнера:

docker exec -it pg psql -U de -d warehouse

-it-i keep STDIN open + -t allocate TTY (нужно для интерактивной psql-сессии). Внутри psql:

warehouse=# SELECT version();
                                                  version
-----------------------------------------------------------------------------------------------------------
 PostgreSQL 16.3 (Debian 16.3-1.pgdg120+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14) ...

warehouse=# \dt
Did not find any relations.

warehouse=# \q

Подключение работает. Чтобы убедиться, что данные переживают рестарт, создадим тестовую таблицу, перезапустим контейнер и проверим, что таблица на месте.

docker exec -it pg psql -U de -d warehouse -c "CREATE TABLE t (id INT); INSERT INTO t VALUES (1), (2), (3);"

docker restart pg
sleep 2

docker exec -it pg psql -U de -d warehouse -c "SELECT * FROM t;"

Выведет три строки. Данные пережили рестарт, потому что лежат в volume pg-data. Если бы мы запустили без -v, после docker rm pg && docker run ... таблица бы исчезла.


Подключение с хоста

Из терминала на хосте (без docker exec) — нужен psql локально (например, brew install libpq на macOS) или любой GUI типа DBeaver, TablePlus, pgAdmin:

psql postgresql://de:secret@localhost:5432/warehouse -c "SELECT 1;"

Или из Python:

import psycopg

with psycopg.connect("postgresql://de:secret@localhost:5432/warehouse") as conn:
    with conn.cursor() as cur:
        cur.execute("SELECT 1")
        print(cur.fetchone())

Работает, потому что мы пробросили -p 5432:5432. Без -p снаружи контейнера до базы добраться нельзя (только через docker exec или другие контейнеры в той же docker-сети, об этом было в модуле 10).


Почему 5432 не торчит наружу в production compose

Как TCP-соединения открываются к порту 5432

Это критически важный момент, который Junior DE часто не понимает. На локальной машине -p 5432:5432 удобно: подключился из IDE, посмотрел данные, всё. Но в production-стенде (compose, который крутится на сервере) делать ports: ["5432:5432"] для Postgres — это открыть базу в интернет.

Логика такая. Когда ты пишешь:

services:
  postgres:
    image: postgres:16
    ports:
      - "5432:5432"

Docker создаёт правило в iptables, которое пробрасывает порт хоста 5432 в контейнер. Если хост — это VPS с публичным IP, то теперь любой человек в интернете может подключиться к твоему your-server.com:5432. Боты в интернете постоянно сканируют известные порты — Postgres-сканеров на ipv4 буквально миллионы запросов в сутки. Если пароль слабый (secret, postgres, admin) — база скомпрометирована за минуты.

Правильный production-вариант — не пробрасывать порт вообще:

services:
  postgres:
    image: postgres:16
    # ports: НЕТ
    environment:
      POSTGRES_USER: de
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: warehouse
    volumes:
      - pg-data:/var/lib/postgresql/data

  app:
    image: my-etl:latest
    depends_on:
      - postgres
    environment:
      DATABASE_URL: postgresql://de:secret@postgres:5432/warehouse

Сервис app подключается к Postgres по DNS-имени postgres внутри Docker-сети — это работает, потому что оба контейнера в одной user-defined bridge сети (compose автоматически создаёт сеть <project>_default). Снаружи (с публичного IP сервера) Postgres недоступен — порт 5432 не открыт на хосте.

WARNING

Если очень надо подключиться к проду из IDE для дебага — делай это через SSH-туннель: ssh -L 5432:localhost:5432 user@server. Это пробросит локальный 5432 на сервер, а уже на сервере — на контейнер. Никакого открытого порта в интернете.

В локальной разработке ports: ["5432:5432"] — норма. В compose для прода — anti-pattern. Простое правило: если порт нужен только другим контейнерам в стеке — не открывай его наружу.


Параметры postgres через env и command

Помимо POSTGRES_USER/PASSWORD/DB, образ принимает ещё несколько переменных. Полный список — в официальной документации, но самые часто используемые:

  • POSTGRES_INITDB_ARGS — флаги для initdb. Полезно для русской локали: POSTGRES_INITDB_ARGS=--locale=ru_RU.UTF-8 --encoding=UTF8.
  • PGDATA — путь к data dir внутри контейнера. По умолчанию /var/lib/postgresql/data. Меняют редко, но иногда удобно перенести в /var/lib/postgresql/data/pgdata (sub-directory volume), чтобы избежать конфликта с lost+found на некоторых ФС.
  • POSTGRES_HOST_AUTH_METHOD — метод аутентификации. По умолчанию scram-sha-256 (Postgres 14+). Менять не надо.

Параметры самого Postgres (max_connections, shared_buffers, work_mem) передаются через command:

docker run -d \
  --name pg \
  -e POSTGRES_PASSWORD=secret \
  -p 5432:5432 \
  postgres:16 \
  -c shared_buffers=256MB -c max_connections=100

Внутри контейнера entrypoint выполнит postgres -c shared_buffers=256MB -c max_connections=100. Это удобно для quick-experiments, но в проде лучше монтировать postgresql.conf через bind mount или собирать свой образ с конфигом.


Попробуй сам

  1. Запусти Postgres 16 с volume:
    docker run -d --name pg \
      -e POSTGRES_USER=lab \
      -e POSTGRES_PASSWORD=lab \
      -e POSTGRES_DB=lab \
      -p 5432:5432 \
      -v lab-pg-data:/var/lib/postgresql/data \
      postgres:16
  2. Подключись через docker exec -it pg psql -U lab -d lab, создай таблицу cities (id INT, name TEXT), вставь 3 строки.
  3. Сделай docker stop pg && docker rm pg, потом запусти контейнер с тем же volume. Проверь, что таблица на месте.
  4. Запусти ещё один контейнер с тем же -v lab-pg-data:..., но без -p 5432:5432. Проверь, что снаружи не подключиться, а через docker exec — можно.
  5. Посмотри, где volume на хосте: docker volume inspect lab-pg-data. На Linux это путь типа /var/lib/docker/volumes/lab-pg-data/_data. На macOS volume живёт внутри VM Docker Desktop / OrbStack, на хосте напрямую недоступен.

Проверка знанийKnowledge check
Почему в production compose-файле порт 5432 для Postgres обычно НЕ пробрасывают наружу, даже если в локальной разработке мы делаем ports: ["5432:5432"]?
ОтветAnswer
Потому что ports: ["5432:5432"] в production = открытая в интернет база данных. Docker создаёт iptables-правило, которое делает порт 5432 хоста доступным с публичного IP. Боты в интернете непрерывно сканируют этот порт, и при слабом пароле базу взламывают за минуты. В production правильно: НЕ пробрасывать порт вообще. Контейнер app подключается к postgres по DNS-имени postgres внутри docker-сети (compose создаёт её автоматически), а снаружи база недоступна. Если нужен доступ из IDE для дебага — через SSH-туннель: ssh -L 5432:localhost:5432 user@server. Локально ports: ["5432:5432"] норма, потому что хост — твоя машина, не интернет. Простое правило: если порт нужен только другим контейнерам стека — не открывай его наружу.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Какая переменная окружения ОБЯЗАТЕЛЬНА для первого запуска контейнера postgres, иначе контейнер падает с ошибкой?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5