PostgreSQL 18 в Docker: PGDATA и монтирование тома
АйТи Фреш
1С и базы данных

PGDATA и монтирование в Docker для PostgreSQL 18: как собрать compose, чтобы база пережила пересоздание контейнера

Автор: , директор ООО «АйТи-Фреш» · · ~20 мин чтения
Подключение тома к правильному каталогу данных PostgreSQL 18 в Docker вместо лишнего отсоединённого тома
Данные не пропали: том просто смонтирован не туда, куда пишет база.

В образе postgres:18 данные лежат в /var/lib/postgresql/18/docker, а том нужно монтировать на /var/lib/postgresql, а не на /var/lib/postgresql/data. PGDATA менять не обязательно: достаточно одного тома на правильный путь. Ниже рабочий compose, проверка до боевого запуска и правила, если PGDATA всё же приходится переопределять.

Что именно изменилось в образе postgres:18 и почему база оказывается пустой

Я разворачиваю PostgreSQL в Docker для 1С и учётных систем уже много лет, и до осени 2025 года у меня в голове сидело одно правило: том монтируем на /var/lib/postgresql/data, и всё. Образ postgres:18 это правило сломал. Это не баг и не «внезапная несовместимость PostgreSQL», а осознанное решение мейнтейнеров образа. Сам сервер PostgreSQL 18 вышел 25 сентября 2025 года и к раскладке каталогов в Docker отношения не имеет: поменялся именно Docker Official Image.

Что сказано в описании образа на Docker Hub. Переменная PGDATA начиная с 18-й версии привязана к мажорной версии: для 18 это /var/lib/postgresql/18/docker, для будущих релизов вместо 18 подставится их номер. Объявленный в образе VOLUME теперь указывает на /var/lib/postgresql, и в документации прямо написано, что тома и монтирования нужно направлять на обновлённое место. Я проверил это и по самому Dockerfile для ветки 18: там стоят строки ENV PGDATA /var/lib/postgresql/18/docker и VOLUME /var/lib/postgresql. Один и тот же вывод в двух независимых местах, значит это не слух.

Теперь о механике поломки. Если вы по привычке оставили в compose строку pgdata:/var/lib/postgresql/data, то ваш именованный том смонтирован мимо реального каталога данных. А поскольку в образе объявлен VOLUME на /var/lib/postgresql, и на этот путь ничего явно не смонтировано, Docker создаёт анонимный том. Документация Docker подтверждает: анонимные тома получают случайное имя и переживают удаление контейнера, но не подцепляются новым контейнером автоматически. Сервис стартует, healthcheck зелёный, приложение работает, а после первого down и up база пустая. Меня до сих пор веселит формулировка «ничего не менялось, только тег образа».

Важная оговорка про тему «это баг или нет». В issue №1400 репозитория docker-library/postgres разобран конкретный случай: образ 18.1-alpine, том смонтирован на /var/lib/postgresql/data, а кластер с PGDATA по умолчанию оказывается вне того каталога, который пользователь считал своим томом, и после пересоздания контейнера данные пропадают без какой-либо ошибки или предупреждения. Issue закрыт 21 апреля 2026 года через PR №1409: проверка «нет ли старого неиспользуемого монтирования» молча не срабатывала на Alpine-образах, потому что mountpoint из BusyBox сравнивает только device и inode и не видит bind-монтирований. Это не повод объявлять, что postgres:18 сегодня теряет данные у всех, но это отличная иллюстрация, почему на встроенную защиту образа полагаться нельзя. Если вам нужен разбор именно «уже пустая, что делать», он у меня отдельно: база пустая после обновления до postgres:18. Здесь же разговор про то, как собрать конфигурацию так, чтобы до этого не дошло.

Какое монтирование выбрать: именованный том, bind mount или свой PGDATA

Вариантов три, и у каждого своя цена. Первый и мой выбор по умолчанию: один именованный том на /var/lib/postgresql, PGDATA не трогаем. Образ сам создаст внутри тома каталог 18/docker, при следующем мажорном обновлении рядом встанет 19/docker, и оба кластера окажутся на одной файловой системе. Это важно, потому что pg_upgrade в режиме --link требует, чтобы каталоги данных старого и нового кластеров лежали в одной файловой системе (табличные пространства и pg_wal могут быть отдельно). Именно ради этого мейнтейнеры и перенесли точку монтирования на уровень выше.

