Підводні камені контейнера Postgres у Docker: сценарії втрати даних і як їх уникнути

Було корисно?

Якщо ви коли-небудь перезапускали «безстанний» контейнер Postgres і виявляли «свіжу» базу даних, яка чекає на вас як золота рибка з амнезією, ви зустріли
найбільшу неправду у світі контейнерів: начебто база даних більше дбає про ваш YAML, ніж про файлову систему.

Postgres нудний у найкращому сенсі — поки ви не запустите його в Docker з неправильним вибором зберігання, недбалими бекапами або безтурботними оновленнями.
Тоді це перетворюється на детективну історію, де винуватець зазвичай — ви, вчора.

Що насправді йде не так (і чому Docker це спрощує)

Більшість інцидентів «втрата даних Postgres у Docker» не містять містики. Це зіткнення двох цілком логічних систем:
Docker припускає, що контейнер тимчасовий; Postgres припускає, що його каталог даних — святе.

Docker дає прості примітиви — образи, контейнери, томи, bind-монти, мережі. Postgres дає суворі правила стійкості — WAL,
fsync, контрольні точки та відверту нелюбов до пошкоджених файлів. Коли хтось постраждає, зазвичай це через неправильне припущення:
«Контейнер — це база даних». Ні. Контейнер — це обгортка процесу. База даних — це стан на диску плюс WAL плюс бекапи та правила, яких ви дотримуєтесь.

Ви безумовно можете запускати Postgres у Docker. Багато продуктивних стеків так роблять. Але потрібно бути явним щодо:

  • Де знаходиться каталог даних і як він монтується.
  • Як працюють резервні копії — і як ви довели, що їх можна відновити.
  • Як ви робите оновлення, не ініціалізуючи випадково кластер заново.
  • Які компроміси по надійності ви зробили (іноді непомітно).
  • Як ви виявляєте проблеми зі зберіганням і тиском на WAL до того, як це перетвориться на простої.

Патерн помилки постійний: невеликий варіант зручності на першій добі стає аварією на дванадцятому тижні, бо ніхто не переглянув налаштування після того, як «воно працювало».

Цікавинки та історичний контекст

  • Postgres починався в середині 1980-х (як POSTGRES в Берклі) і зберіг культуру «робити правильно з даними» навіть з розвитком інструментів.
  • Write-Ahead Logging (WAL) — серце надійності Postgres. Це не опція в сенсі духу, навіть коли конфіги намагаються робити вигляд протилежного.
  • Docker volumes керуються Docker і за замовчуванням живуть під директорією даних Docker; це робить їх портативними між замінами контейнерів, але не між втратою хоста.
  • Bind-монти передували Docker як Unix-концепт. Вони потужні й прозорі, тому й саме через них часто «стріляють собі у ногу» через права й помилки в шляхах.
  • “docker system prune” існує роками і залишається одним з найшвидших способів знищити не те, якщо ви вважаєте томи «кешем».
  • Мажорні оновлення Postgres не відбуваються in-place за замовчуванням; зазвичай потрібні dump/restore або pg_upgrade, і обидва мають гострі кути всередині контейнерів.
  • Overlay-файлові системи стали мейнстрімом з контейнерами; вони чудові для образів і шарів, але погане місце для баз даних, якщо вам подобаються сюрпризи з I/O.
  • Kubernetes популяризував мантру «pets vs cattle», яка працює чудово, поки ви не застосуєте логіку «cattle» до самого зберігання бази даних.
  • Postgres давно має потужну логічну реплікацію (publication/subscription); це практичний шлях міграції з поганого Docker-налаштування без простого часу простою.

Ваша ментальна модель: життєвий цикл контейнера проти життєвого циклу бази даних

Контейнери замінні. Стан бази — ні.

Контейнер — це запущений екземпляр образу. Вбити його, створити знову, переселити — нормально. Саме для цього він призначений. Postgres байдуже до контейнерів.
Postgres дбає про PGDATA (каталог даних) і припускає:

  • Файли залишаються на місці.
  • Власність та права залишаються послідовними.
  • fsync дійсно означає fsync.
  • WAL доходить до стійкого сховища, коли він про це повідомляє.

Ваше завдання — гарантувати, щоб зберігання Docker не порушувало ці припущення. Більшість проблем зводиться до:

  • Неправильна ціль монтирования: Postgres пише в шари файлової системи контейнера, а не в персистентне сховище.
  • Неправильне джерело монтирования: ви змонтували порожній каталог і випадково запустили initdb.
  • Неправильні права: Postgres не може писати, тому він падає або поводиться дивно в логіці entrypoint.
  • Неправильний метод оновлення: ви створили новий кластер і підключили до нього додатки.
  • Неправильні налаштування надійності: «перемоги» по продуктивності, які тихо погоджуються на втрату даних при збоях.

Чому «в мене на ноутбуці працювало» — пастка

