Kevin Veen-BirkenbachandClaude Opus 5 cd21f1fa67 fix(backup): keep no twin of what the cold pass replaces
Each volume is copied twice into the same destination: once hot with the
container running, once cold after it is stopped (backup/app.py:127-131).
rsync ran with -b, so --delete did not remove a file the source had dropped
between the passes - it renamed it. Stopping a container is exactly what
makes the source drop files: a graceful shutdown flushes, and the format
rolls its commit point.

For an opaque payload the twin is stale bytes nobody reads. For a format
that enumerates its own directory it is corruption. Lucene resolves the
current commit by parsing every file starting with segments as a radix-36
generation, so a restored segments_3~ raises NumberFormatException, the
shard store cannot be read, and the primary is left NO_VALID_SHARD_COPY.
With .security-7 unallocatable the reserved elastic user has no password
hash, every probe gets HTTP 401, and the container never turns healthy.

Seen in infinito-nexus-core CI run 30963648828: the restore drill waited
1200s on elasticsearch while all 29 other containers came back healthy;
the generation held segments_3~ next to segments_4 in all three index
directories. A run two days earlier passed the same drill because that
stack was idle and nothing rolled between the passes - which is why this
reads as flaky rather than broken.

Reproduced with real rsync in all three shapes: two passes into the same
destination with -b leave segments_3~ beside segments_4, without -b only
segments_4 survives, and a single pass keeps segments_3. Measured on the
same fixtures, dropping -b leaves the predecessor generation byte-identical
and keeps the --link-dest hardlinks intact, so the incremental scheme is
unaffected; generations get smaller, never larger.

--link-dest already provides the cheap incrementals. -b contributed nothing
on top of it but the twins, and no caller reads them: the restore path is an
unfiltered rsync -avv --delete into the live volume (restore/files.py:35).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 12:43:08 +02:00
2026-08-02 12:07:34 +02:00
2020-10-11 11:54:16 +02:00
2025-12-27 12:49:24 +01:00
2026-08-02 12:07:34 +02:00

baudolo – Deterministic Backup & Restore for Docker Volumes 📦🔄

GitHub Sponsors Patreon Buy Me a Coffee PayPal License: AGPL v3 Docker Version Python Version GitHub stars

baudolo is a backup and restore system for Docker volumes with mandatory file backups and explicit, deterministic database dumps. It is designed for environments with many Docker services where:

  • file-level backups must always exist
  • database dumps must be intentional, predictable, and auditable

✨ Key Features

  • 📦 Incremental Docker volume backups using rsync --link-dest
  • 🗄 Optional SQL dumps for:
    • PostgreSQL
    • MariaDB / MySQL
  • 🌱 Explicit database definition for SQL backups (no auto-discovery)
  • 🧾 Backup integrity stamping via dirval (Python API)
  • ⏸ Automatic container stop/start when required for consistency
  • 🚫 Whitelisting of containers that do not require stopping
  • ♻️ Modular, maintainable Python architecture

🧠 Core Concept (Important!)

baudolo separates file backups from database dumps.

  • Docker volumes are always backed up at file level
  • SQL dumps are created only for explicitly defined databases

This results in the following behavior:

Database defined File backup SQL dump
No ✔ yes ✘ no
Yes ✔ yes ✔ yes

📁 Backup Layout

Backups are stored in a deterministic, fully nested structure:

<backups-dir>/
└── <machine-hash>/
    └── <repo-name>/
        └── <timestamp>/
            └── <volume-name>/
                ├── files/
                └── sql/
                    └── <database>.backup.sql

Meaning of each level

  • <machine-hash> SHA256 hash of /etc/machine-id (host separation)

  • <repo-name> Logical backup namespace (project / stack)

  • <timestamp> Backup generation (YYYYMMDDHHMMSS)

  • <volume-name> Docker volume name

  • files/ Incremental file backup (rsync)

  • sql/ Optional SQL dumps (only for defined databases)

🚀 Installation

Local (editable install)

python3 -m venv .venv
source .venv/bin/activate
pip install -e .

🌱 Database Definition (SQL Backup Scope)

How SQL backups are defined

baudolo creates SQL dumps only for databases that are explicitly defined via configuration (e.g. a databases definition file or seeding step).

If a database is not defined:

  • its Docker volume is still backed up (files)
  • no SQL dump is created

No database definition → file backup only Database definition present → file backup + SQL dump

Why explicit definition?

baudolo does not inspect running containers to guess databases.

Databases must be explicitly defined to guarantee:

  • deterministic backups
  • predictable restore behavior
  • reproducible environments
  • zero accidental production data exposure

Required database metadata

Each database definition provides:

  • database instance (container or logical instance)
  • database name
  • database user
  • database password

This information is used by baudolo to execute pg_dump, pg_dumpall, or mariadb-dump.

💾 Running a Backup

baudolo \
  --compose-dir /srv/docker \
  --databases-csv /etc/baudolo/databases.csv \
  --database-containers central-postgres central-mariadb \
  --images-no-stop-required alpine postgres mariadb mysql \
  --images-no-backup-required redis busybox

Common Backup Flags

Flag Description
--everything Always stop containers and re-run rsync
--dump-only-sql Skip file backups only for DB volumes when dumps succeed; non-DB volumes are still backed up; fallback to files if no dump.
--shutdown Do not restart containers after backup
--backups-dir Backup root directory (default: /Backups)
--repo-name Backup namespace under machine hash

♻️ Restore Operations

Restore Volume Files

baudolo-restore files \
  my-volume \
  <machine-hash> \
  <version> \
  --backups-dir /Backups \
  --repo-name my-repo

Restore into a different target volume:

baudolo-restore files \
  target-volume \
  <machine-hash> \
  <version> \
  --source-volume source-volume

Restore PostgreSQL

baudolo-restore postgres \
  my-volume \
  <machine-hash> \
  <version> \
  --container postgres \
  --db-name appdb \
  --db-password secret \
  --empty

Restore MariaDB / MySQL

baudolo-restore mariadb \
  my-volume \
  <machine-hash> \
  <version> \
  --container mariadb \
  --db-name shopdb \
  --db-password secret \
  --empty

baudolo automatically detects whether mariadb or mysql is available inside the container

🔍 Backup Scheme

The backup mechanism uses incremental backups with rsync and stamps directories with a unique hash. For more details on the backup scheme, check out this blog post.
Backup Scheme

👨‍💻 Author

Kevin Veen-Birkenbach

📜 License

This project is licensed under the GNU Affero General Public License v3.0. See the LICENSE file for details.

🔗 More Information


Happy Backing Up! 🚀🔐

S
Description
Backup Docker Volumes to Local is a comprehensive solution that leverages rsync to create incremental backups of Docker volumes, providing seamless recovery for both file and database data. Ideal for ensuring the integrity and security of your container data.
Readme AGPL-3.0
943 KiB
0 Stars 1 Watchers 0 Forks
Languages
Python 97%
Shell 2%
Makefile 0.7%
Dockerfile 0.3%