Второй вариант: bind mount на каталог хоста, например /srv/pg18. Он хорош там, где бэкап у вас построен на снимках файловой системы или нужно видеть данные с хоста без лишних команд. Документация Docker честно описывает разницу: тома полностью управляются Docker и проще в бэкапе и переносе, bind mount зависит от структуры каталогов и ОС хоста, зато удобен, когда нужен доступ к файлам и с хоста, и из контейнера. Подвох один и он всегда один и тот же: права. Пользователь postgres в Debian-образе имеет uid 999, в Alpine-образе uid 70 (оба значения видны в Dockerfile). Каталог на хосте с неверным владельцем даёт «permission denied» на старте, и это ещё хороший исход.

Третий вариант: переопределить PGDATA своим значением. Он нужен нечасто: например, когда на одном томе живут несколько кластеров или когда политика хоста требует определённой структуры. Правило железное: новый PGDATA обязан лежать внутри того пути, на который смонтирован том. Иначе вы получите ту же самую анонимную пустоту, только теперь по собственной инициативе. И ещё: если вы задаёте PGDATA сами, вы уходите от версионной раскладки по умолчанию, а значит, перекладывать каталоги под pg_upgrade --link придётся вручную. Вот сводка, как я это раскладываю. | Вариант | Монтирование | PGDATA | Когда подходит | Риск | |---|---|---|---|---| | Именованный том | pgdata:/var/lib/postgresql | по умолчанию, /var/lib/postgresql/18/docker | новые установки, большинство офисов | почти нулевой, если путь верный | | Bind mount | /srv/pg18:/var/lib/postgresql | по умолчанию | бэкап снимками ФС, доступ с хоста | неверный владелец каталога | | Свой PGDATA | том на /var/lib/postgresql | внутри точки монтирования | особая структура, несколько кластеров | ручная работа при апгрейде | | Старый путь | pgdata:/var/lib/postgresql/data | по умолчанию | не подходит для 18 | анонимный том и пустая база |

Отдельно скажу про распространённый совет «просто задайте PGDATA=/var/lib/postgresql/data и не мучайтесь». Технически база заработает и данные в именованном томе сохранятся. Но вы сознательно отказываетесь от раскладки, под которую образ спроектирован, и от возможности выполнить pg_upgrade --link внутри одного тома. Для маленькой базы под скромный сервис это допустимый костыль, для всего, что вы собираетесь обновлять годами, я бы так не делал. Костыль имеет свойство пережить того, кто его поставил.

Не копируйте compose из статей и репозиториев, написанных до осени 2025 года, без проверки пути монтирования. Строка с /var/lib/postgresql/data в них была правильной, а для postgres:18 уже нет.
Сравнение неверного и верного монтирования тома postgres:18: старый путь /data против /var/lib/postgresql
Для образа 18 том монтируется на /var/lib/postgresql, старый путь ведёт к анонимному тому.

Рабочий compose для postgres:18: что писать и чего не писать

Вот минимальная конфигурация, которую я кладу на старте. Тег зафиксирован до патч-версии: на 18.10.2026 текущая минорная версия PostgreSQL 18 по сайту postgresql.org равна 18.6, и в Dockerfile для ветки trixie указана версия 18.6. Тег latest или голый postgres:18 в боевом compose я не использую, потому что обновление образа в такой схеме происходит в момент пересоздания контейнера, а не тогда, когда вы к нему готовы. Пароль берём из .env, который лежит рядом и не попадает в репозиторий.