На ноутбуці ви можете й не помітити, що зберегли Postgres всередині шару контейнера. Ви перезапускаєте контейнер кілька разів; дані там.
Потім хтось робить docker rm або CI пересоздає контейнери, і дані зникають, бо ніколи не були в томі.

Контейнери прекрасно маскують неправильні речі. Вони охоче збережуть вашу помилку, аж поки одного дня не перестануть це робити.

Сценарії втрати даних, які ви можете відтворити (і запобігти)

Сценарій 1: Немає монтування тому → дані живуть у шарі контейнера

Класика. Ви запускаєте Postgres без персистентного монтування. Postgres пише в /var/lib/postgresql/data всередині файлової системи контейнера.
Перезапуск контейнера зберігає дані. Видалення/створення контейнера знищує їх.

Запобігання: завжди монтуйте Docker-том або bind-монт у фактичний каталог PGDATA і перевіряйте монтування командою docker inspect.

Сценарій 2: Монтовано неправильний шлях → дані потрапляють в інше місце

Офіційний образ використовує /var/lib/postgresql/data. Люди інколи монтують /var/lib/postgres або /data, бо робили так деінде.
Postgres продовжує писати в шлях за замовчуванням. Ваше змонтоване сховище лежить непомітне і пусте, як запасний парашут, залишений у літаку.

Запобігання: перевіряйте монтування контейнера і підтверджуйте, що база пише в змонтовану файлову систему. Перевірте SHOW data_directory; всередині Postgres.

Сценарій 3: Bind-монтування порожнього каталогу → запускається initdb і «створюється» новий кластер

Багато entrypoint-ів ініціалізують новий кластер, коли PGDATA виглядає порожнім. Якщо ви випадково змонтуєте абсолютно новий хост-каталог поверх реального каталогу даних,
контейнер бачить пустоту і запускає initdb. Ви тепер створили зовсім нову базу поруч з реальною, і ваш додаток підключається не туди.

Так починається «втрата даних»: спочатку «чому немає таблиці?», потім «чому ми шість годин писали в неправильний кластер?»

Сценарій 4: “docker compose down -v” та друзі → ви попросили Docker видалити вашу базу

Docker робить очищення простим. Це включає томи. Якщо ваше сховище Postgres — це іменований том у Compose, down -v його видалить.
Якщо це анонімний том, ви можете видалити його через prune, не помітивши.

Запобігання: ставтеся до тому Postgres як до стану продакшну. Захищайте його іменами, мітками та процесами. Уникайте анонімних томів для баз даних.

Сценарій 5: Запуск Postgres на overlay-сховищі → дивна продуктивність і більший ризик корупції

Змінний шар Docker зазвичай — overlay2 (або подібний). Він підходить для логів додатків. Це не те місце, де ви хочете інтенсивних випадкових I/O та fsync для бази даних.
Продуктивність стає непослідовною; з’являються скачки затримок. При збоях або тиску на диск випадки корупції стають більш ймовірними.

Запобігання: використовуйте том або bind-монт, підкріплений реальною файловою системою на хості, а не записуваним шаром контейнера.

Сценарій 6: Тюнінг надійності, що насправді включає режим втрати даних

Вимкнення fsync або встановлення synchronous_commit=off може зробити бенчмарки вражаючими.
Потім хост падає, і база втрачає останні транзакції. Це не «неочікувано». Це була домовленість.

Є законні причини послабити надійність (наприклад, епhemeral dev або певні аналітичні пайплайни, де втрата кількох секунд прийнятна).
Але для всього, що орієнтоване на користувача: не робіть цього. Postgres достатньо швидкий при розміщенні на адекватному сховищі й правильних налаштуваннях.

Жарт #1: Вимкнути fsync, щоб «прискорити Postgres», — це як зняти гальма, щоб «швидше доїхати на роботу». Короткочасно ви дійсно приїдете швидше.

Сценарій 7: WAL заповнює диск → Postgres зупиняється, і відновлення стає складним

WAL — це інтенсивне додавання в кінець. Коли диск заповнений, Postgres не може писати WAL і зупиняється. Якщо у вас також погані налаштування зберігання або відсутній моніторинг,
можна отримати завислий екземпляр і відсутність чистого шляху відновлення.

Запобігання: моніторте використання диска там, де розташовані PGDATA і WAL. Встановіть розумний max_wal_size, налаштуйте архівування, якщо потрібен PITR, і тримайте запас місця.

Сценарій 8: Невідповідність часових зон/локалі контейнера й помилки кодування → «втрата даних» через неправильне тлумачення

Не всі «втрати» — це видалення. Якщо ви ініціалізували кластер з іншою локаллю/кодуванням і потім порівнюєте дампи, ви можете побачити пошкоджені сортування,
проблеми з колацією або неправильно відображений текст. Це не втрачені байти, але виглядає як корупція.

Запобігання: явно встановлюйте локаль/кодування при ініціалізації, документуйте це і тримайте стабільним при перебудовах.

