PGDATA и монтирование в Docker для PostgreSQL 18: как собрать compose, чтобы база пережила пересоздание контейнера
В образе 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 внутри одного тома. Для маленькой базы под скромный сервис это допустимый костыль, для всего, что вы собираетесь обновлять годами, я бы так не делал. Костыль имеет свойство пережить того, кто его поставил.
- Новая установка: один именованный том на /var/lib/postgresql, PGDATA не задавать
- Нужны снимки ФС или доступ к файлам с хоста: bind mount на тот же путь внутри контейнера, владелец каталога по uid образа
- Свой PGDATA: только внутри смонтированного пути и только осознанно
- Путь /var/lib/postgresql/data для образа 18 не использовать
Рабочий 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Если вам нужно, чтобы кто-то прошёл по вашим контейнерам с базами и проверил именно это, мы это делаем за пару часов на сервер: инвентаризация томов, тест пересоздания и отчёт, что где лежит.
Если 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, о чём сказано в описании образа. Если вы идёте через дамп, этот вопрос вас не касается, а контрольные суммы на новой базе можно считать приятным бонусом.
Кейс: музыкальная школа «Гармония звука», 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 удалит именно её.
Источники
- Docker Hub: официальный образ postgres — Проверено: PGDATA в 18+ равен /var/lib/postgresql/18/docker, VOLUME изменён на /var/lib/postgresql, смысл изменения для pg_upgrade --link, опт-ин PGDATA для старых версий, поведение при непустом каталоге данных, POSTGRES_PASSWORD, POSTGRES_INITDB_ARGS. https://hub.docker.com/_/postgres
- docker-library/postgres: Dockerfile и docker-entrypoint.sh для 18 — Проверено: ENV PGDATA и VOLUME в Dockerfile (trixie и alpine), PG_VERSION 18.6, uid postgres 999 (Debian) и 70 (Alpine), проверка PG_VERSION и обнаружение неиспользуемого /var/lib/postgresql/data в entrypoint. https://github.com/docker-library/postgres/tree/master/18
- docker-library/postgres, issue 1400 — Проверено: случай записи базы в 18/docker при монтировании на /var/lib/postgresql/data, причина с BusyBox mountpoint на Alpine, закрытие через PR 1409 в апреле 2026. https://github.com/docker-library/postgres/issues/1400
- PostgreSQL Documentation: pg_upgrade и Release 18 — Проверено: --link требует одной файловой системы для старого и нового кластеров, после старта нового кластера старый небезопасен; initdb по умолчанию включает контрольные суммы, опция --no-data-checksums, pg_upgrade требует совпадения настроек. https://www.postgresql.org/docs/current/pgupgrade.html и https://www.postgresql.org/docs/18/release-18.html
- PostgreSQL: Versioning Policy — Проверено: релиз PostgreSQL 18 от 25.09.2025, актуальная минорная версия 18.6, окончание поддержки 14.11.2030. https://www.postgresql.org/support/versioning/
- Docker Docs: volumes, volume prune, compose down, compose services — Проверено: поведение анонимных томов, volume prune только для анонимных (--all для всех), compose down -v, длинный синтаксис bind с create_host_path. https://docs.docker.com/engine/storage/volumes/ , https://docs.docker.com/reference/cli/docker/volume/prune/ , https://docs.docker.com/reference/cli/docker/compose/down/ , https://docs.docker.com/reference/compose-file/services/



