Стратегія резервного копіювання пошти: консистентні бекапи Maildir, бази користувачів і відновлення окремої скриньки
Бекап пошти ламається не тоді, коли ви його робите, а коли намагаєтесь відновити. Розбираємо, чому наївний rsync живого Maildir дає дублікати й зламаний UIDVALIDITY, як зняти консистентний зріз трьох незалежних станів і як повернути ОДНУ скриньку, не зачепивши решту 2000 акаунтів.
EvilMail Team18 липня 2026 р.12 хв читання
Чому «бекап є, а відновити не можна» — три стани, які треба зловити разом
Класична сцена. О третій ночі спрацював cron, rsync -a /var/mail/vhosts backup:/ відпрацював без помилок, лог зелений, ви спите спокійно. Через тиждень користувач зносить теку «Клієнти», ви розгортаєте вчорашню копію — і IMAP-клієнт замість повернутих листів показує 4000 дублікатів, а Thunderbird починає перекачувати всю скриньку заново. Бекап був. Відновлення — ні.
Причина проста і болюча: rsync копіює файли, а поштова скринька — це не папка з файлами. Це три незалежні стани, які мають бути взаємно консистентними на один момент часу:
Файли Maildir — самі листи в new/ і cur/
Бекап поштового сервера: Maildir, dsync і відновлення однієї скриньки — EvilMail Blog
.
Метадані Dovecot — dovecot-uidlist, dovecot.index*, subscriptions, dovecot-keywords, maildirfolder. Саме тут живе відповідність «файл ↔ IMAP UID».
Реляційна база `mailserver` — таблиці virtual_domains, virtual_users, virtual_aliases: хеші паролів, квоти, мапінг адрес на каталоги.
rsync живого Maildir ловить лист у процесі переїзду з tmp/ у new/: файл у копії вже є, а рядка в dovecot-uidlist ще немає (або навпаки — uidlist оновився, файл не долетів). При restore Dovecot бачить неузгоджений uidlist, вирішує, що це чужа скринька, і присвоює нові UID під новим UIDVALIDITY. Для IMAP-клієнта це сигнал «скринька перестворена» — стягуй усе наново. Звідси дублікати й трафік.
Далі — як зняти всі три стани узгоджено і, головне, як відновити один акаунт, не зачепивши інші.
Anatomy Maildir: чому цей формат прощає копіювання (і де все одно кусає)
Maildir спроєктований під atomic rename, і це його головна перевага для бекапу. Кожен лист — окремий файл, який ніколи не редагується in-place. MTA спершу пише в tmp/ під тимчасовим ім'ям, а потім робить rename() у new/. rename() у межах одного тому — атомарна операція, тож у new/ файл або є цілим, або його немає. Пів-листа не буває. Коли клієнт вперше бачить повідомлення, Dovecot переносить його з new/ у cur/ і дописує суфікс прапорців.
Ім'я файлу несе стан: 1720099200.M12345P6789.mx1:2,S. Після :2, — прапорці одним рядком: P (passed/переслано), R (replied/відповіли), S (seen/прочитано), T (trashed/у кошику), D (draft/чернетка), F (flagged/позначено). Позначити лист прочитаним — це не запис усередину файлу, а rename з додаванням S до суфікса. Тому копія, зроблена посеред такого rename, може мати файл зі старим ім'ям, а dovecot.index — уже з новим станом.
Де саме кусає:
`dovecot-uidlist` — плоский файл відповідності «UID ↔ ім'я файлу». rsync може зловити його напівоновленим.
`UIDVALIDITY` — 32-бітне число на теку. IMAP-клієнт кешує пару (UIDVALIDITY, UID). Якщо UIDVALIDITY змінилося, клієнт зобов'язаний за протоколом викинути весь локальний кеш і перезавантажити теку. Збереження цього числа — і є різниця між «тихим» restore і бурею дублікатів.
Прапорці в суфіксах легко розходяться з dovecot-keywords та index, якщо копіювати файли й метадані окремими проходами.
Висновок для бекапу: окремий лист завжди цілий, а от набір метаданих — це живий стан, який не можна фотографувати покадрово різними інструментами.
dsync проти rsync: правильний інструмент для реплікації скриньок
doveadm backup і doveadm sync (двигун dsync) розуміють семантику Maildir + index. Вони синхронізують на рівні листів і прапорців, а не байтів файлів: зберігають UID, UIDVALIDITY, keywords, не плодять дублікатів навіть на скриньці, куди прямо зараз падає пошта. dsync читає стан через ту саму бібліотеку, що й сам Dovecot, тож бачить консистентну картину, а не файлову «фотографію на ходу».
bash
# Бекап однієї скриньки в Maildir на бекап-хості
doveadm backup -u [email protected] \
maildir:/backup/vhosts/evilmail.pro/user
# Бекап усіх скриньок разом: %d = домен, %n = локальна частина
doveadm backup -A maildir:/backup/%d/%n
# Інкрементальна дельта (тільки зміни з минулого разу)
doveadm sync -u [email protected] \
maildir:/backup/vhosts/evilmail.pro/user
Різниця backup і sync: backup — односторонній, робить приймач точною копією джерела (зайве на приймачі видаляє). sync — двонаправлений merge. Для нічного бекапу беремо backup, для вливання в живу скриньку — sync (про це нижче).
Критерій
rsync -a
doveadm (dsync)
UID / UIDVALIDITY
губить, генерує нові
зберігає
Дублікати при restore
так, масово
ні
Робота на живій скриньці
небезпечно
штатно
Швидкість інкременту
сканує все дерево
бачить лише дельту index
Keywords / прапорці
розходяться
синхронізує
Чесно: rsync не «поганий» узагалі — він валідний для холодного повного знімка, коли Dovecot зупинений, або поверх атомарного снапшота ФС. Саме цей нюанс і веде до наступного розділу.
Консистентний знімок без зупинки сервісу
Гасити Postfix і Dovecot щоночі ніхто не буде. Рішення — зняти два узгоджені зрізи майже в один момент: дамп бази без блокувань і атомарний снапшот тому з поштою.
База mailserver на InnoDB дампиться консистентно без зупинки прийому пошти:
--single-transaction відкриває одну транзакцію з consistent snapshot — усі таблиці бачаться на один момент, без LOCK TABLES, доставка не завмирає. Але це працює лише для InnoDB. Якщо хоч одна таблиця на MyISAM — --single-transaction її не захистить, потрібен --lock-tables, а це вже блокування. Перевірте рушій через SHOW TABLE STATUS перед тим, як довіритися прапорцю.
Файли й метадані Dovecot фіксуємо снапшотом ФС у ту саму секунду:
Снапшот атомарний — він фотографує /var/mail/vhosts разом з dovecot-uidlist та index на один момент. Далі з примонтованого снапшота ганяємо dsync або навіть rsync (тут файли вже «холодні», не рухаються) на off-site.
Порядок важливий, але дрейф прощенний. Робіть дамп бази, потім снапшот ФС. Якщо між ними хтось створить акаунт, ви отримаєте каталог без запису в SQL — некритично: скринька ще порожня, наступний бекап її підхопить. Зворотна ситуація — запис у SQL без каталогу — теж нестрашна, Dovecot створить Maildir при першій доставці. Небезпечний лише зворотний порядок при видаленні, тому видалення акаунтів робіть окремою процедурою, а не покладайтесь на дрейф.
Відновлення однієї скриньки, не зачепивши інші 2000
Заради цього все й будувалося. Сценарій: [email protected] зніс теку «Клієнти», треба повернути стан на вчора. Решта 2000 акаунтів працюють — їх не можна навіть торкнутися.
Спокуса зробити cp -a /backup/.../user /var/mail/vhosts/evilmail.pro/user — і це рівно та помилка, що породжує тікети. cp -a поверх живого Maildir змішує вчорашній index із сьогоднішніми файлами, Dovecot ловить розсинхрон, форсує reindex, скидає прапорці seen і ламає UIDVALIDITY. Клієнт качає все наново.
Правильно — merge через dsync прямо в цільову скриньку:
bash
# Дістаємо ТІЛЬКИ цю скриньку з бекапа (снапшот/окремий шлях)
# і мерджимо в живий Maildir через ту саму бібліотеку Dovecot.
doveadm sync -u [email protected] \
maildir:/backup/vhosts/evilmail.pro/user
# Права ОБОВ'ЯЗКОВО: інакше Dovecot отримає Permission denied
chown -R 5000:5000 /var/mail/vhosts/evilmail.pro/user
doveadm sync зіллє листи з бекапа з поточним станом: повернені повідомлення приїдуть зі збереженими UID, а нові листи, що прийшли після бекапа, залишаться на місці. Жодного затирання. Забутий chown 5000:5000 — друга за популярністю причина «скринька порожня після restore»: файли є, але Dovecot під uid 5000 їх не читає.
Якщо зник сам акаунт (рядок у virtual_users), не робіть restore всієї таблиці — це затре всі акаунти, створені після бекапа. Точковий INSERT:
domain_id 53 — це evilmail.pro, 54 — evilmail.cloud. Схема пароля — PLAIN, як налаштовано в Dovecot.
Фінальна валідація — переконатися, що UIDVALIDITY не змінився і кількість листів збіглася:
bash
doveadm mailbox status -u [email protected] \
'messages uidvalidity' '*'
Ротація, off-site і перевірка, що бекап живий
GFS-схема інкрементів dsync: 7 щоденних / 4 тижневих / 6 місячних. Off-site копія — обов'язково шифрованим каналом; бекап поштової бази з хешами паролів на незашифрованому чужому диску — це інцидент, що чекає нагоди.
Але головне не ротація, а restore-drills. Бекап без регулярного тестового відновлення — це кіт Шредінгера: він одночасно робочий і зламаний, поки ви не відкрили коробку в момент аварії. Ритуал: раз на місяць піднімайте випадкову скриньку з бекапа в ізольований Dovecot-інстанс і звіряйте лічильник через doveadm search або doveadm mailbox status. Збіглося — бекап живий.
Що моніторити автоматично:
Вік останнього успішного dsync — alert, якщо > 26 годин (добовий цикл плюс запас).
Розмір інкременту — аномально малий інкремент означає, що щось не синкнулось, а лог усе одно зелений.
Дельта SQL ↔ ФС — SELECT COUNT(*) FROM virtual_users проти кількості каталогів у /var/mail/vhosts/*/. Розбіжність більша за кілька свіжостворених акаунтів — сигнал, що бекап або створення акаунтів десь розходяться.
Чек-лист: перед тим, як назвати бекап пошти «готовим»
dsync, не rsync, для гарячих скриньок; rsync — лише поверх снапшота або зупиненого Dovecot.
`UIDVALIDITY` зберігається — перевірено через doveadm mailbox status.
`mysqldump --single-transaction`, рушій таблиць підтверджено як InnoDB.
Снапшот ФС і дамп БД зняті узгоджено за часом, порядок задокументований.
Off-site копія шифрована.
Restore-drill пройдено цього місяця в ізольованому інстансі.
Права `5000:5000` прописані в процедурі restore.
Вік останнього успішного бекапа моніториться (alert > 26 год).
Є документована процедура відновлення ОДНІЄЇ скриньки через doveadm sync, а не cp -a.
Дельта SQL ↔ каталоги vhosts під контролем.
Бекап, який проходить усі десять пунктів, — це не «папка на іншому диску», а відтворюваний стан пошти, який ви вже вміли повертати до того, як він знадобився насправді.