Сценарій 9: Мажорне оновлення просто зміною тега образу → створено несумісний кластер

Заміна postgres:14 на postgres:16 і перезапуск з тим самим томом не «оновлює» Postgres.
Postgres відмовиться запускатися, бо формат каталогу даних відрізняється. У стресі люди «виправляють» це видаленням тому.
Це не оновлення. Це підпал.

Запобігання: використовуйте pg_upgrade (часто найпростіше з двома контейнерами і стратегією спільного тому), або логічну реплікацію, або dump/restore — залежно від розміру і терпимості до простою.

Сценарій 10: Дрейф прав (rootless Docker, зміни UID/GID на хості) → Postgres не стартує, хтось «пересоздає» базу

Bind-монти успадковують права хоста. Зміна відображень користувачів на хості, переміщення директорій, перехід на rootless Docker або відновлення з бекапу з іншим власником —
і раптом Postgres не має доступу до своїх файлів. У паніці команди часто видаляють монт і «запускають з нуля».

Запобігання: стандартизуйте власність і використовуйте іменовані томи, коли це можливо. Якщо потрібно bind-монтувати, зафіксуйте очікувані UID/GID і тестуйте на одній родині ОС.

Швидкий план діагностики

Коли Postgres у Docker поводиться дивно, не блукайте. Почніть з трьох питань, які вирішують усе: «Де дані, чи може записуватися, і чи це стійко?»

Перший: підтвердьте, що ви дивитесь на правильний кластер

  1. Перевірте монтування PGDATA: чи каталог даних підкріплений томом/bind-монтом?
  2. Перевірте шлях каталогу даних: що Postgres показує як data_directory?
  3. Перевірте ідентифікацію кластера: подивіться system_identifier і timeline; порівняйте з очікуваним.

Другий: перевірте явний тиск на зберігання

  1. Диск заповнений: використання файлової системи хоста там, де живе том.
  2. WAL роздувся: розмір pg_wal і реплікаційні слоти.
  3. Завісання I/O: затримки і часи fsync; CPU контейнера може виглядати нормально, поки сховище помирає.

Третій: перевірте налаштованість надійності та стан аварійного відновлення

  1. Санітарність конфігів: переконайтеся, що fsync і full_page_writes не відключені для продакшну.
  2. Логи: цикли відновлення після збою, «invalid checkpoint record» або помилки прав.
  3. Події ядра/файлової системи: dmesg на предмет помилок I/O; вони часто пояснюють «випадкову» корупцію.

Як швидко знайти вузьке місце

Якщо симптом — «повільно», вирішіть, чи це CPU, пам’ять, блокування або I/O зберігання. У Docker зазвичай підозрюють саме зберігання, і логи часто це ранньо показують.
Якщо симптом — «дані відсутні», негайно припиніть запис і перевірте, чи ви не створили випадково новий кластер.

Практичні завдання: команди, виводи та рішення

Ось завдання, які я реально виконую, коли щось запахло горілим. Кожне містить (1) команду, (2) що означає вивід і (3) рішення, яке потрібно прийняти.
Припустимо, контейнер називається pg і у вас є shell-доступ до хоста.

Task 1: List containers and confirm which Postgres is running

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'
NAMES   IMAGE        STATUS          PORTS
pg      postgres:16  Up 2 hours      0.0.0.0:5432->5432/tcp

Значення: Підтверджує, який тег образу активний і чи був нещодавній рестарт. Підозріле «Up 3 minutes» часто корелює з проблемами каталогу даних.

Рішення: Якщо тег образу змінився нещодавно, вважайте оновлення головним підозрюваним. Якщо був несподіваний рестарт, переходьте безпосередньо до логів і перевірки монтувань.

Task 2: Inspect mounts and verify PGDATA is persisted

cr0x@server:~$ docker inspect pg --format '{{json .Mounts}}'
[{"Type":"volume","Name":"pgdata","Source":"/var/lib/docker/volumes/pgdata/_data","Destination":"/var/lib/postgresql/data","Driver":"local","Mode":"z","RW":true,"Propagation":""}]

Значення: Потрібно бачити монтування, яке цілиться в реальний каталог даних. Якщо немає монтування до /var/lib/postgresql/data, ваші дані в шарі контейнера.

Рішення: Якщо монтування відсутнє або вказує кудись інакше, зупиніться й виправте зберігання, перш ніж робити щось ще. Не «перезапускайте, поки працює».

Task 3: Confirm Postgres thinks its data directory is where you mounted it

cr0x@server:~$ docker exec -it pg psql -U postgres -Atc "SHOW data_directory;"
/var/lib/postgresql/data

Значення: Це має відповідати місцю монтування. Якщо не відповідає, ви пишете куди не мали.

Рішення: Якщо невідповідність, виправте змінні середовища або параметри командного рядка і переконайтеся, що очікуваний шлях PGDATA використовується скрізь.

