Learning Platform
Глоссарий Troubleshooting
Урок 10.04 · 26 мин
Средний
dockerpostgresvolumesinitdbpg_upgrade

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. Его работа:

  1. Проверить PGDATA (по умолчанию /var/lib/postgresql/data).
  2. Если PGDATA пустой — запустить initdb, создать кластер, выставить пароль для postgres пользователя, выполнить файлы из /docker-entrypoint-initdb.d/*.sql.
  3. Если PGDATA не пустой — пропустить init, просто запустить postgres (он сам прочитает существующую базу).
Decision flow postgres-entrypoint при старте
docker run -v pgdata:/dataVolume может быть пустым (только создали) или с существующими данными (рестарт)
ls /var/lib/postgresql/dataПроверка наличия PG_VERSION файла. Если есть — данные есть. Если нет — пустой volume
Empty: initdbСоздание нового кластера. Применяется POSTGRES_PASSWORD, POSTGRES_USER, POSTGRES_DB. Запускаются /docker-entrypoint-initdb.d/*.sql и *.sh
Existing: skip initVolume не пустой — Postgres подцепляет существующую базу. POSTGRES_PASSWORD не применяется, /docker-entrypoint-initdb.d не выполняется
exec postgresГлавный процесс контейнера — postgres daemon. Listens on 0.0.0.0:5432 (внутри контейнера), доступен снаружи через -p mapping
Ready: accept connectionspg_isready возвращает 0. Можно подключаться через psql или client-app
WARNING

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
TIP

В 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
NOTE

В compose volume префиксуются именем проекта. Если ты запускаешь compose в папке etl/, volume pgdata физически называется etl_pgdata. Если переименовал папку — compose потеряет ссылку на старый volume. Для стабильности можно явно задать имя: volumes: pgdata: { name: etl-pgdata }.

В следующем уроке — права доступа, UID-mismatch и типичные ошибки Postgres вроде data directory has wrong ownership.


Проверка знанийKnowledge check
Ты запускаешь postgres:17 с новым named volume и переменной POSTGRES_PASSWORD=secret . База создалась. Через неделю ты меняешь POSTGRES_PASSWORD=newsecret в docker run и перезапускаешь контейнер. psql с новым паролем не подключается — пускает только старый secret . Почему так, и как реально сменить пароль?
ОтветAnswer
POSTGRES_PASSWORD применяется только при первом запуске на пустом volume — именно во время initdb внутри entrypoint-скрипта. На втором запуске entrypoint видит, что PGDATA не пустой (PG_VERSION файл существует), и пропускает initdb целиком — поэтому новый POSTGRES_PASSWORD просто игнорируется. Чтобы реально сменить пароль, нужно сделать это через SQL: docker exec -it pg psql -U postgres -c "ALTER USER postgres PASSWORD 'newsecret';" — это меняет хешированный пароль в pg_authid внутри базы. После этого можно (и нужно) синхронно обновить compose/run, чтобы для будущих fresh-volume значение совпадало.

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Ты задал POSTGRES_PASSWORD=newsecret и перезапустил postgres-контейнер с тем же volume. Старый пароль продолжает работать, новый — нет. Почему?

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

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

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

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