АйТи Фреш
Главная / Статьи / DevOps и автоматизация
DevOps и автоматизация

Обновил образ до postgres:18 — база создаётся заново, хотя старый том на месте

Автор: Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш» · · ~29 мин чтения
Обновил образ до postgres:18 — база создаётся заново, хотя старый том на месте
Иллюстрация к статье «Обновил образ до postgres:18 — база создаётся заново, хотя старый том на месте».

Вы поменяли в docker-compose.yml тег postgres:16 на postgres:18, контейнер поднялся, приложение отрапортовало «база пустая, накатываю миграции». Том на месте, docker volume ls его показывает, размер прежний — а данных нет. Это не баг вашего приложения и не порча тома. В восемнадцатом образе поменялись PGDATA и объявленный VOLUME, и ваше старое монтирование теперь указывает мимо каталога данных. Ниже — что именно изменилось и почему, как за минуту понять, в каком из трёх сценариев вы находитесь, как вытащить данные из анонимного тома, если контейнер уже пересоздали, и как правильно готовить переход с 17-й версии, чтобы потом сработал быстрый pg_upgrade --link и не споткнуться о контрольные суммы, которые initdb 18 включает по умолчанию.

Что поменялось в образе: две строки Dockerfile, ломающие привычку

Начну с фактуры, потому что дальше всё крутится вокруг неё. Официальный образ postgres до 17-й версии включительно объявляет каталог данных прямо в корне тома: PGDATA равен /var/lib/postgresql/data, и VOLUME объявлен на тот же путь. Все compose-файлы последних десяти лет написаны под это: монтируем именованный том на /var/lib/postgresql/data и живём спокойно. В 18-й версии обе строки изменились.

# 17/trixie/Dockerfile
ENV PG_MAJOR 17
ENV PGDATA /var/lib/postgresql/data
VOLUME /var/lib/postgresql/data

# 18/trixie/Dockerfile — на момент написания PG_VERSION 18.6-1.pgdg13+2
# NOTE: in 18+, PGDATA has changed to match the pg_ctlcluster standard
#       directory structure, and the VOLUME has moved from
#       /var/lib/postgresql/data to /var/lib/postgresql
ENV PG_MAJOR 18
ENV PGDATA /var/lib/postgresql/18/docker
VOLUME /var/lib/postgresql

Сделано это осознанно, в pull request №1259 репозитория docker-library/postgres, влитом 5 июня 2025 года. Мотив честный и технически правильный: pg_upgrade с ключом --link требует, чтобы старый и новый каталоги данных лежали в пределах одной точки монтирования — иначе жёсткие ссылки между ними просто не создаются. При старой раскладке каталог данных был корнем тома, и уложить рядом два кластера разных мажорных версий было невозможно без плясок с двумя томами и ручными пробросами. Теперь том монтируется на /var/lib/postgresql, а внутри лежат подкаталоги вида 18/docker, 19/docker — ровно как это делает pg_ctlcluster в Debian и Ubuntu. То есть образ наконец стал похож на нормальную пакетную установку PostgreSQL.

И сразу важная оговорка, которую я слышу как вопрос от каждого второго: это изменение самого образа, а не PostgreSQL 18. Если вы ставите восемнадцатую версию пакетами на голый Debian, PGDATA у вас будет там же, где и был — /var/lib/postgresql/18/main. Никакой каталог docker в мире вне Docker Official Image не появляется. Не переносите этот путь в свои bare-metal инструкции и не ищите его в документации PostgreSQL — его там нет.

Никогда не пишите тег просто postgres:18 или postgres:latest в боевом compose. Пиньте патч-версию — postgres:18.6-trixie. Сменить патч-версию — одно осознанное действие, а смену мажорной версии тогда уже никто не проведёт случайно, одним `docker compose pull`. Половина историй про «а у нас образ сам обновился ночью» заканчивается именно тем, что описано в этой статье.

Механика: откуда берётся анонимный том и почему ваш именованный не при делах