Task 4: Check whether you accidentally initialized a new cluster (system identifier)

cr0x@server:~$ docker exec -it pg psql -U postgres -Atc "SELECT system_identifier FROM pg_control_system();"
7264851093812409912

Значення: system identifier фактично ідентифікує кластер. Якщо він змінився після деплоймента, ви не на тому ж кластері.

Рішення: Якщо ідентифікатор несподіваний, зупиніть записи додатка, знайдіть оригінальний том/bind-монт і відновіть підключення до правильного каталогу даних.

Task 5: Check container logs for initdb or permission errors

cr0x@server:~$ docker logs --since=2h pg | tail -n 30
PostgreSQL Database directory appears to contain a database; Skipping initialization
2026-01-03 10:41:07.123 UTC [1] LOG:  starting PostgreSQL 16.1 on x86_64-pc-linux-gnu
2026-01-03 10:41:07.124 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432

Значення: «Skipping initialization» — добре. Якщо ви бачите «initdb: warning» або «Database directory appears to be empty» несподівано, ви змонтували порожній каталог.
Якщо бачите «permission denied», це проблема з правами при bind-монті.

Рішення: Initdb коли ви цього не очікували — червоний прапорець. Не продовжуйте, поки не поясните, чому Postgres вважав каталог порожнім.

Task 6: Identify whether your volume is named or anonymous

cr0x@server:~$ docker volume ls
DRIVER    VOLUME NAME
local     pgdata
local     3f4c9b6a7c1b0b3e8b8d8af2c2e1d2f9d8e7c6b5a4f3e2d1c0b9a8f7e6d5

Значення: Іменовані томи (pgdata) легше захищати й посилатися на них. Анонімні томи легко втратити під час прибирання.

Рішення: Для баз даних використовуйте іменовані томи або явні bind-монти. Якщо бачите анонімні томи, мігруйте дані до іменованих перед «днем прибирання».

Task 7: See what containers are using the volume (avoid deleting the wrong one)

cr0x@server:~$ docker ps -a --filter volume=pgdata --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'
NAMES   STATUS          IMAGE
pg      Up 2 hours      postgres:16

Значення: Якщо більше одного контейнера використовує той самий том, можливо випадковий multi-writer доступ. Це ризик корупції.

Рішення: Переконайтеся, що лише один екземпляр Postgres записує у даний каталог. Якщо потрібна висока доступність, використовуйте реплікацію, а не спільне сховище з multi-writer.

Task 8: Check free space on the host filesystem backing Docker

cr0x@server:~$ df -h /var/lib/docker
Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p2  450G  410G   40G  92% /

Значення: 92% — небезпечна зона для росту WAL і піків вакууму. Бази даних не поводяться чемно, коли диски заповнюються.

Рішення: Якщо ви вище ~85–90% у продакшні, плануйте термінове очищення або розширення. Потім налаштуйте алерти і цільові запасні місця.

Task 9: Check WAL directory size inside the container

cr0x@server:~$ docker exec -it pg bash -lc 'du -sh /var/lib/postgresql/data/pg_wal'
18G	/var/lib/postgresql/data/pg_wal

Значення: Великий WAL може бути нормою при інтенсивних записах, але раптове зростання часто свідчить про застряглі реплікаційні слоти, надвеликий max_wal_size,
або архівування, яке не обробляється.

Рішення: Якщо WAL роздувається, перевірте слоти реплікації й стан архіватора негайно, перш ніж диск заповниться.

Task 10: Check replication slots (common cause of unbounded WAL)

cr0x@server:~$ docker exec -it pg psql -U postgres -x -c "SELECT slot_name, active, restart_lsn FROM pg_replication_slots;"
-[ RECORD 1 ]----------------------------
slot_name   | analytics_consumer
active      | f
restart_lsn | 0/2A3F120

Значення: Неактивний слот може утримувати WAL нескінченно, якщо споживач не просувається.

Рішення: Якщо слот не використовується — видаліть його. Якщо потрібен — виправте споживача і підтвердіть, що він просувається. Не просто «збільшити диск».

Task 11: Check archiver health if you use WAL archiving

cr0x@server:~$ docker exec -it pg psql -U postgres -x -c "SELECT archived_count, failed_count, last_archived_wal, last_failed_wal FROM pg_stat_archiver;"
-[ RECORD 1 ]-----------------------
archived_count     | 18241
failed_count       | 12
last_archived_wal  | 0000000100000000000001A3
last_failed_wal    | 0000000100000000000001A1

Значення: Збої означають, що ваш ланцюг PITR може мати прогалини. Це також означає, що WAL може накопичуватися, якщо архівування — частина вашої політики зберігання.

Рішення: Розберіться з командою архівування і сховищем. Якщо збої недавні — вважайте, що варіанти відновлення скомпрометовані, поки не доведете протилежне.

Task 12: Verify durability settings (catch accidental “benchmark mode”)

