Скрипты для бекапа и восстановления Docker образов

Предпосылки к созданию
Я уже давно и прочно подсел на идеологию self-hosted. То один, то другой сервис неожиданно отказывают в доступе с территории РФ. Конечно, есть способы получить этот самый доступ, но это вызывает некоторые неудобства. Да и перефразируя классика, полбеды в том, что сервисы смертны - беда в том, что они могут быть внезапно смертны. Я помню, как накрылся звездою Google Reader, царство ему небесное, Springnote, ICQ, Skype, пачка сервисов Yahoo и другие, помню, как закончилось место на Google Photo, как были предпосылки к закрытию фигмы и Miro на территории РФ и т.п., поэтому стараюсь быть улиточкой и жить по принципу "Всё своё носи с собой". Если кто-то может что-то заблокировать, то нужно исходить из предположения, что это рано или поздно будет заблокировано. Сервисов уже много, в кубер пока не донёс, но стараюсь придерживаться принципа KISS - в /opt директории с сервисами, у каждого сервиса свой docker-compose и локальные директории, которые мапятся в качестве вольюмов. Т.е. переносится всё при необходимости не просто, а очень просто - сворачивается архив, копируется на другую машину, разворачивается, docker compose up -d и сервис жив. Я стараюсь в композах не использовать latest или использую при старте, а потом меняю на текущую версию. А старые версии образов иногда могут и перестать существовать в репозиториях или репозиторий вообще в целом может стать недоступен. Тут вариантов 2 - либо разворачивать свой репозиторий docker образов (и да, такой тоже имеется), либо делать бекапы образов и складывать рядом в поддиректорию backups. Два этих варианта никак не исключают друг-друга, но когда проект самодостаточен, мне спится спокойнее. Собственно, чтобы автоматизировать процесс локального резервного копирования я и написал данные скрипты. И да, виртуальные машины с сервисами тоже регулярно бекапятся.
Для чего ещё это может быть нужно?
Типичные ситуации, когда это может пригодиться:
-
Офлайн-развертывание. Часто нужно перенести собранный стенд на сервер, у которого нет доступа в интернет. Раньше я иногда бодался с зависимостями, теперь просто бекаплю образы на машине со сборкой и разворачиваю их на целевом сервере.
-
Бэкап перед обновлением. Перед обновлением
docker-composeсервисов я создаю бэкап используемых образов. Если что-то идет не так —restore_images -fв течение минуты возвращает всё к исходному состоянию. -
Освобождение места. Бывает, что нужно удалить локальные образы, чтобы освободить место на диске, но при этом сохранить возможность быстро их восстановить. Скрипты делают эту задачу тривиальной.
Как это работает?
В основе работы лежат стандартные команды Docker: docker save для создания архива и docker load для восстановления. Однако скрипты обрастают удобной оберткой, которая берет на себя всю "рутину".
Скрипт бэкапа (backup_images)
Если запустить его без аргументов в директории с docker-compose.yml, он:
-
Проверяет, какая команда доступна для работы с Compose:
docker compose(новая версия) илиdocker-compose(устаревшая). -
Выполняет команду
$COMPOSE_CMD config --images, чтобы получить список используемых образов. -
Для каждого образа создает архив с именем вида
<image_name>_<tag>_<timestamp>.tar.gz, где все слэши в имени образа заменены на подчеркивания. -
Сохраняет список всех созданных архивов в файл
backup_manifest.txt.
Скрипт восстановления (restore_images)
Эта часть, на мой взгляд, получилась даже интереснее, так как я постарался сделать её максимально безопасной. При восстановлении скрипт:
-
Определяет имя образа из имени файла, игнорируя путь, по которому этот файл лежит. Это крайне удобно, так как позволяет хранить архивы в любых папках.
-
Выполняет пре-чек (pre-flight check): перед тем как начать восстанавливать что-либо, скрипт проверяет все указанные для восстановления образы. Если хотя бы один из них используется запущенным контейнером, процесс восстановления прерывается, а пользователь видит список конфликтующих образов и контейнеров. Это полностью исключает ситуацию, когда вы случайно "поломаете" работающий сервис.
-
Если образ не используется, скрипт проверяет, существует ли он локально. Если да — спрашивает, нужно ли его заменить (при запуске без ключа
-f) или просто заменяет его без вопросов (с ключом-f). -
После успешного восстановления выводит список только что восстановленных образов.
Примеры из жизни
Давайте посмотрим, как это выглядит на практике. У меня есть проект espcan с двумя сервисами: platformio и espconnect. Вот как происходит бэкап и восстановление.
Создание бэкапа
evgeniy@ligthcoder:~/projects/espcan/backups$ ./backup_images
=== Docker Image Backup Tool ===
Auto-detecting images using 'docker compose config --images'...
Found images:
ruseler/espconnect:latest
shaguarger/platformio:latest
Output directory: ./backups_20260802_192015
Images to backup:
ruseler/espconnect:latest
shaguarger/platformio:latest
Backing up ruseler/espconnect:latest to ruseler_espconnect_latest_20260802_192015.tar.gz...
[OK] Successfully backed up (25M)
Backing up shaguarger/platformio:latest to shaguarger_platformio_latest_20260802_192021.tar.gz...
[OK] Successfully backed up (376M)
=========================================
Backup completed
Successful: 2 images
=========================================
Backup files saved in: ./backups_20260802_192015
Manifest:
./backups_20260802_192015/ruseler_espconnect_latest_20260802_192015.tar.gz
./backups_20260802_192015/shaguarger_platformio_latest_20260802_192021.tar.gz
Как видите, всё очень прозрачно: скрипт нашел образы, создал для них директорию с таймстемпом и сгенерировал архив.
Восстановление образа (интерактивный режим)
Если образ уже существует локально, скрипт спросит, что делать:
evgeniy@ligthcoder:~/projects/espcan/backups$ ./restore_images backups_20260802_193802/ruseler_espconnect_latest_20260802_193802.tar.gz
=== Docker Image Restore Tool ===
Checking for conflicts...
No conflicts found, proceeding with restore...
Restoring ruseler/espconnect:latest from backups_20260802_193802/ruseler_espconnect_latest_20260802_193802.tar.gz...
[WARN] Image ruseler/espconnect:latest already exists. Replace? (y/N): y
Removing existing image...
Untagged: ruseler/espconnect:latest
Deleted: sha256:fa2387995dc8988049fbdb29fa7aeb6c25b0d2c10051b0e35d924fc00fc82d13
Loading image (32M)...
Loaded image: ruseler/espconnect:latest
[OK] Successfully restored ruseler/espconnect:latest
=========================================
Restore completed
Restored: 1 images
=========================================
Restored images:
ruseler/espconnect:latest (33.7MB)
Восстановление с принудительной заменой (-f)
Если вы уверены в своих действиях, можно пропустить вопрос, добавив флаг -f:
evgeniy@ligthcoder:~/projects/espcan/backups$ ./restore_images -f backups_20260802_193802/ruseler_espconnect_latest_20260802_193802.tar.gz
=== Docker Image Restore Tool ===
Checking for conflicts...
No conflicts found, proceeding with restore...
Restoring ruseler/espconnect:latest from backups_20260802_193802/ruseler_espconnect_latest_20260802_193802.tar.gz...
[WARN] Image ruseler/espconnect:latest already exists, removing (forced)...
Untagged: ruseler/espconnect:latest
Deleted: sha256:fa2387995dc8988049fbdb29fa7aeb6c25b0d2c10051b0e35d924fc00fc82d13
Loading image (32M)...
Loaded image: ruseler/espconnect:latest
[OK] Successfully restored ruseler/espconnect:latest
=========================================
Restore completed
Restored: 1 images
=========================================
Restored images:
ruseler/espconnect:latest (33.7MB)
Ключевые особенности
Я постарался, чтобы скрипты обладали следующими важными качествами:
-
POSIX-совместимость. Скрипты написаны на чистом
shи не используютbash-специфичных конструкций. Это гарантирует их работу в любом окружении, будь то Alpine, BusyBox или минимальная система, где нетbash. Для меня это было принципиально, так как я часто работаю с легковесными контейнерами (например, lxc c alpine в proxmox). Также я не любитель unicode смайликов, пиктограммочек и прочей мешуры, как и русского языка в комментариях и выводе, ибо не факт, что в вашей среде будут нужные шрифты, SHELL поддерживает подсветку и есть русская локаль. -
Универсальность. Скрипты не привязаны к конкретному проекту. Вы можете запустить
backup_imagesв любой директории, где естьdocker-compose.yml, или просто передать список образов в качестве аргументов. -
Безопасность восстановления. Как я уже упоминал, встроенная проверка на использование образов запущенными контейнерами — это, на мой взгляд, одна из главных "фишек". Она спасает от необдуманных действий и предотвращает простои сервисов.
-
Информативность. Скрипты выводят только нужную информацию. Они не используют ANSI-цвета, чтобы не засорять вывод "мусором" в системах без поддержки цветов. Вместо цветов используются простые маркеры
[OK],[WARN],[FAIL], которые понятны в любом терминале.
Заключение
В итоге получился небольшой инструмент, который я теперь периодически использую. Он сделал процесс работы с Docker-образами проще и быстрее. Далее можно автоматизировать - например, поиск с помощью find на глубину 1 в /opt, вход в каждую директорию и бекап по крону, очистка старых бекапов и т.п.. И до этого тоже руки дойдут, я думаю.
Исходный код скриптов доступен в репозитории на GitHub: alive-corpse/docker_images_backups. Если у вас есть идеи по улучшению или вы нашли баг — буду рад вашим issue и pull request'ам. Всем добра.
Теги: docker, shell, instruments