Дальше чистая механика Docker, без магии. Инструкция VOLUME в Dockerfile означает: если при запуске контейнера на этот путь ничего не смонтировано явно, Docker сам создаст анонимный том, смонтирует его туда и скопирует внутрь содержимое каталога из образа. Анонимный — значит с именем в виде шестидесяти четырёх шестнадцатеричных символов, без метки, без принадлежности к compose-проекту.

Теперь наложите на это ваш старый compose. Вы монтируете именованный том pgdata на /var/lib/postgresql/data. Образ объявляет VOLUME на /var/lib/postgresql — путь уровнем выше, и на нём у вас ничего не смонтировано. Docker послушно создаёт анонимный том и вешает его на /var/lib/postgresql, а ваш pgdata монтируется поверх, во вложенный каталог data. Внутри контейнера получается вот такая картинка: каталог /var/lib/postgresql — анонимный том, /var/lib/postgresql/data — ваш именованный том, а PGDATA указывает на /var/lib/postgresql/18/docker, то есть в анонимный. Ваш том формально смонтирован, физически цел, размер прежний — и полностью не при делах.

Пока контейнер жив, всё работает: база пишется, приложение довольно. Важная деталь, которую часто упускают: docker compose up -d при пересоздании контейнера (сменили переменную окружения, обновили образ) по умолчанию переносит анонимные тома из предыдущего контейнера — в документации Compose ключ -V/--renew-anon-volumes описан именно как «пересоздать анонимные тома вместо того, чтобы брать данные из предыдущих контейнеров». Поэтому ловушка часто молчит неделями. Беда приходит, когда старого контейнера уже нет: docker compose down и затем up -d, docker rm плюс docker run, up -V, redeploy в CI через down/up, пересоздание стека из Portainer. Новый контейнер получает новый пустой анонимный том, entrypoint не находит в нём PG_VERSION и запускает initdb. Приложение видит чистую базу и радостно накатывает миграции с нуля. Старый анонимный том при этом остаётся висеть без владельца — и если где-то в конвейере есть docker volume prune (с Docker 23 он по умолчанию удаляет как раз неиспользуемые анонимные тома) или docker compose down -v (сносит и именованные тома проекта, и анонимные тома контейнеров), данных больше нет вообще.

Диагностика занимает три команды. Первая показывает, сколько на самом деле томов у контейнера и куда они смонтированы; вторая — куда реально смотрит PGDATA; третья — какие анонимные тома болтаются без дела.

# 1. все монтирования контейнера: имя тома -> точка внутри
docker inspect pg --format '{{range .Mounts}}{{.Type}} {{.Name}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'

# 2. фактический PGDATA и есть ли там кластер
docker exec pg sh -c 'echo "PGDATA=$PGDATA"; ls -la "$PGDATA" | head'
docker exec pg sh -c 'cat "$PGDATA/PG_VERSION"'

# 3. висячие тома — кандидаты в потеряшки
docker volume ls -qf dangling=true

Признак, по которому я опознаю проблему мгновенно: в выводе первой команды у контейнера с базой два тома вместо одного, и один из них — безымянный хеш на /var/lib/postgresql. Если видите такое — вы уже в этой истории, просто ещё не пересоздавали контейнер.

До того как что-либо чинить: уберите из любых скриптов и CI-шагов docker compose down -v, docker system prune --volumes и docker volume prune. Пока данные лежат в висячем анонимном томе, эти команды — точка невозврата.
Обновил образ до postgres:18 — база создаётся заново, хотя старый том на месте — схема
Схема к статье. Открыть схему в полном размере

Три разных сценария, которые постоянно путают

«Поломка при обновлении на postgres:18» — это на самом деле три разные ситуации с разными последствиями. Разводить их важно, потому что в двух из трёх у вас всё в порядке и паниковать не нужно.