services:
  db:
    image: postgres:18.6-trixie
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: ${PG_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  pgdata:

Обратите внимание на то, чего в этом файле нет: переменной PGDATA и каких-либо строк с /data. Образ сам знает, где лежат данные, и это тот случай, когда меньше конфигурации означает меньше способов ошибиться. Переменная POSTGRES_PASSWORD по описанию образа обязательна и не должна быть пустой. Дальше, о поведении при повторных запусках: Docker-специфичные переменные и скрипты из /docker-entrypoint-initdb.d срабатывают только при пустом каталоге данных, а существующая база при старте остаётся нетронутой. Поэтому менять POSTGRES_PASSWORD в compose «потом» бесполезно: пароль уже записан в кластер. Это один из самых частых вопросов «почему не меняется пароль».

Если вы выбираете bind mount, правка минимальная: вместо именованного тома указываем каталог хоста и заранее создаём его с нужным владельцем. Для Debian-образа это uid 999, для Alpine uid 70.

sudo install -d -o 999 -g 999 -m 0700 /srv/pg18

В compose тогда будет строка /srv/pg18:/var/lib/postgresql. И здесь есть скрытая ловушка в самом Compose: в коротком синтаксисе bind mount несуществующий каталог хоста создаётся автоматически, причём от имени root. Документация Compose называет это поведением ради обратной совместимости и предлагает длинный синтаксис с type: bind и create_host_path: false, при котором каталог не создаётся, и контейнер просто не запустится, если пути нет. Для базы данных это именно то поведение, которое нужно: лучше громкая ошибка, чем тихо созданный пустой каталог.

Про Alpine скажу отдельно. Раньше в Alpine-сборках встроенная проверка неиспользуемого монтирования не срабатывала из-за BusyBox, и это исправлено, как я писал выше, после закрытия issue №1400. Я всё равно беру Debian-вариант (trixie) для баз, с которыми работают люди, и оставляю Alpine для вспомогательных сервисов, где важен размер образа. Экономия места на диске за счёт более компактного Alpine-образа для базы не стоит лишних сюрпризов с поведением утилит BusyBox. Если у вас в стеке уже есть похожий compose с другим сервисом, посмотрите, как у меня описаны правила compose для продакшена: там те же принципы про фиксацию тегов и тома.

Как проверить монтирование до того, как в базе появятся данные

Это самый «больной» раздел всей истории, потому что проверку почти никто не делает. Контейнер с PostgreSQL выглядит здоровым, даже когда пишет в мусорный том. Проверять нужно не «поднялся ли сервис», а «куда он пишет и переживёт ли это пересоздание». Я делаю четыре шага, и они занимают минут десять. Сначала смотрим, как Compose собрал итоговую конфигурацию, и убеждаемся, что в ней нет старых путей.

docker compose config

Затем, после первого запуска, выводим все монтирования контейнера.

docker inspect $(docker compose ps -q db) --format '{{range .Mounts}}{{.Type}} {{.Name}} {{.Destination}}{{println}}{{end}}'

Нормальная картина: ровно одна строка, destination равен /var/lib/postgresql. Если строк две, и у одной из них имя выглядит как длинный хеш, значит у вас лишний анонимный том, и вы нашли проблему до того, как она стоила данных.

Второй шаг: убедиться, что PGDATA лежит внутри смонтированного пути, и что в нём действительно есть кластер. Образ ищет файл PG_VERSION, чтобы понять, что база уже существует (это видно в docker-entrypoint.sh), и я проверяю то же самое руками.

docker compose exec db sh -c 'echo $PGDATA; cat $PGDATA/PG_VERSION'
docker compose exec db psql -U postgres -c 'SHOW data_directory;'

Ожидаем /var/lib/postgresql/18/docker и номер 18. Если PGDATA указывает на что-то ещё, а смонтированный путь другой, дальше можно не идти.

Третий шаг, самый важный и самый пропускаемый: тест на пересоздание. Создаём контрольную таблицу, сносим контейнер без ключа -v, поднимаем снова и смотрим, что таблица на месте.

docker compose exec db psql -U postgres -c 'CREATE TABLE persist_check(id int); INSERT INTO persist_check VALUES (1);'
docker compose down
docker compose up -d
docker compose exec db psql -U postgres -c 'SELECT * FROM persist_check;'

Ключ -v здесь принципиален. По документации docker compose down с -v удаляет именованные тома из секции volumes и анонимные тома контейнеров, а без него анонимные тома сохраняются. Тест без -v показывает, пережили ли данные пересоздание, а тест с -v в боевой среде показывает только, как быстро вы можете всё потерять. Таблицу потом удаляем командой DROP TABLE, мусор в боевой базе нам не нужен.

Четвёртый шаг касается осиротевших томов. Команда docker volume prune по умолчанию удаляет только анонимные тома, а чтобы она затронула и именованные, нужен ключ --all (он помечен в документации как API 1.42+). Звучит безопасно, но представьте: ваша база по ошибке писала в анонимный том, а «приберёмся на сервере» запускает prune. Данные, которые приложение считало основными, исчезают как раз тем способом, который описан как безопасный. Поэтому на хостах с базами я не оставляю регулярные задания очистки томов и смотрю глазами на список.

docker volume ls -f dangling=true

Если вам нужно, чтобы кто-то прошёл по вашим контейнерам с базами и проверил именно это, мы это делаем за пару часов на сервер: инвентаризация томов, тест пересоздания и отчёт, что где лежит.

Чек-лист проверки монтирования тома postgres:18 в Docker перед запуском в работу: четыре шага
Десять минут на тест пересоздания дешевле, чем восстановление базы в понедельник.

Если PGDATA всё-таки нужно переопределить: безопасный порядок

Бывают причины, по которым дефолтная раскладка не подходит: единый стандарт каталогов на всех серверах, второй кластер на том же томе, жёсткое требование аудита. Тогда PGDATA задаётся переменной окружения, а том монтируется на родительский каталог. Например, том на /var/lib/postgresql и PGDATA=/var/lib/postgresql/school/data. Главное условие я уже называл: PGDATA внутри монтирования. Второе условие: каталог должен быть пуст или уже содержать кластер этой же мажорной версии. Образ при пустом каталоге выполнит initdb, при существующем PG_VERSION выведет, что каталог содержит базу, и пропустит инициализацию.

Что делать, если данные уже есть в старой раскладке (например, версия 17 в /var/lib/postgresql/data), а вы хотите перейти на новую схему? В описании образа предложен путь: для версий ниже 18 можно заранее включить новую раскладку, указав PGDATA=/var/lib/postgresql/17/docker и смонтировав том на /var/lib/postgresql, предварительно переложив все файлы базы в подкаталог PG_MAJOR/docker. Перекладывать нужно при остановленном контейнере, с сохранением владельца и прав, а перед этим обязательно снять логический дамп. Я не доверяю ни одной перекладке файлов, у которой нет свежего дампа и проверенного восстановления рядом.

Теперь про апгрейд версии. pg_upgrade с ключом --link использует жёсткие ссылки вместо копирования файлов, работает намного быстрее и занимает меньше места, но документация PostgreSQL предупреждает: после запуска нового кластера в режиме link старый кластер использовать нельзя. Если новый кластер уже стартовал, он записал данные в общие файлы, и старый придётся восстанавливать из резервной копии. Поэтому «откат» при --link это не откат, а восстановление из бэкапа. Для баз размером в пару гигабайт я чаще делаю обычный pg_dumpall и восстановление в чистый кластер 18: медленнее на минуты, зато старый том лежит нетронутым и откат тривиален.

И ещё один подводный камень апгрейда с 17-й. В примечаниях к релизу PostgreSQL 18 сказано, что initdb теперь по умолчанию включает контрольные суммы данных, а pg_upgrade требует совпадения настроек контрольных сумм у старого и нового кластера. Для апгрейда кластера, созданного без сумм, предусмотрена опция initdb --no-data-checksums. В Docker аргументы initdb передаются через переменную POSTGRES_INITDB_ARGS, о чём сказано в описании образа. Если вы идёте через дамп, этот вопрос вас не касается, а контрольные суммы на новой базе можно считать приятным бонусом.

Итоги переноса PostgreSQL 18 в Docker для школы на 50 рабочих мест: размер базы, дамп и окно работ
Перенос через дамп и предварительный тест пересоздания прошёл за вечернее окно без потерь.

Кейс: музыкальная школа «Гармония звука», 50 рабочих мест

Школа переезжала со старого сервера на новый. Рабочих мест 50: преподаватели, администраторы, бухгалтерия, несколько классов с компьютерами. Из контейнеров на сервере были система расписания и учёта абонементов и родительский портал, обе используют PostgreSQL. Хост на Ubuntu 24.04, Docker с Compose v2. Прежний подрядчик собирал стек «по старинке», с монтированием на /var/lib/postgresql/data, и когда мы вместе планировали переход на образ 18, я сразу предупредил, что копировать старый compose нельзя.

Мы сделали так. На новом сервере собрали compose под postgres:18.6-trixie с одним именованным томом на /var/lib/postgresql, без PGDATA, пароль вынесли в .env. Данные со старого сервера перенесли логическим дампом pg_dumpall: база заняла около 2,4 ГБ, дамп в сжатом виде получился примерно 380 МБ. Окно обслуживания согласовали на вечер после занятий, и всё уложилось в три часа вместе с проверкой. Перед загрузкой дампа мы прошли все четыре шага из предыдущего раздела: docker compose config, docker inspect по монтированиям, проверка PG_VERSION и тест пересоздания контейнера с контрольной таблицей.

После тестового down и up контрольная таблица осталась на месте, а список монтирований показал ровно один том. Дальше мы загрузили дамп, проверили выборочно данные вместе с администратором школы (абонементы, расписание на ближайшую неделю) и только после этого переключили рабочие места. Ночной дамп настроили на отдельный диск, а раз в месяц проверяем восстановление в чистый контейнер. Старый том со старого сервера мы оставили на хранении на две недели и только потом удалили. Итого: один рабочий день на подготовку и вечерняя миграция, потерь данных ноль, ну а «магии» никакой, только дисциплина.

Бэкап, обновления и что можно не делать

Корректное монтирование защищает вас от одного класса проблем, но не заменяет резервную копию. Бэкап делают все, проверяет его почти никто, а том на том же диске, что и база, бэкапом считаться не может. Для небольших баз я использую ночной pg_dumpall на отдельное хранилище.

docker compose exec -T db pg_dumpall -U postgres | gzip > /srv/backup/pg_$(date +%F).sql.gz

Файл проверяю на ненулевой размер и раз в месяц восстанавливаю в отдельный контейнер. Когда база вырастает и дамп перестаёт укладываться в окно, переходим на физические бэкапы с архивом WAL, и тут я отсылаю к своему сравнению pgBackRest и Barman.

Порядок действий по приоритету такой. Первое, сегодня: найдите в своих compose все строки с /var/lib/postgresql/data у сервисов на образе 18 и всех контейнеров с PostgreSQL, у которых больше одного тома. Второе, на этой неделе: зафиксируйте теги до патч-версии и отключите автоматическую очистку томов на хостах с базами. Третье, в плановое окно: проверьте восстановление дампа. А мажорные обновления делайте отдельным шагом, не смешивая с перекладкой каталогов и сменой тега.

На что можно не тратить время. Одноразовые базы в CI и тестовых стендах, где данные создаются заново скриптом, защищать от пересоздания бессмысленно, там пустота это норма. Управляемые базы у облачных провайдеров тоже не про эту историю: каталог данных там не ваша забота. А вот всё, что хранит расписание, абонементы, заказы и учёт, должно проходить описанный в статье тест на пересоздание. Он честно занимает десять минут, и это самый дешёвый способ не узнавать о проблеме в понедельник утром. Похожая механика есть и в других стеках: например, в netbox-docker такая же «пустая база» возникает из-за смены тома, я разбирал её в статье про netbox-docker и PostgreSQL 18, но там причина в самом релизе проекта, а здесь в пути монтирования.

Частые вопросы

Какой PGDATA по умолчанию в postgres:18 и нужно ли его менять?

По умолчанию PGDATA равен /var/lib/postgresql/18/docker, это особенность Docker Official Image. Менять его не нужно: достаточно смонтировать один том на /var/lib/postgresql, и версионные подкаталоги будут создаваться внутри него.

Почему после пересоздания контейнера postgres:18 база пустая, хотя том есть?

Чаще всего том смонтирован на старый путь /var/lib/postgresql/data. Реальный каталог данных лежит в /var/lib/postgresql/18/docker, на этот путь ничего не смонтировано, и Docker создаёт анонимный том, который новый контейнер не подхватывает.

Куда монтировать том для PostgreSQL 18 в Docker?

На /var/lib/postgresql, как объявлено в VOLUME образа начиная с 18-й версии. Для версий 17 и ниже по умолчанию остаётся путь /var/lib/postgresql/data.

Можно ли задать свой PGDATA в postgres:18?

Можно через переменную окружения PGDATA, но путь должен лежать внутри смонтированного тома. При этом вы отказываетесь от версионной раскладки по умолчанию, и перекладывать каталоги под pg_upgrade --link придётся вручную.

Как быстро проверить, что данные переживут пересоздание контейнера?

Создайте контрольную таблицу, выполните docker compose down без ключа -v, затем docker compose up -d и выберите данные из таблицы. Заодно проверьте docker inspect: у контейнера должен быть один том с destination /var/lib/postgresql.

Удаляет ли docker volume prune именованные тома?

По умолчанию нет, только анонимные. Именованные тома затрагивает команда с ключом --all. Но если база по ошибке писала в анонимный том, prune удалит именно её.

Столкнулись с похожей задачей? Обращайтесь — решим

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

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

📞 +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Источники

© ООО «АйТи-Фреш» · Москва · Все статьи