cr0x@server:~$ docker exec -it pg psql -U postgres -Atc "SHOW fsync; SHOW synchronous_commit; SHOW full_page_writes;"
on
on
on

Значення: Для продакшн OLTP це мінімум, який вам потрібен. Якщо fsync=off, ви свідомо прийняли ризик корупції при збоях.

Рішення: Якщо ці параметри вимкнені — увімкніть їх і заплануйте контрольований рестарт. Потім поясніть зацікавленим сторонам, чому попередні налаштування були небезпечні.

Task 13: Confirm the container isn’t memory-starved (OOM kills look like random crashes)

cr0x@server:~$ docker stats --no-stream pg
CONTAINER ID   NAME   CPU %   MEM USAGE / LIMIT     MEM %   NET I/O       BLOCK I/O        PIDS
a1b2c3d4e5f6   pg     85.3%   1.9GiB / 2.0GiB       95.0%   1.2GB/1.1GB   35GB/22GB       78

Значення: 95% використання пам’яті з високим CPU свідчить про тиск. Якщо хостовий OOM killer втручається, Postgres перезапускається і ви отримуєте довше відновлення.

Рішення: Збільшіть ліміти пам’яті, налаштуйте параметри пам’яті Postgres (shared_buffers, work_mem) і переконайтеся, що хост не перевантажений. Перевірте з dmesg і статистикою контейнера.

Task 14: Check for filesystem I/O errors on the host (the unglamorous truth)

cr0x@server:~$ sudo dmesg -T | tail -n 20
[Sat Jan  3 10:22:11 2026] nvme0n1: I/O 128 QID 7 timeout, aborting
[Sat Jan  3 10:22:11 2026] EXT4-fs error (device nvme0n1p2): ext4_find_entry:1459: inode #262401: comm postgres: reading directory lblock 0

Значення: Якщо ядро повідомляє про таймаути I/O або помилки файлової системи, перестаньте звинувачувати Docker. Ваше сховище виходить з ладу або поводиться неправильно.

Рішення: Обробляйте це як інцидент. Зменшіть навантаження на запис, зробіть бекап якщо можливо і плануйте відмовостійкість або заміну сховища.

Task 15: Confirm backups exist and are restorable (don’t confuse “files” with “backups”)

cr0x@server:~$ docker exec -it pg bash -lc 'ls -lh /backups | tail -n 5'
-rw-r--r-- 1 root root 1.2G Jan  3 02:00 pg_dumpall_2026-01-03.sql.gz
-rw-r--r-- 1 root root 1.1G Jan  2 02:00 pg_dumpall_2026-01-02.sql.gz

Значення: У вас є артефакти. Це не те ж саме, що перевірений відновлюваний бекап.

Рішення: Якщо не можете довести відновлення — заплануйте тест відновлення. Продакшн — не час вчитися на тому, що дампи порожні або обірвані.

Task 16: Run a quick restore smoke test into a throwaway database

cr0x@server:~$ docker exec -it pg bash -lc 'createdb -U postgres restore_smoke && gunzip -c /backups/pg_dumpall_2026-01-03.sql.gz | psql -U postgres -d restore_smoke -v ON_ERROR_STOP=1'
SET
SET
CREATE TABLE
ALTER TABLE

Значення: Якщо все виконується без помилок і створює об’єкти, ваш дамп принаймні синтаксично придатний. Це не повна валідація, але краще за інтуїцію.

Рішення: Якщо виникають помилки — припиніть вважати, що у вас є бекапи. Виправте пайплайн і повторіть, поки це не стане нудним.

Три міні-історії з корпоративного світу

Міні-історія №1: Неправильне припущення (інцидент «контейнери — це персистенція»)

Середня SaaS-команда перенесла застарілий додаток у Docker Compose, щоб локальний dev відповідав staging. Postgres опинився у тому самому Compose-файлі.
Інженер, який робив міграцію, мав добрі наміри і дедлайн — комбінація, яка дає найбільш цікаві аварії.

Вони тестували рестарти через docker restart. Дані залишалися. Вони одружилися з Всесвітом і задеплоїли той самий Compose на невелику продакшн-VM.
Сервіс Postgres не мав конфігурації тому — лише стандартна файлова система контейнера.

Через тижні вони оновили хост для патчу безпеки. Новий хост запустив стек Compose, і Postgres стартував чистим… бо він був порожнім.
Додаток також стартував чистим… і почав відтворювати таблиці, бо інструмент міграції побачив «немає схеми» і вирішив бути «допоміжним».

Реакція на інцидент була клопіткою, бо команда спочатку трактувала це як корупцію. Вони шукали проблеми з WAL і версіями ядра.
Справжня причина була простішою: вони ніколи не робили персистентний кластер. Не було «відновлення з диска». Були лише «відновлення з бекапу», і бекапи були частковими.