Сценарий первый, самый частый и самый безобидный: у вас была рабочая база 17-й версии в томе на /var/lib/postgresql/data, вы поменяли тег на 18 и запустили. Контейнер не стартует и падает с внятной ошибкой. Это защита в entrypoint: он проверяет, что PGDATA пуст, но при этом видит PG_VERSION по старым путям — и отказывается инициализировать новую базу поверх. Данные целы, вы просто ещё не сделали pg_upgrade.

Error: in 18+, these Docker images are configured to store database data in a
       format which is compatible with "pg_ctlcluster" (specifically, using
       major-version-specific directory names).  This better reflects how
       PostgreSQL itself works, and how upgrades are to be performed.

       Counter to that, there appears to be PostgreSQL data in:
         /var/lib/postgresql/data

       This is usually the result of upgrading the Docker image without
       upgrading the underlying database using "pg_upgrade"...

Сценарий второй — тот, ради которого написана статья. База создаётся с нуля уже на 18-м образе, но со старым монтированием на /var/lib/postgresql/data. Никаких PG_VERSION по старым путям нет, значит первая проверка молчит. В актуальных образах есть вторая: если PGDATA стандартный, старых баз не нашлось, а на /var/lib/postgresql/data при этом что-то смонтировано — entrypoint ругается на «unused mount/volume» и тоже отказывается стартовать. Логика в коде выглядит так:

# 18/trixie/docker-entrypoint.sh, функция docker_setup_env
if [ -s "$PGDATA/PG_VERSION" ]; then
    DATABASE_ALREADY_EXISTS='true'
elif [ "$PGDATA" = "/var/lib/postgresql/$PG_MAJOR/docker" ]; then
    for d in /var/lib/postgresql /var/lib/postgresql/data /var/lib/postgresql/*/docker; do
        if [ -s "$d/PG_VERSION" ]; then
            OLD_DATABASES+=( "$d" )
        fi
    done
    if [ "${#OLD_DATABASES[@]}" -eq 0 ] && [ "$PG_MAJOR" -ge 18 ] && {
        mountpoint -q /var/lib/postgresql/data \
        || awk '$5 == "/var/lib/postgresql/data" { found = 1 } END { exit !found }' /proc/self/mountinfo
    }; then
        OLD_DATABASES+=( '/var/lib/postgresql/data (unused mount/volume)' )
    fi
fi

И вот тут была дыра, из-за которой люди теряли данные. В alpine-вариантах нет coreutils, и mountpoint там — апплет BusyBox, который определяет точку монтирования по смене устройства относительно родительского каталога. Тома Docker с драйвером local — это bind-монтирования каталогов из /var/lib/docker/volumes, часто на той же файловой системе, и такая эвристика их не распознаёт. Проверка молча возвращала «не смонтировано», контейнер стартовал, initdb отрабатывал в анонимном томе. Исправление пришло в PR №1409 («Fix Alpine missing /var/lib/postgresql/data mounts»), влитом 21 апреля 2026 года: резервный разбор /proc/self/mountinfo через awk, который вы видите во второй строке условия, — он повторяет логику coreutils без установки лишних пакетов. В обсуждении PR пользователи описывали тихую пропажу данных на postgres:18.3-alpine3.23, а в issue №1400 (февраль 2026) — на postgres:18.1-alpine. Мораль простая: если вы застряли на alpine-образе, собранном до этого исправления, защита вас не прикрывает.

Сценарий третий — экзотика, но встречается. Кто-то заранее прописал PGDATA руками, например /var/lib/postgresql/data/pgdata (популярный паттерн из старых compose-файлов) или, начитавшись форумов, /var/lib/postgresql/data/18/docker. В этом случае PGDATA не равен стандартному пути, вся ветка проверок вообще не выполняется, и база спокойно живёт внутри вашего именованного тома. Работать будет. Но вы теряете ровно то, ради чего затевался переезд — возможность pg_upgrade --link внутри одного монтирования, — и остаётесь вне поддерживаемой схемы. Вариант с /var/lib/postgresql/data/18/docker к тому же не входит в рекомендуемую схему: цикл поиска старых баз в entrypoint смотрит только /var/lib/postgresql/*/docker, и такой вложенный путь он не видит — ровно этому посвящён issue №1400. Рекомендация в README образа одна: один том на /var/lib/postgresql.

