Postgres + volume: первый запуск, initdb, миграция версий
В этом уроке собираем самый частый production-сценарий: Postgres-контейнер с named volume под data directory. Разберём, что делает Postgres при первом запуске, что произойдёт при подмене volume, какие environment variables работают, и как мигрировать данные с PG16 на PG17 через pg_upgrade внутри контейнера.
Mount, /etc/fstab и mount namespaces
Минимальный запуск
docker volume create pgdata
docker run -d --name pg17 \
-e POSTGRES_PASSWORD=secret \
-v pgdata:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:17
Что мы попросили:
- Volume
pgdataсмонтирован в/var/lib/postgresql/data— это PGDATA для официального образа. POSTGRES_PASSWORD=secretобязателен: без него образ упадёт сyou must specify POSTGRES_PASSWORD(или указатьPOSTGRES_HOST_AUTH_METHOD=trust, что плохо).-p 5432:5432пробрасывает порт на хост.
Через несколько секунд Postgres готов принимать подключения:
docker exec -it pg17 psql -U postgres -c '\l'
# Список баз: postgres, template0, template1
Что произошло при первом запуске
В Postgres-образе есть entrypoint-скрипт docker-entrypoint.sh. Его работа:
- Проверить
PGDATA(по умолчанию/var/lib/postgresql/data). - Если PGDATA пустой — запустить
initdb, создать кластер, выставить пароль дляpostgresпользователя, выполнить файлы из/docker-entrypoint-initdb.d/*.sql. - Если PGDATA не пустой — пропустить init, просто запустить
postgres(он сам прочитает существующую базу).
POSTGRES_PASSWORD применяется ТОЛЬКО при первом запуске на пустом volume. Если ты запустил контейнер раз, потом сменил POSTGRES_PASSWORD=newsecret и сделал рестарт — пароль не поменяется. Volume уже инициализирован, entrypoint пропустит шаг с паролем. Чтобы реально сменить пароль, нужно ALTER USER postgres PASSWORD 'newsecret' через psql.
Init-скрипты
Файлы из /docker-entrypoint-initdb.d/*.sql и *.sh выполняются при первом запуске, после initdb, до открытия порта 5432 наружу. Стандартный способ заложить начальную схему:
mkdir -p ./init
cat > ./init/01-schema.sql <<'SQL'
CREATE DATABASE etl;
\c etl
CREATE TABLE events (
id BIGSERIAL PRIMARY KEY,
event_type TEXT NOT NULL,
payload JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX events_created_at_idx ON events(created_at);
SQL
docker run -d --name pg-init \
-e POSTGRES_PASSWORD=secret \
-v $(pwd)/init:/docker-entrypoint-initdb.d:ro \
-v pgdata-fresh:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:17
Через 2-3 секунды:
docker exec pg-init psql -U postgres -d etl -c '\dt'
# Schema | Name | Type | Owner
# public | events | table | postgres
Файлы выполняются в алфавитном порядке. Префикс 01-, 02- — стандартный приём упорядочивания.
Что произойдёт при подмене volume
Сценарий: ты остановил Postgres-контейнер, забыл, что у тебя были данные, создал новый контейнер с другим volume:
docker run -d --name pg-old -e POSTGRES_PASSWORD=x -v pgdata-A:/var/lib/postgresql/data postgres:17
# поработал, написал данные
docker rm -f pg-old
# Случайно или специально — другой volume.
docker run -d --name pg-new -e POSTGRES_PASSWORD=x -v pgdata-B:/var/lib/postgresql/data postgres:17
pg-new с volume pgdata-B стартует с пустого state: initdb, новый кластер, нет таблиц. pgdata-A лежит нетронутый — docker volume ls его покажет. Это не «потеря данных», но похоже, потому что приложение видит пустую базу.
Лечение: переключиться обратно на нужный volume:
docker rm -f pg-new
docker run -d --name pg -e POSTGRES_PASSWORD=x -v pgdata-A:/var/lib/postgresql/data postgres:17
В compose эта ошибка случается редко, потому что имя volume фиксировано в YAML. В ручных docker run — частый источник «у меня данные пропали». Всегда смотри docker volume ls после рестарта.
Миграция Postgres 16 -> 17
Postgres хранит данные в формате, специфичном для major-версии. PGDATA, созданный postgres:16, нельзя просто запустить через postgres:17 — версия 17 при старте откажется работать с чужим data dir:
FATAL: database files are incompatible with server
DETAIL: The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17.x.
Способов миграции три, по убывающей удобства:
Способ 1: pg_dump / pg_restore (универсальный)
Самый простой и универсальный.
# Снять дамп с PG16
docker exec pg16 pg_dumpall -U postgres > all.sql
# Поднять PG17 на новом volume
docker run -d --name pg17 -e POSTGRES_PASSWORD=secret \
-v pgdata-17:/var/lib/postgresql/data postgres:17
# Залить дамп
cat all.sql | docker exec -i pg17 psql -U postgres
Минус: на больших базах долго. dump + restore для базы в 100 ГБ — это часы.
Способ 2: pg_upgrade в специальном image
Есть community-образ tianon/postgres-upgrade, который содержит и PG16, и PG17 binaries:
docker run --rm \
-v pgdata-16:/var/lib/postgresql/16/data \
-v pgdata-17:/var/lib/postgresql/17/data \
tianon/postgres-upgrade:16-to-17
pg_upgrade использует hard-link mode (--link) для скорости — не копирует файлы, а делает links. Бэкап оригинала обязателен до запуска.
Способ 3: Custom pg_upgrade-контейнер
Если community-образа нет под нужную пару версий — собрать свой. Внутри устанавливаем оба сервера, запускаем pg_upgrade. Это уже задача DBA, не Junior DE.
Полный мини-стенд
Готовый compose для local-стенда с init-скриптом, healthcheck’ом и volume:
services:
postgres:
image: postgres:17
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: etl
volumes:
- pgdata:/var/lib/postgresql/data
- ./init:/docker-entrypoint-initdb.d:ro
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d etl"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
volumes:
pgdata:
docker compose up -d
# Жди healthcheck:
docker compose ps
# postgres ... healthy
Этот compose ты будешь модифицировать через весь курс: добавим в него Airflow, MinIO, Redis, наконец Kafka.
Попробуй сам
# 1. Создай Postgres с init-скриптом, проверь, что таблицы создались.
mkdir -p init
cat > init/01-schema.sql <<'SQL'
CREATE TABLE products (id SERIAL, name TEXT);
INSERT INTO products(name) VALUES ('apple'), ('banana');
SQL
docker run -d --name pg-demo \
-e POSTGRES_PASSWORD=secret \
-v pg-demo-data:/var/lib/postgresql/data \
-v $(pwd)/init:/docker-entrypoint-initdb.d:ro \
postgres:17
# Подожди пару секунд
sleep 5
docker exec pg-demo psql -U postgres -c 'SELECT * FROM products;'
# id | name
# 1 | apple
# 2 | banana
# 2. Убедись, что данные переживают рестарт.
docker rm -f pg-demo
docker run -d --name pg-demo \
-e POSTGRES_PASSWORD=secret \
-v pg-demo-data:/var/lib/postgresql/data \
postgres:17
sleep 5
docker exec pg-demo psql -U postgres -c 'SELECT count(*) FROM products;'
# 2
# 3. Симулируй "случайный новый volume".
docker rm -f pg-demo
docker run -d --name pg-demo-wrong \
-e POSTGRES_PASSWORD=secret \
-v pg-demo-NEW:/var/lib/postgresql/data \
postgres:17
sleep 5
docker exec pg-demo-wrong psql -U postgres -c 'SELECT * FROM products;'
# ERROR: relation "products" does not exist — потому что новый volume пустой.
# Cleanup.
docker rm -f pg-demo-wrong
docker volume rm pg-demo-data pg-demo-NEW
rm -rf init
В compose volume префиксуются именем проекта. Если ты запускаешь compose в папке etl/, volume pgdata физически называется etl_pgdata. Если переименовал папку — compose потеряет ссылку на старый volume. Для стабильности можно явно задать имя: volumes: pgdata: { name: etl-pgdata }.
В следующем уроке — права доступа, UID-mismatch и типичные ошибки Postgres вроде data directory has wrong ownership.