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 стартует впервые (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
Если порт 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.
Альтернатива 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 не открыт на хосте.
Если очень надо подключиться к проду из 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 или собирать свой образ с конфигом.
Попробуй сам
- Запусти 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 - Подключись через
docker exec -it pg psql -U lab -d lab, создай таблицуcities (id INT, name TEXT), вставь 3 строки. - Сделай
docker stop pg && docker rm pg, потом запусти контейнер с тем же volume. Проверь, что таблица на месте. - Запусти ещё один контейнер с тем же
-v lab-pg-data:..., но без-p 5432:5432. Проверь, что снаружи не подключиться, а черезdocker exec— можно. - Посмотри, где volume на хосте:
docker volume inspect lab-pg-data. На Linux это путь типа/var/lib/docker/volumes/lab-pg-data/_data. На macOS volume живёт внутри VM Docker Desktop / OrbStack, на хосте напрямую недоступен.