Если вы на alpine-образе 18-й версии и у вас старый тег вроде 18.1-alpine или 18.3-alpine3.23 — просто обновите образ до сборки после 21 апреля 2026 года (актуальный тег вида 18.6-alpine3.24) перед тем, как что-то трогать. Одно это включит проверку, которая не даст контейнеру молча стартовать с потерянным томом.
Цифры и версии: Три разных сценария, которые постоянно путают — схема
Цифры и версии: Три разных сценария, которые постоянно путают. Открыть схему в полном размере

Разбор: «Внутренний компас», 11 рабочих мест, потерянная база записи клиентов

Проект, на котором я всё это трогал руками. Центр психологического консультирования «Внутренний компас», 11 рабочих мест: психологи, администратор ресепшена и бухгалтер; инфраструктуру обслуживаем целиком. Есть небольшой внутренний сервер на Ubuntu 24.04 в виртуалке — Docker 27, docker compose v2, несколько контейнеров: сервис онлайн-записи клиентов с расписанием специалистов, внутренняя вики с методичками и Nextcloud для документов, и рядом с каждым свой postgres. Всё поднято compose-файлами, тома именованные, бэкап — ночной pg_dumpall в сетевую шару, хранение 14 копий. Для консультационного центра база записи — это персональные данные клиентов и график приёмов, так что её потеря — не просто неудобство.

В феврале 2026-го приходящий администратор в рамках планового обновления поменял в трёх compose-файлах postgres:16-alpine на postgres:18-alpine. Compose-файлы были типовые, со строкой volumes: - pgdata:/var/lib/postgresql/data. Базы вики и Nextcloud были не пустые: оба контейнера честно упали с ошибкой про OLD_DATABASES — админ увидел, вернул тег 16 и отложил разбирательство. А третий, база нового сервиса записи, был поднят с нуля неделей раньше на пустом томе, и в alpine-варианте того времени проверка «unused mount/volume» не срабатывала. Контейнер стартовал. Сервис работал. Три недели, пока ресепшен вносил туда записи.

# было — и это ровно та строка, которая всё ломает
services:
  db:
    image: postgres:18-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data   # <-- мимо PGDATA

# стало
services:
  db:
    image: postgres:18.6-alpine3.24
    volumes:
      - pgdata:/var/lib/postgresql        # <-- том накрывает 18/docker

Дальше — плановое обслуживание в пятницу вечером: обновление ядра, и перед перезагрузкой скрипт выполнил docker compose down, после неё — docker compose up -d. Контейнер создан заново, получил новый анонимный том, initdb, миграции сервиса на пустой базе. Утром понедельника администратор ресепшена написала: «пропало расписание и все клиенты, ошибок никаких». Классика: система не сломана, она просто новая.

Восстановление заняло сорок минут и прошло гладко ровно потому, что никто не успел выполнить docker volume prune. Висячих томов на хосте было три, нужный опознали по дате создания и по размеру — около 1,8 ГБ против сотен килобайт у соседей. Дальше подняли спасательный контейнер поверх найденного тома и сняли дамп.

# кандидаты
docker volume ls -qf dangling=true \
  | xargs -I{} docker volume inspect {} --format '{{.CreatedAt}} {{.Name}} {{.Mountpoint}}'