Коригувальна дія, що прижилась: додали іменований том, зафіксували PGDATA явним чином і написали preflight-скрипт, який відмовлявся стартувати продакшн, якщо каталог даних виглядав щойно ініціалізованим.
Цей скрипт дратував людей один раз, а потім врятував їх під час наступного деплоймента.

Міні-історія №2: Оптимізація, що зіграла назло («швидкий диск», що обманював)

Інша організація мала галасливий контейнер Postgres і великий обсяг записів. Пікові затримки з’являлися, і звичних підозрюваних звинувачували:
autovacuum, блокування, плани запитів. Вони провели певний тюнінг, отримали помірні виграші, але іноді затримки з’являлися.

Інфраструктурник запропонував перемістити директорію даних Docker на «швидшу» мережеву файлову систему, що використовувалась для артефактів.
Вона гарно бенчмаркувалася для послідовних записів і великих файлів. Postgres же використовує fsync-важкі шаблони, дрібні випадкові I/O і метадані.
Перенесення виглядало добре в синтетичних тестах і навіть у перші дні продакшну.

Потім стався перший реальний збій хоста. Коротке зависання сховища викликало I/O-попередження в логах Postgres і падіння. Після рестарту пішло довге відновлення.
Відновлення тривало значно довше, і таймаути додатка перетворилися на помітний для користувача простий.

Постмортем виявив правду: «швидка» ФС була оптимізована для пропускної здатності, а не для консистентності затримок і семантики fsync.
Їхня поведінка під навантаженням не відповідала очікуванням Postgres під час виклику fsync. Вони проміняли короткочасну продуктивність на крихкий відновлюваний стан.

Вони відкотилися на локальне SSD-сховище і зосередилися на простих виправах: правильно підібрали розмір інстансу, налаштували контрольні точки і додали репліки.
Продуктивність покращилася, але реальний виграш — стабільність: відновлення стало передбачуваним.

Міні-історія №3: Нудна, але правильна практика, що врятувала день (репетиції відновлення)

Велика внутрішня платформа запускала Postgres у контейнерах для десятків сервісів. Нічого екзотичного: томи, зафіксовані версії, пристойний моніторинг.
Частина, яка здавалася надмірною новачкам, — квартальні репетиції відновлення. Щоквартально вони відновлювали підмножину баз у ізольованому середовищі.
Перевіряли схему, кількість рядків у кількох ключових таблицях і smoke-тести додатка.

Одного дня хост зазнав відмови сховища. Первинний Postgres вмер жорстко. Репліка відставала більше, ніж хотілося, і була невизначеність щодо доступності WAL.
Ніхто не панікував — що не є рисою характеру — це процедура.

Вони переключили трафік там, де могли. Для одного сервісу довелося відновити з бекапу на новий том, бо стан реплікації був під питанням.
Відновлення було повільнішим за фейловер, але спрацювало саме так, як репетирували. Сервіс повернувся з прийнятною втратою свіжості даних, погодженою з бізнесом.

У висновку команда не мала «секретного соусу» у вигляді складного інструмента. Їхній секрет — повторюваність. Вони так часто репетирували відновлення, що реальний інцидент відчувався як трохи дратівливіший тренувальний прогін.

Поширені помилки: симптоми → корінь проблеми → вирішення

1) «Моя база скинулася після redeploy»

  • Симптоми: Порожня схема, тільки стандартні користувачі, міграції додатка стартують з нуля.
  • Корінь проблеми: Нема персистентного тому, або змонтували порожній каталог поверх PGDATA, що спричинило initdb.
  • Вирішення: Зупиніть записи, знайдіть оригінальний том/bind-монт і приєднайте його. Додайте іменований том і guard на старті, який перевіряє очікувану ідентичність кластера.

2) «Дані є, але додаток їх не бачить»

  • Симптоми: psql показує правильні дані; додаток бачить відсутні рядки/таблиці; або додаток підключається, але повертає «relation does not exist».
  • Корінь проблеми: Додаток підключається до іншого екземпляра Postgres/порту, неправильна назва бази, неточна мережа або до іншого контейнера з подібною назвою прикріплено інший том.
  • Вирішення: Підтвердіть connection string, розв’язування імен контейнерів, портмепінги і system_identifier. Чітко мітьте томи й контейнери.

3) «Postgres не стартує після оновлення»

  • Симптоми: Фатальна помилка про несумісність файлів бази з сервером.
  • Корінь проблеми: Зміна мажорної версії без pg_upgrade або dump/restore.
  • Вирішення: Відкат на попередній тег образу, щоб відновити сервіс. Плануйте реальне оновлення: pg_upgrade у контрольованому процесі або логічну міграцію.

4) «WAL росте, поки диск не заповниться»

  • Симптоми: Великий pg_wal; зростання використання диска; Postgres зупиняється.
  • Корінь проблеми: Неактивний replication slot, збої архівування або відставання репліки при політиці збереження WAL.
  • Вирішення: Ідентифікуйте слоти, видаліть невикористовувані, відновіть споживачів, полагодьте архівування і додайте алерти на ріст WAL і запас диска.

