База в контейнере — это файлы в Docker volume, и бэкап у неё такой же, как у базы на обычном сервере: дамп средствами самой СУБД. Отличия — в деталях: как правильно вызвать pg_dump через docker exec, чтобы не испортить файл, где взять пароль внутри контейнера и что делать с SQLite, которая лежит в volume приложения. Ниже — команды для PostgreSQL, MySQL и SQLite, расписание и встроенные бэкапы Coolify.
Почему нельзя просто скопировать volume
Самая частая ошибка — tar или cp -r каталога /var/lib/docker/volumes/... при работающей базе. PostgreSQL и MySQL пишут в несколько файлов одновременно и держат часть изменений в памяти и журналах. Копирование идёт файл за файлом несколько секунд или минут, и за это время база меняет уже скопированные файлы. На выходе — набор файлов из разных моментов времени, который при запуске может не подняться или подняться с повреждёнными таблицами.
Копировать файлы данных можно только в двух случаях:
- Контейнер с базой остановлен.
- Вы делаете атомарный снимок файловой системы (LVM, ZFS, снапшот диска у облачного провайдера) — тогда база при старте пройдёт обычное восстановление после сбоя. Но это отдельная тема со своими оговорками.
Во всех остальных случаях делайте логический дамп: pg_dump, mysqldump, sqlite3 .backup.
docker exec: -t ломает дамп
Флаг -t выделяет псевдотерминал. Через терминал переводы строк превращаются в \r\n, а сообщения из stderr смешиваются с выводом. Для текстового SQL это даёт файл с лишними \r и предупреждениями посередине, а бинарный дамп (pg_dump -Fc) становится нечитаемым. Правила:
- Выгрузка из контейнера (вывод в файл на хосте):
docker execбез-t,docker compose exec -T. Важно:docker compose execпо умолчанию выделяет TTY, поэтому-Tтам обязателен. - Загрузка в контейнер (дамп на stdin):
docker exec -iилиdocker compose exec -T.
PostgreSQL в контейнере
docker compose exec -T db pg_dump -U app -d app --format=plain > app-$(date -u +%Y-%m-%dT%H%M%SZ).sql
В официальном образе postgres подключения через локальный сокет внутри контейнера обычно не требуют пароля, поэтому пароль в команде не нужен. Если в вашем образе это не так, задайте PGPASSWORD через -e из переменной окружения хоста, а не строкой в скрипте.
Плюс такого подхода: pg_dump из образа совпадает по версии с сервером, и проблема «клиент старее сервера» исчезает. Для сжатия и проверки ошибок используйте конвейер с set -o pipefail:
#!/usr/bin/env bash
set -euo pipefail
cd /opt/myapp
OUT="/var/backups/myapp/app-$(date -u +%Y-%m-%dT%H%M%SZ).sql.gz"
docker compose exec -T db pg_dump -U app -d app --format=plain | gzip > "$OUT.part"
mv "$OUT.part" "$OUT"
Роли кластера в дамп одной базы не попадают — их выгружает docker compose exec -T db pg_dumpall -U app --globals-only. Подробно о форматах, pipefail, загрузке в Yandex Object Storage и Selectel — в статье про бэкап PostgreSQL через pg_dump в S3.
MySQL и MariaDB в контейнере
Пароль root в официальных образах лежит в переменной окружения контейнера. Главная ловушка — кавычки:
# правильно: одинарные кавычки, переменная раскрывается внутри контейнера
docker compose exec -T db sh -c 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" \
--single-transaction --routines --triggers --events --hex-blob --no-tablespaces app' > app.sql
С двойными кавычками $MYSQL_ROOT_PASSWORD раскроется на хосте, где переменной нет, и mysqldump запросит пароль или упадёт.
Пароль в -p виден в списке процессов внутри контейнера. Альтернатива — MYSQL_PWD:
docker compose exec -T db sh -c 'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysqldump -uroot \
--single-transaction --routines --triggers --events --hex-blob --no-tablespaces app' > app.sql
Документация MySQL считает MYSQL_PWD небезопасной: окружение процесса можно прочитать. Здесь это мало что меняет — тот же пароль уже лежит в окружении контейнера и виден через docker inspect. Надёжнее завести отдельного пользователя только с правами на чтение и смонтировать в контейнер файл [client] с правами 600 для --defaults-extra-file. В образе mariadb используйте mariadb-dump и переменную MARIADB_ROOT_PASSWORD.
Почему важны --single-transaction, --routines и --events и как настроить ротацию копий, описано в статье бэкап MySQL: автоматизация mysqldump.
SQLite в volume приложения
SQLite — это файл внутри volume приложения, отдельного контейнера с базой нет. Простой cp файла при работающем приложении небезопасен, особенно в режиме WAL: часть транзакций ещё лежит в файле -wal. Нужен онлайн-бэкап через .backup или VACUUM INTO.
Если в образе приложения есть sqlite3:
docker compose exec -T app sqlite3 /data/app.db ".backup '/data/app-backup.db'"
docker compose cp app:/data/app-backup.db ./app-$(date +%F).db
Если sqlite3 в образе нет, запустите временный контейнер с тем же volume:
docker volume ls # имя volume в compose обычно с префиксом проекта: myapp_app_data
docker run --rm \
-v myapp_app_data:/data \
-v /var/backups/myapp:/backup \
alpine sh -c 'apk add --no-cache sqlite >/dev/null && sqlite3 /data/app.db ".backup /backup/app-$(date +%F).db"'
Блокировки SQLite работают между процессами на одном хосте, так что приложение продолжит работать, пока идёт копирование. После бэкапа полезно проверить копию: sqlite3 app-2026-10-03.db "PRAGMA integrity_check;". Подробно про .backup, VACUUM INTO и WAL — в статье как сделать бэкап SQLite.
Tar volume — только при остановленном контейнере
Иногда нужен именно файловый бэкап volume, например перед обновлением мажорной версии. Порядок:
docker compose stop db
docker run --rm \
-v myapp_pgdata:/source:ro \
-v /var/backups/myapp:/backup \
alpine tar czf /backup/pgdata-$(date +%F).tar.gz -C /source .
docker compose start db
Такой архив восстанавливается только в образ той же мажорной версии СУБД. Для регулярных бэкапов он не подходит: каждый раз нужна остановка базы.
Расписание: cron на хосте или sidecar
Самый простой вариант — скрипт с docker compose exec -T и cron или systemd timer на хосте:
# crontab -e
15 3 * * * flock -n /tmp/myapp-backup.lock /opt/myapp/backup.sh >> /var/log/myapp-backup.log 2>&1
Если доступа к cron хоста нет или хочется держать всё в compose.yaml, добавьте отдельный сервис-sidecar на базе того же образа postgres — в нём уже есть pg_dump нужной версии:
services:
db-backup:
image: postgres:17
depends_on:
- db
env_file: .env.backup # PGHOST=db, PGUSER, PGDATABASE, PGPASSWORD
volumes:
- ./backups:/backups
entrypoint: ["bash", "-c"]
command:
- |
set -o pipefail
while true; do
f=/backups/app-$$(date -u +%Y-%m-%dT%H%M%SZ).sql.gz
pg_dump --format=plain | gzip > "$$f.part" && mv "$$f.part" "$$f" || echo "backup failed"
find /backups -name 'app-*.sql.gz' -mtime +7 -delete
sleep 86400
done
$$ нужен, чтобы Compose не подставлял переменные сам. У цикла со sleep время запуска «плывёт» после каждого перезапуска контейнера. Если нужно точное расписание, используйте внутри sidecar планировщик для контейнеров, например supercronic: соберите свой образ на базе postgres и скачайте бинарник из официальных релизов проекта. PGPASSWORD в .env.backup виден через docker inspect — выдайте этому пользователю только права на чтение.
Coolify: встроенные бэкапы и Scheduled Tasks
Если базы развёрнуты как ресурсы Coolify, проще всего воспользоваться встроенными бэкапами. Для PostgreSQL, MySQL, MariaDB, MongoDB и ClickHouse на странице базы есть раздел Backups, где можно добавить расписание (cron-выражение или daily, weekly и т. п.), задать хранение по количеству копий, сроку или объёму и включить отправку в S3. Хранилище сначала добавляется и проверяется в настройках S3 Storages — подойдёт и Yandex Object Storage, и Selectel. Локальные копии по умолчанию остаются на сервере; их можно отключить после успешной загрузки в S3. Для Redis этот механизм не предусмотрен. Названия пунктов меню меняются от версии к версии, поэтому сверяйтесь с документацией Coolify для вашей версии.
SQLite внутри приложения Coolify не видит как базу. Для неё есть Scheduled Tasks в настройках приложения: команда с cron-расписанием выполняется в shell внутри работающего контейнера. Если в образе приложения есть sqlite3, задача может выглядеть так (каталог для копий должен существовать и лежать в постоянном хранилище):
sqlite3 /data/app.db ".backup '/data/backups/app-latest.db'"
Дальше копию нужно вывезти с сервера — например, скриптом, который отправит файл в S3 из того же контейнера, если там есть нужный клиент. Задачу сначала запустите вручную и проверьте результат. Выбор между Coolify и другими PaaS — в обзоре аналогов Railway в России.
Копия вне сервера и восстановление
Бэкап, который лежит на том же диске, что и база, не спасёт от удалённого сервера, сбоя диска или ошибки в docker volume prune. Минимум одна копия должна уезжать в S3 или на другую машину.
Восстановление в контейнер — тот же конвейер в обратную сторону, с -T или -i:
# PostgreSQL
gunzip -c app.sql.gz | docker compose exec -T db psql -U app -d app -v ON_ERROR_STOP=1
# MySQL
docker compose exec -T db sh -c 'exec mysql -uroot -p"$MYSQL_ROOT_PASSWORD" app' < app.sql
# SQLite: остановить приложение, подменить файл, убрать старые -wal/-shm
docker compose stop app
docker run --rm -v myapp_app_data:/data -v /var/backups/myapp:/backup alpine \
sh -c 'cp /backup/app-2026-10-03.db /data/app.db && rm -f /data/app.db-wal /data/app.db-shm'
docker compose start app
Восстанавливайте PostgreSQL в пустую базу, иначе psql упадёт на уже существующих объектах. Остальные сценарии описаны в статье как восстановить базу из бэкапа, а регулярная проверка копий — в материале как проверить, что бэкап рабочий.
Как это сделать в dbsend
dbsend не подключается к контейнерам и не запускает дампы: вы делаете дамп командами выше, а CLI загружает файл. Для SQLite CLI сам делает согласованную копию через VACUUM INTO, поэтому ему достаточно пути к файлу — например, к каталогу volume, смонтированному на хосте.
npm install --global @dbsend/sdk
export DBSEND_API_KEY='sqv_pk_…'
docker compose exec -T db pg_dump -U app -d app --format=plain > /var/backups/myapp/app.sql
dbsend backup /var/backups/myapp/app.sql -d "$DBSEND_DATABASE_ID" -e postgresql_dump -l "nightly-$(date +%F)"
dbsend backup /srv/myapp/data/app.db -d "$DBSEND_SQLITE_ID" -e sqlite
Каждая загрузка становится версией. Для SQLite и plain-дампов PostgreSQL/MySQL видны схема и число строк, а между любыми двумя версиями доступен diff. Если бэкап не пришёл по расписанию, приходит уведомление в Telegram, на почту или в webhook. Хранение — S3 в России с шифрованием AES-256-GCM. Diff и уведомления доступны на платных тарифах, подробности на странице тарифов. Команды CLI описаны в документации, возможности для Postgres — на странице бэкапов PostgreSQL.
Частые вопросы
Можно ли делать бэкап Docker volume через tar при работающей базе?
Для PostgreSQL и MySQL — нет: файлы копируются в разные моменты времени, и база может не подняться из такой копии. Либо останавливайте контейнер, либо делайте логический дамп через pg_dump или mysqldump.
Почему дамп через docker exec получился битым?
Скорее всего, вы использовали docker exec -it или docker compose exec без -T. Псевдотерминал меняет переводы строк и подмешивает stderr в вывод. Для выгрузки используйте docker exec без -t или docker compose exec -T.
Нужен ли отдельный бэкап, если Coolify уже делает бэкапы базы?
Встроенных бэкапов Coolify достаточно, если они уходят в S3 вне сервера и вы проверили восстановление. SQLite внутри приложения в эти бэкапы не входит — для неё настройте Scheduled Task или cron на хосте.
Где хранить бэкапы базы из Docker?
Не на том же сервере и тем более не в соседнем volume. Отправляйте копии в S3 (Yandex Object Storage, Selectel) или на другую машину, а на сервере держите несколько последних копий для быстрого отката.
Каким pg_dump делать бэкап — с хоста или из контейнера?
Из контейнера с базой или из sidecar на том же образе: версия pg_dump гарантированно совпадёт с сервером. pg_dump с хоста тоже подойдёт, если его версия не старее сервера и порт базы доступен.