# размеры (нужны права root на /var/lib/docker)
sudo du -sh /var/lib/docker/volumes/*/_data 2>/dev/null | sort -h | tail -5

# проверяем, что это действительно кластер 18-й версии
sudo cat /var/lib/docker/volumes/<hash>/_data/18/docker/PG_VERSION

# спасательный контейнер: том монтируем на /var/lib/postgresql
docker run --rm -d --name pgrescue \
  -v <hash>:/var/lib/postgresql \
  -e POSTGRES_PASSWORD=irrelevant \
  postgres:18.6-alpine3.24

# ждём, пока сервер примет подключения
until docker exec pgrescue pg_isready -U postgres; do sleep 1; done

# без -t: TTY испортит переводы строк в дампе
docker exec pgrescue pg_dumpall -U postgres > /backup/booking-rescue.sql
docker stop pgrescue

Дамп снялся примерно за минуту, восстановили его в пересозданный по правильной схеме контейнер, сервис поднялся с данными на момент пятничного обслуживания. Потеряли ровно ноль записей: новая пустая база прожила выходные, центр в субботу и воскресенье не работает, и в неё никто ничего не внёс. Итог по проекту: во всех compose-файлах монтирование переведено на /var/lib/postgresql, теги зафиксированы до патч-версии, в скрипт обслуживания добавлена простая проверка — если у контейнера с базой больше одного тома, скрипт останавливается. И отдельная строчка в регламенте: ночной pg_dumpall теперь проверяется восстановлением раз в месяц, а не «мы же его настроили».

Обратите внимание на самое неприятное в этой истории: три недели всё работало. Ошибка проявляется не в момент обновления образа, а в момент следующего пересоздания контейнера — часто при первом `down`/`up` — иногда через месяц, руками совершенно другого человека.

Как правильно переходить с 17 на 18

Здесь надо развести две вещи, которые сливают в одну и потом удивляются. Смена монтирования — это про раскладку каталогов. Смена мажорной версии PostgreSQL — это про формат данных, и её всё равно надо делать либо через pg_upgrade, либо через дамп и восстановление. Образ за вас апгрейд не сделает и делать не должен: в контейнере с 18-й версией нет бинарников 17-й, а pg_upgrade требует обеих.

Мой порядок такой. Сначала переводим существующую 17-ю на новую раскладку, оставаясь на 17-й версии — это обратимая, дешёвая операция без изменения формата. Документация образа предлагает ровно это: явно задать PGDATA=/var/lib/postgresql/17/docker и монтировать том на /var/lib/postgresql, предварительно переложив файлы в подкаталог PG_MAJOR/docker. Я делаю это не переименованием внутри старого тома, а копированием в новый — чтобы в любой момент можно было откатиться на прежний compose.

# 0. останавливаем базу штатно, не kill
docker compose stop db

# 1. новый том и перекладка. uid проверяем у своего же образа:
#    alpine -> 70, debian/trixie -> 999
docker run --rm postgres:17.11-alpine3.24 id postgres

docker volume create pgdata_new
docker run --rm -v pgdata_old:/old:ro -v pgdata_new:/new alpine:3.22 sh -c '
  mkdir -p /new/17/docker &&
  cp -a /old/. /new/17/docker/ &&
  chown -R 70:70 /new'

# 2. compose: тот же тег 17, но новый том и явный PGDATA
#    image: postgres:17.11-alpine3.24
#    environment:
#      PGDATA: /var/lib/postgresql/17/docker
#    volumes:
#      - pgdata_new:/var/lib/postgresql
docker compose up -d db
docker compose logs -f db

Убедились, что 17-я поднялась на новом томе и приложение работает — старый том пока не трогаем, он ваш откат. Дальше сам апгрейд до 18-й. Внутри одного тома /var/lib/postgresql теперь лежит 17/docker, и pg_upgrade может создать рядом 18/docker с жёсткими ссылками, не упираясь в границу файловой системы. Ради этого всё и затевалось. Практических путей два: собрать одноразовый образ, где есть бинарники обеих версий, либо взять готовый образ tianon/postgres-upgrade с тегами вида 17-to-18 — его автор сам называет proof of concept, так что сначала прогоните на копии тома. Для маленьких баз я честно предпочитаю третий вариант — pg_dumpall со старого контейнера и восстановление в чистый 18-й. На базе до 20–30 ГБ это укладывается в то же окно и не требует ни одного нестандартного образа.

Здесь есть подводный камень, который в Docker-инструкциях почти не упоминают. В PostgreSQL 18 initdb по умолчанию включает контрольные суммы страниц (отключаются ключом --no-data-checksums), а pg_upgrade, согласно release notes 18, требует, чтобы настройка контрольных сумм у старого и нового кластера совпадала. Кластеры, созданные образами 17 и старше без POSTGRES_INITDB_ARGS=--data-checksums, контрольных сумм не имеют — и апгрейд упадёт на проверке совместимости. Выхода два: передать новому кластеру --no-data-checksums или заранее включить суммы на остановленной 17-й утилитой pg_checksums (она переписывает все файлы, на больших базах это долго). Проверить текущее состояние можно запросом SHOW data_checksums;.

# 1. у старого кластера checksums выключены?
docker compose exec db psql -U postgres -c 'SHOW data_checksums;'
docker compose stop db

# 2. апгрейд внутри одного тома; новый кластер — без checksums, как старый
docker run --rm \
  -v pgdata_new:/var/lib/postgresql \
  -e PGDATAOLD=/var/lib/postgresql/17/docker \
  -e PGDATANEW=/var/lib/postgresql/18/docker \
  -e POSTGRES_INITDB_ARGS='--no-data-checksums' \
  tianon/postgres-upgrade:17-to-18 --link

# 3. compose: тег 18, PGDATA из переменных убрать, том тот же
#    image: postgres:18.6-alpine3.24
#    volumes:
#      - pgdata_new:/var/lib/postgresql

Образ tianon собран на Debian, а uid пользователя postgres в alpine (70) и в debian (999) различается — если основной образ у вас alpine, проверьте владельца каталогов после апгрейда. Если выбираете дамп и восстановление, вопрос контрольных сумм снимается сам: новый кластер 18 получит их по умолчанию. Заодно в 18-й pg_upgrade по умолчанию переносит статистику планировщика (кроме расширенной), так что после апгрейда vacuumdb --analyze-in-stages нужен уже не для всего подряд.

И ещё одно, что экономит нервы: после успешного запуска 18-й не спешите удалять каталог 17/docker и старый том. Если апгрейд шёл с --link, старый кластер после запуска нового считается непригодным к использованию — в документации pg_upgrade прямо сказано, что после запуска нового кластера к старому доступа уже не будет: файлы общие, и записи 18-й версии портят 17-ю. То же относится к новому режиму --swap из PostgreSQL 18 — он разрушает старый каталог уже на этапе переноса. Ваш настоящий откат в этом случае — не старый каталог, а дамп, снятый до начала работ. Снимайте его всегда, даже если уверены в --link.

Проверьте uid у своего образа перед chown: в alpine-вариантах пользователь postgres имеет uid и gid 70, в debian-вариантах (bookworm, trixie) — 999. Перепутаете — получите «permission denied» на PGDATA и полчаса недоумения.
Порядок действий: Как правильно переходить с 17 на 18 — схема
Порядок действий: Как правильно переходить с 17 на 18. Открыть схему в полном размере

Если контейнер уже пересоздали и база пустая

Порядок действий в аварийном режиме, по убыванию срочности. Первое и главное — остановить всё, что может подчистить тома. Никаких prune, никаких down -v, не запускать плановые скрипты уборки, не давать мониторингу «освободить место на диске». Ваши данные с высокой вероятностью живы и лежат в висячем анонимном томе, у которого нет владельца. Ровно по этому признаку его и убивают все автоматические чистилки.

Второе — снять слепок состояния, пока никто ничего не трогал: список томов с датами и размерами, вывод docker inspect по контейнеру, логи контейнера с момента подозрительного старта. В логах ищите строки инициализации — «The files belonging to this database system will be owned by user» и «PostgreSQL init process complete; ready for start up» — — она покажет, когда именно база родилась заново. Это ваша граница потерянного периода.

Третье — поиск нужного тома. Опознаём по трём признакам сразу: дата создания около момента перехода на 18-й образ, размер, сопоставимый с ожидаемым размером базы, и наличие файла 18/docker/PG_VERSION внутри. На загруженном хосте с десятками контейнеров висячих томов бывает много, поэтому смотрите именно на комбинацию, а не на что-то одно.

Четвёртое — не поднимайте найденный том сразу в боевой compose. Запустите отдельный спасательный контейнер, снимите pg_dumpall, положите дамп в надёжное место вне хоста и только потом собирайте прод по правильной схеме. Соблазн «сейчас просто поправлю путь в compose и перезапущу» велик, но если вы промахнётесь ещё раз, вы получите второй анонимный том и потеряете ориентацию в том, где какая база.

# слепок состояния до любых действий
docker volume ls > /root/vol-before.txt
docker inspect pg > /root/pg-inspect.json
docker logs pg --since 720h 2>&1 | grep -iE 'init process complete|files belonging|initdb|PG_VERSION' | head -40

# восстановление в чистый контейнер, собранный по правильной схеме
docker compose up -d db
docker compose exec -T db psql -U postgres < /backup/booking-rescue.sql

И отдельно про то, чего делать не надо. Не пытайтесь скопировать файлы кластера из анонимного тома в именованный «пофайлово, чтобы быстрее» при работающем контейнере — получите неконсистентный каталог. Не восстанавливайте дамп поверх базы, куда приложение уже накатило свои миграции с нуля: сначала уроните и пересоздайте базу, потом лейте дамп. И не выключайте бэкапы «на время разбирательства» — именно в такие моменты они и нужны.

Если висячего тома нет и дампов тоже нет — дальше только файловое восстановление на уровне ФС хоста, и шансы обратно пропорциональны времени работы новой базы. Это тот случай, когда сервер надо выключить, а не «ещё немножко поискать».

Что я делаю по умолчанию и на что можно забить

Соберу приоритеты, потому что список рекомендаций всегда длиннее, чем есть времени. В первую очередь — инвентаризация. Пройдите по всем хостам с Docker и найдите контейнеры с постгресом, у которых больше одного тома. Это делается одной командой и сразу показывает, где вы уже стоите на мине. Дальше — фиксация патч-версий в compose. Не мажорной, а именно патч: postgres:18.6-trixie, а не postgres:18. Оба пункта закрываются за час на десяток хостов и снимают процентов девяносто риска.

# инвентаризация: контейнеры postgres с более чем одним томом
for c in $(docker ps -aq); do
  img=$(docker inspect "$c" --format '{{.Config.Image}}')
  case "$img" in *postgres*) ;; *) continue ;; esac
  n=$(docker inspect "$c" --format '{{len .Mounts}}')
  [ "$n" -gt 1 ] && echo "ПРОВЕРИТЬ: $(docker inspect "$c" --format '{{.Name}}') $img томов=$n"
done

Во вторую очередь — бэкапы, и не в смысле «настроены», а в смысле «проверены восстановлением». В истории с «Внутренним компасом» всё закончилось хорошо не потому, что мы умные, а потому что висячий том никто не удалил. Это везение, а не процесс. Ночной pg_dumpall с хранением двух недель и ежемесячной проверкой восстановления стоит копейки и закрывает не только эту проблему, но и десяток других — от dropped table руками разработчика до сдохшего NVMe.

В третью очередь — сам переезд на новую раскладку. Он не горит. Если у вас сейчас работает postgres:17 с монтированием на /var/lib/postgresql/data — ничего не сломано и ломаться не собирается, 17-я версия поддерживается до ноября 2029 года. Переезжайте спокойно, когда будете планово идти на 18-ю, и делайте это в два отдельных шага, как я описал выше. Спешить и совмещать смену раскладки со сменой мажорной версии в одном окне — плохая идея.

И то, на что можно забить со спокойной душой. Не надо переписывать раскладку на dev-стендах и в CI, где база живёт минуты и создаётся из фикстур — там потеря анонимного тома ничего не стоит, а времени на аккуратный переезд уйдёт столько же. Не надо мигрировать managed-базы у облачных провайдеров: там свой запуск, PGDATA образа Docker Official Image к ним отношения не имеет. И не надо бояться самой схемы с подкаталогами версий — она правильная, она давно принята в Debian, и через год-два все compose-файлы будут написаны под неё, а сегодняшняя боль забудется как боль перехода с links на networks.

Простой критерий здоровья, который я вешаю в мониторинг: у контейнера с PostgreSQL должен быть ровно один том, и внутри него должен находиться каталог PGDATA. Два тома у базы — повод разбираться до того, как кто-то нажмёт redeploy.
Памятка: Что я делаю по умолчанию и на что можно забить — схема
Памятка: Что я делаю по умолчанию и на что можно забить. Открыть схему в полном размере

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

Это изменение PostgreSQL 18 или только Docker-образа?

Только образа Docker Official Image. Путь /var/lib/postgresql/18/docker придуман мейнтейнерами образа, чтобы раскладка соответствовала pg_ctlcluster. Если вы ставите PostgreSQL 18 пакетами на Debian или Ubuntu, кластер будет в /var/lib/postgresql/18/main, как и раньше. В документации самого PostgreSQL каталога docker нет.

У меня работает postgres:17 с монтированием на /var/lib/postgresql/data. Надо срочно всё менять?

Нет. На 17-й версии старая раскладка — штатная и поддерживаемая, поддержка ветки 17 идёт до ноября 2029 года. Проблема возникает только в момент перехода на образ 18-й версии. Спокойно переезжайте в плановое окно, отдельно перекладку каталогов и отдельно апгрейд мажорной версии.

Можно просто задать PGDATA=/var/lib/postgresql/data и оставить старый compose?

Технически заработает: PGDATA окажется внутри вашего именованного тома. Но при этом полностью отключаются защитные проверки образа (они выполняются только когда PGDATA равен стандартному пути), и вы теряете возможность pg_upgrade --link при следующем мажорном обновлении. Я так не делаю — это откладывание проблемы на год.

Как понять, где сейчас лежат мои данные?

Выполните docker exec <контейнер> sh -c 'echo $PGDATA' и сравните с выводом docker inspect <контейнер> --format '{{range .Mounts}}{{.Name}} -> {{.Destination}}{{"\n"}}{{end}}'. Если PGDATA не находится внутри пути, куда смонтирован ваш именованный том, — данные пишутся в анонимный том и исчезнут при следующем пересоздании контейнера.

Контейнер падает с ошибкой про OLD_DATABASES. Это уже потеря данных?

Наоборот — это защита сработала, и данные целы. Образ увидел кластер старой версии по прежнему пути и отказался инициализировать поверх него новую базу. Вам нужно выполнить апгрейд самой базы через pg_upgrade или дамп с восстановлением; смены тега образа для этого недостаточно.

Помогает ли обновление патч-версии образа?

От тихой потери — да, существенно. В ранних 18-х alpine-образах проверка «unused mount/volume» не срабатывала из-за особенностей mountpoint в BusyBox, и контейнер стартовал молча. В образах, собранных после PR №1409 (влит 21 апреля 2026), добавлен резервный разбор /proc/self/mountinfo, и такая конфигурация теперь останавливает запуск с ошибкой. Обновиться стоит до того, как начинать разбирательство.

pg_upgrade с 17 на 18 падает на проверке контрольных сумм. Что делать?

В PostgreSQL 18 initdb по умолчанию включает data checksums, а кластеры из образов 17 и старше обычно созданы без них; pg_upgrade требует совпадения этой настройки. Инициализируйте новый кластер с --no-data-checksums (в tianon/postgres-upgrade — через POSTGRES_INITDB_ARGS) или включите суммы на остановленной 17-й утилитой pg_checksums --enable. При переходе через дамп и восстановление проблемы нет.

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

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

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

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

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

Источники

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