5) «Випадкові рестарти, іноді під навантаженням»

  • Симптоми: Контейнер перезапускається; логи показують раптове завершення; запити іноді падають.
  • Корінь проблеми: OOM вбиває процес через тісні ліміти пам’яті контейнера або тиск пам’яті на хості.
  • Вирішення: Збільшіть пам’ять контейнера, налаштуйте пам’ять Postgres і уникайте overcommit на хості. Підтвердіть через dmesg і stats контейнера.

6) «Ми відновили том зі сніпшота, і тепер Postgres свариться»

  • Симптоми: Відновлення після збою не проходить, відсутні сегменти WAL, помилки неконсистентності.
  • Корінь проблеми: Сніпшот на рівні сховища зроблено без координації з файловою системою/додатком; сніпшот захопив неконсистентний стан щодо WAL.
  • Вирішення: Віддавайте перевагу логічним бекапам або координованим фізичним бекапам (pg_basebackup, архівування). Якщо знімаєте сніпшот — заморожуйте I/O або використовуйте фічі ФС, призначені для crash-consistent сніпшотів, і тестуйте відновлення.

7) «Permission denied при старті»

  • Симптоми: Логи Postgres вказують permission denied у PGDATA; контейнер виходить відразу.
  • Корінь проблеми: Bind-монт власник з неправильним UID/GID; проблеми з SELinux; невідповідність rootless Docker.
  • Вирішення: Виправте власність на користувача Postgres, відрегулюйте опції монтування, розгляньте іменовані томи, щоб уникнути дрейфу прав, і стандартизуйте UID/GID.

8) «Продуктивність непередбачувана: чудово, потім жахливо»

  • Симптоми: Скачки затримок, повільні контрольні точки, зупинки autovacuum, випадкові очікування I/O.
  • Корінь проблеми: Overlay-зберігання, мережеві файлові системи, «шумні сусіди», неправильно налаштовані контрольні точки або WAL на повільному сховищі.
  • Вирішення: Помістіть PGDATA на стабільне локальне сховище, налаштуйте параметри checkpoint, моніторте часи fsync/checkpoint і ізолюйте базу від конкурентного дискового навантаження.

Контрольні списки / покроковий план

Checklist A: План «я збираюсь реально запускати Postgres у Docker»

  1. Виберіть сховище навмисно. Використовуйте іменований том або bind-монт до виділеного хостового сховища. Не покладайтеся на записуваний шар контейнера.
  2. Зафіксуйте теги образів. Використовуйте postgres:16.1 (приклад) замість postgres:latest. «Latest» — це не стратегія.
  3. Закріпіть імена томів. Називайте їх як продакшн-ресурс. Додавайте мітки, що вказують середовище і сервіс.
  4. Встановіть явний PGDATA. Тримайте його послідовним між середовищами і скриптами.
  5. Налаштуйте бекапи з першого дня. Вирішіть: логічні дампи, фізичні бекапи + архівування WAL або обидва варіанти.
  6. Тестуйте відновлення. Регулярно запускайте smoke-тести відновлення, а не в момент проблеми.
  7. Ставте алерти на диск і ріст WAL. Ви маєте знати про проблему при 70–80%, а не при 99%.
  8. Тримайте налаштування надійності за замовчуванням, якщо не можете їх захистити. Якщо змінюєте fsync-параметри — документуйте бюджет втрати даних.
  9. Плануйте оновлення як міграцію. Мажорні оновлення вимагають процедури, а не простого перемикання тега.

Checklist B: Кроки при підозрі на втрату даних

  1. Зупиніть записи. Якщо додаток пише в неправильний або свіжий кластер, кожна хвилина збільшує шкоду.
  2. Зберіть докази. Логи контейнера, вивід docker inspect, system_identifier Postgres і деталі монтувань.
  3. Ідентифікуйте правильний каталог даних. Знайдіть том/bind-монт, що містить очікуваний кластер (шукайте PG_VERSION, relation-файли і відповідний ідентифікатор).
  4. Підтвердіть статус бекапів. Який найсвіжіший відновлюваний бекап? Чи цілі архіви WAL для PITR?
  5. Відновіть сервіс безпечно. Віддавайте перевагу приєднанню правильного тому. Якщо треба відновити — робіть це в новий том і валідируйте, перш ніж переключати.
  6. Запобігайте повторенню. Додайте guards на старт, приберіть анонімні томи і захистіть продакшн-томи від prune-воркфлоу.

Checklist C: Шлях оновлення, що не зіпсує вихідні вихідні

  1. Інвентаризуйте розширення. Переконайтеся, що розширення існують і сумісні з цільовою версією Postgres.
  2. Виберіть підхід до оновлення: pg_upgrade (швидко, потребує координації) vs dump/restore (просто, може бути повільно) vs логічна реплікація (нуль/низький downtime, більше компонентів).
  3. Клонувати продакшн-дані. Використовуйте staging з реалістичними даними для репетиції оновлення.
  4. Обрати час переходу. Мати явний план відкату: старий образ + старий том залишаються недоторканими.
  5. Валідація. Запустіть smoke-тести додатка, перевірте кількість рядків у ключових таблицях і порівняйте продуктивність ключових запитів.

FAQ

1) Чи «безпечно» запускати Postgres у Docker у продакшні?

Так, якщо ви ставите зберігання, бекапи та оновлення в ранг першочергових завдань. Docker не скасовує відповідальності за базу даних; він додає нові способи її неправильно налаштувати.

2) Docker volume чи bind-монт — що обрати?

Іменовані Docker volume-ї зазвичай безпечніші операційно: менше сюрпризів з правами, легше посилатися і менше зв’язку з хост-путями.
Bind-монти підходять, коли потрібно контролювати файлову систему і знімати сніпшоти, але вони вимагають дисципліни в керуванні власністю й шляхами.

3) Чому моя база «скинулася», коли я змінив Compose-файл?

Часто тому, що змінилася назва сервісу, ім’я тому або шлях монтування, через що Docker створив новий том, або ви змонтували новий порожній хост-каталог.
Postgres тоді ініціалізував свіжий кластер.

4) Чи можна просто оновити, змінивши тег образу?

Мінорні версії: зазвичай так. Мажорні версії: ні. Мажорні оновлення вимагають pg_upgrade, dump/restore або логічної реплікації. Перемикання тега — це спосіб виявити несумісність під час виконання.

5) Який найшвидший спосіб підтвердити, що я на правильних даних?

Запитайте system_identifier, перевірте data_directory і підтвердіть монтування через docker inspect. Якщо це не збігається — нічому іншому не довіряйте.

6) Чому WAL величезний, хоча трафік нормальний?

Поширені причини: неактивний replication slot, відстаюча репліка або збої архівування. Утримання WAL — це контракт з його споживачами, а не автоматичне прибирання.

7) Чи безпечно запускати “docker system prune” на хості з Postgres-контейнерами?

Може бути, але тільки якщо ваші операційні правила суворі і ви розумієте, що буде видалено. Якщо база використовує анонімні томи або «не використовується» томи,
prune може знищити не те. Ставтеся до prune як до бензопили: корисна, але не делікатна.

8) Які налаштування надійності я ніколи не маю міняти в продакшні?

Не відключайте fsync або full_page_writes для OLTP-систем. Будьте обережні з synchronous_commit.
Якщо ви послаблюєте надійність — зафіксуйте точний вікно втрати даних і отримайте погодження.

9) Як уникнути випадкового initdb при монтуванні сховища?

Використовуйте guard на старті: перевіряйте наявність очікуваного маркера, очікуваного PG_VERSION і (бажано) очікуваного system_identifier.
Відмовляйтесь стартувати, якщо каталог виглядає щойно ініціалізованим у середовищі, де цього не повинно бути.

10) Чому продуктивність погіршується після переходу на «краще» сховище?

Багато сховищ оптимізовані під пропускну здатність, а не під консистентність затримок і семантику fsync. Бази даних карають за непостійні затримки.
Виміряйте часи fsync і checkpoint, і вибирайте сховище, що поводиться прогнозовано під навантаженням.

Кварта думки (парафраз): «У складних системах відмови нормальні; успіх вимагає постійної адаптації.» — Richard Cook, дослідник з операцій і безпеки.

Жарт #2: Єдина річ більш наполеглива, ніж Docker-том, — інженер, який наполягає, що йому не потрібні бекапи, — аж поки вони не знадобляться.

Висновок: наступні кроки, які ви можете зробити сьогодні

Якщо ви запускаєте Postgres у Docker, ваше завдання — не зробити його «контейнер-нативним». Ваше завдання — зробити його нудним. Нудне сховище. Нудні бекапи. Нудні оновлення.
Захоплива дорога — це та, що закінчується пустою схемою і довгою ніччю.

  1. Аудит монтувань: перевірте, що PGDATA на іменованому томі або навмисному bind-монті, і що Postgres показує очікуваний data_directory.
  2. Захистіть томи: ліквідуйте анонімні томи для баз і припиніть використовувати down -v у будь-якому робочому процесі, що стосується продакшну.
  3. Перевірте бекапи через відновлення: проведіть smoke-тест відновлення цього тижня, потім заплануйте його регулярно.
  4. Перевірте ризики WAL: інспектуйте replication slots і стан архіватора, і додайте алерти на ріст WAL і запас диска.
  5. Напишіть runbook оновлення: зафіксуйте версії і оберіть реальний метод для мажорних оновлень до того, як вони знадобляться.
← Попередня
NVIDIA: від RIVA до GeForce — як зародилась імперія
Наступна →
Коли ядра перемагають тактові частоти: справжня переломна точка

Залишити коментар