Files
docker-volume-backup/CHANGELOG.md

32 KiB
Raw Blame History

Changelog

[7.0.0] - 2026-08-18

Breaking:

  • Backup: mariadb and mysql join the suffix tokens, so a container named -mariadb or -mysql now resolves to the instance instead of to its own name. A databases.csv keyed on the full container name has to move to the application name, or name the container in --database-containers. This narrows what 6.0.0 broke rather than widening it: a container named exactly postgres, mariadb, mysql, db or database resolves again without any declaration, which is the shape a compose file writes as container_name: postgres and the most common configuration there is.

Fixed:

  • Backup: a container named exactly after its engine is dumped again. The suffix match needs a hyphen or underscore in front of the token, which a bare name does not carry, so 6.0.0 resolved container_name: postgres to nothing and stopped dumping it without saying so. ENGINE_NAMES now states the set once and serves both readings — carried as a suffix it makes the rest the instance, being one outright makes the container its own instance.
  • Backup: a central MariaDB under swarm is dumped for the first time. Swarm names its task mariadb_mariadb.1., which matches neither the static mariadb passed through --database-containers nor any token the suffix match knew, since _mariadb is not _db. The database was silently absent from every swarm backup this tool has ever written, before 6.0.0 as well.
  • Backup: an application container is no longer recorded as a database. container_engine recognises an engine by its client tools, which an application image frequently ships, so refusing its dump alone would have written the volume to the manifest as database: true, dumped: false — the exact shape a restore drill reads as a database that was missed. Without an instance there is no database to record, and the volume is a file backup like any other.

[6.0.0] - 2026-08-18

Breaking:

  • Backup: a database container whose name carries no database, db or postgres token — preceded by a hyphen or underscore — must now be named in --database-containers. Without that declaration its databases.csv rows no longer match and no dump is written, silently, because nothing fails. The shape this hits hardest is a container named exactly postgres or mariadb: the token needs a separator in front of it, which a bare name does not have. app-database, app_database.1. from swarm and app-postgres-1 are unaffected, as is any container already declared.

Fixed:

  • Backup: docker exec now forwards PGPASSWORD into the container. execute_to_file set the variable on baudolo's own process, but nothing carried it across the container boundary, so an engine whose pg_hba demands a password on TCP loopback refused every dump — which is every dedicated Postgres instance on a real host. The name travels as a bare -e NAME so docker copies the value out of this process's environment; spelling -e NAME=value instead would publish the secret in the host's process list.
  • Backup: get_instance no longer claims an instance it never derived. It returned the container name unchanged when that name carried no database token, so an application container answered the same databases.csv row as its own dedicated engine. Application images frequently ship the engine's client tools, so the dump command started and wrote a file that looked like a backup and held none of the data: measured against Discourse, 1,680 bytes from the application where the engine produced 10,469,439. The regex stays a normaliser — -database from compose and _database.1. from swarm still resolve to one instance. Only the fallthrough changed.

New:

  • Tests: get_instance has unit coverage for the first time. Eleven cases pin the container names that compose, swarm and explicitly-named engines produce, so a future change to the regex has to state which shape it gives up.
  • Tests: two e2e modules cover shapes the suite structurally could not see. Every fixture passed its container in --database-containers, which left the regex branch — the only one a dedicated database ever takes — dead code under test, and no scenario made a password mandatory, because stock postgres:alpine grants trust on loopback. test_e2e_postgres_password_required starts an engine with --auth-host=scram-sha-256 and carries a negative control asserting the server refuses an unauthenticated dump; without it the module would pass whether or not the password is forwarded at all. test_e2e_app_container_ships_client_tools places an application container beside its engine with neither declared, and requires the engine dumped, the application volume copied as files, and no dump written from the application.

[5.0.0] - 2026-08-18

[5.0.0] - 2026-08-18

Breaking:

  • Library: BackupException is now BackupError and VersionMismatch is now VersionMismatchError. Both names violated the convention that an exception class ends in Error; the first is imported by six modules, so the rename is atomic across the package.
  • Library: backup_dumps_for_volume and backup_mariadb_or_postgres return a VolumeOutcome instead of a (bool, bool) tuple. The pair could not carry the detected engine, which the caller needs for the manifest.

New:

  • Backup: every generation carries a manifest.json stating its layout and, per volume, database (it held one), dumped (a dump was produced) and engine (which one was detected). A volume with database and no dumped was copied as raw engine files — under --only-sql that fallback is the documented behaviour, and until now nothing in the finished tree said it had happened. Restoring such a volume replays engine files instead of a dump.
  • Library: baudolo.generation states the generation layout once — files, sql, the dump suffixes, the manifest name. BackupPaths, the dump writer and the volume copier stop spelling them out separately. The module is import-free on purpose, so a consumer can read a manifest with nothing but json on a host where this package is not installed.

Changed:

  • Build: the test targets no longer depend on clean. clean is git clean -fdX ., so running the unit tests deleted every git-ignored file in the working tree. It was compensating for a missing .dockerignore, which now keeps pycache, egg-info and build output out of the image context where that belongs. clean remains available as its own target.
  • Lint: a ruff configuration is declared. The package ran on ruff's defaults while its consumer held itself to a far wider selection; measured against that selection the tree had 224 findings and now has none. Includes a full os.path to pathlib migration, with two deliberate exceptions: abspath stays where Path.resolve() would follow symlinks and let a symlinked volume test as inside the snapshot subject, and the rsync trailing separator is kept explicit where Path would drop it.

[4.0.0] - 2026-08-17

Breaking:

  • CLI: --repo-name and --databases-csv are required. One default was the literal backup-docker-to-local while its help promised the git folder name; the other pointed into the installed package, so a forgotten flag ran the whole backup silently without a single dump.
  • CLI: --dump-only-sql is now --only-sql; the old spelling exits 2.
  • CLI: --everything is withdrawn. Its only real effect was to ignore --images-no-stop-required, which leaving that list empty already does.
  • Library: the runner injected into volume_snapshot receives an argv list instead of a command string.

New:

  • Backup: --only-files — no dumps at all, every volume as files, for hosts that hold no database credentials. Needs no --databases-csv; mutually exclusive with --only-sql.
  • Backup: the engine is detected by executing the dump tool in the container (pg_dumpall, mariadb-dump, mysqldump), not by reading the image name. A dedicated Postgres tagged -database is finally dumped; an image merely named like an engine no longer kills the run with exit 127. Probed once per image ID, and an image shipping only mysqldump is dumped with it.
  • Library: baudolo.databases states the databases.csv contract once — columns, delimiter, cluster marker, validator, read_rows() — for the seed, the backup, and external consumers.

Changed:

  • Backup: every command is an argv list; shell=True is gone. A database name is validated on read as strictly as the seed writes it, PGPASSWORD travels in the child's environment instead of the command string, and a failing dump deletes its partial file instead of leaving it behind.

[3.6.1] - 2026-08-17

  • Restore: --empty on a cluster dump is a catalog-wide sweep — it drops every non-template database and every non-pg_ role of the instance. On a dedicated instance that is right, because the dump recreates all of it; on a shared one it destroys databases the dump does not carry, with nothing to restore them from. No test had ever executed that sweep: the e2e dropped the cluster by hand first and left the pre-clean with zero rows to generate.
  • Restore: the instance is checked instead of the sweep being narrowed. --empty refuses when the instance holds a database the dump does not carry, names it, and touches nothing. Narrowing the sweep is the obvious fix and is worse — a surviving database that owns or merely grants to one of the dump's roles pins it in pg_shdepend, DROP OWNED BY reaches only the control database the pre-clean is connected to, so DROP ROLE fails after the dump's own databases are already gone and the replay never starts.
  • Restore: the dump's inventory is read with a real identifier parser. A quoted name may hold spaces, and psql options precede the target of a \connect line, so a character class that stops at whitespace read "odd name" as odd and -reuse-previous=on as a database.
  • Tests: the cluster e2e no longer empties the instance itself, so --empty has to do it and the replay has to put it back. A second pass adds a foreign database and requires the refusal to leave both it and the restored data untouched.

[3.6.0] - 2026-08-17

  • Restore: --empty drops the schema in one session and replays in the next, with no rollback across the two, so a dump the engine could not parse left an emptied database behind. The dump's header is now checked against the running engine before anything is dropped, and a newer dump is refused. Forward across a major version stays allowed; --no-version-check is the way out.
  • Restore: a volume with driver options — NFS, a bind device, tmpfs — keeps the usual _data path, but docker mounts its real storage over it only while a container holds it. Restoring meanwhile landed under the mount, stayed hidden there, and rsync reported success. That volume is now refused until something mounts it.
  • Backup: the same volume sits in a snapshot as an empty directory, so it was copied empty and the generation stamped complete. Capture is decided per volume now — an uncaptured one is copied live, the rest keep their snapshot. A single NFS volume no longer costs the whole host its consistent backup.

[3.5.0] - 2026-08-17

  • Restore: a database = ''* row makes the backup write .cluster.backup.sql via pg_dumpall, and nothing could read it back — the CLI knew files, postgres and mariadb, so that dump was stored and unrestorable. baudolo-restore cluster replays it against the control database, deliberately without --single-transaction because CREATE DATABASE is forbidden inside a transaction block, and filters out the CREATE ROLE of the connecting role, which the pre-clean cannot drop while it holds the session. --empty drops the cluster's databases first, then releases what its roles still own, then the roles themselves.

  • Lint: ruff was never wired into the repository — no target, no CI step, no pin — and reported 45 findings across sources and tests. make ruff and make lint now run it over every file, make test gates on a clean run as a fourth parallel spur, and the linter is pinned in a lint extra because a minor bump changes which rules fire.

[3.4.3] - 2026-08-16

  • Backup: create_version_directory carried exist_ok=True, so a run starting in the same wall-clock second as its predecessor claimed that predecessor's generation. Generation names carry seconds, and a host with little to copy finishes inside one — rsync --delete then overwrote a finished generation, and only afterwards did create_stamp_file refuse the already-stamped directory and exit 2. The guard reported the damage instead of preventing it.
  • Backup: the generation directory is claimed exclusively. Claiming it is the first filesystem action of a run, so a collision aborts before the first write and names the second it collided on.
  • Tests: the idempotence test asserted the reuse and gave way to one that requires the refusal.

[3.4.2] - 2026-08-15

  • Backup: has_image matched the raw .Config.Image, so the registry host and the tag decided the dump tool. On a swarm node named after the app under test, svc-db-mariadb-swarm-mgr-01:5000/postgres_custom read as MariaDB and mariadb-dump ran inside a Postgres container: exit 127, and the BackupException took the backup unit with it.
  • Backup: image_name strips digest, tag and registry host, so the engine rests on the repository path alone — the exact-matching intent of 3.0.0 applied to the one place that change did not reach.
  • Tests: both false-positive directions on has_image, plus an e2e that reproduces the shape with a docker tag and asserts a real pg_dump lands.

[3.4.1] - 2026-08-05

  • Backup: each volume is copied twice into the same destination — once hot, once cold after the container is stopped — and rsync ran with -b, so --delete renamed rather than removed a file the source had dropped between the passes. Stopping a container is what makes the source drop files: a graceful shutdown flushes and the format rolls its commit point. The superseded file survived as name~ beside the real one and was restored into live data.
  • Backup: for an opaque payload that 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~ leaves the shard store unreadable and the primary at NO_VALID_SHARD_COPY. With .security-7 unallocatable the reserved elastic user has no password hash, every probe answers 401 and the container never turns healthy.
  • Backup: --link-dest already provides the incrementals and nothing reads the twins — the restore path is an unfiltered rsync -avv --delete into the live volume. Dropping -b leaves the predecessor generation byte-identical, keeps the hardlinks intact and makes generations smaller, never larger.
  • Tests: the absence of --backup is asserted on the rsync invocation.

[3.4.0] - 2026-08-02

  • Backup: -a implies -D, so a generation was written with --devices --specials and rsync recreated every unix socket and fifo found in a volume. Where the backup root is an nfs-ganesha export, ganesha accepts the socket on write but cannot serve it back, and the remote pull's sender then fails with readdir / readlink_stat "Invalid argument (22)" and exits 23 — deterministically, for every retry. --no-D keeps them out of the generation.
  • Backup: nothing restorable is lost. Sockets and fifos are recreated by the daemons that own them, and the postfix queue itself — incoming, active, deferred, hold, maildrop — is unaffected, so accepted-but-undelivered mail stays in the backup. Device nodes go too; the only volume that could hold them is a nested docker data root, which does not belong in a backup anyway.
  • Tests: the flag is asserted on the rsync invocation.

[3.3.0] - 2026-08-02

  • Backup: --volumes-no-backup-required excludes a volume by name. --images-no-backup-required resolves through volume_is_fully_ignored, which skips a volume only when every container using it is ignored — a container holding a derived tree beside state that must be kept cannot express the exclusion at all. A docker-in-docker data root is exactly that shape, and excluding by image would drop all three of its volumes.
  • Backup: the name check runs before containers_using_volume, so an excluded volume costs no docker inspection and the decision does not depend on which containers exist when the run starts.
  • Tests: two volumes off one container, asserting the sibling survives — the property the image lever cannot provide — as unit and end-to-end.

[3.2.2] - 2026-07-31

  • Backup: the btrfs snapshot is carved inside its subject, as /.baudolo-, not beside it. The kernel refuses a snapshot whose destination is on another filesystem, which is exactly what the parent directory is when the data root is a mountpoint of its own — a dedicated disk mounted onto /var/lib/docker failed every run with EXDEV. Placing it inside makes source and destination the same filesystem by construction, and aligns btrfs with the zfs path, which already resolves its snapshot inside the subject at /.zfs/snapshot/. A leftover from an interrupted run appears in the next snapshot as an empty directory rather than recursing, since btrfs does not include nested subvolumes.

[3.2.1] - 2026-07-31

  • Backup: the snapshot resolver keeps the trailing separator get_storage_path puts on a volume path — os.path.abspath stripped it. rsync reads dir as "copy the directory" where dir/ means "copy its contents", so every snapshot generation landed at /files/_data/... while the live path lands at /files/.... Restores read the live layout, and --link-dest had nothing to match against the previous generation.
  • Backup: snapshot teardown no longer fails a completed run. A busy btrfs subvolume delete raised out of the finally, skipping the generation stamp and the compose handling on a run whose data was already copied, and masking whatever the body had raised. The leftover is reported instead.
  • Backup: a volume created after the snapshot was taken is copied live with a warning instead of aborting the run. Nothing is stopped in snapshot mode, so the host keeps creating volumes for the whole copy.
  • Backup: the snapshot pass compares by content (--checksum) again. 3.2.0 dropped it because a snapshot source cannot move, which is true, but the comparison that matters is against --link-dest: a file that changed while keeping its size and whole-second mtime was hard-linked stale out of the previous generation, and the single pass had no authoritative pass to repair it. Still one pass where the live path takes two.
  • Backup: --hard-restart-projects is refused alongside --snapshot, like --shutdown already is. It exists for stacks whose database cannot be backed up hot, which is what a snapshot removes.
  • Tests: the trailing separator, both teardown behaviours, the new refusal, and app.main driving the snapshot branch — the caller that runs in production, which no test had exercised, which is why the layout defect shipped.

[3.2.0] - 2026-07-31

  • Backup: --snapshot {btrfs,zfs} with --snapshot-subject captures every volume from one atomic filesystem snapshot. The subject — the btrfs subvolume or zfs dataset holding the docker volumes, e.g. /var/lib/docker — is frozen once per run, so a generation shares a single point in time and no container is stopped. Restoring such a copy is an ordinary crash recovery, which every supported engine performs at startup. This is the mode 3.1.4 pointed to for volumes where two rsync passes over a live tree stop being affordable.
  • Backup: snapshot passes copy once and drop --checksum. Verification exists because a hot pass writes its destination from a moving source; a snapshot source cannot move, so size and mtime cannot race and the second full read is pure cost.
  • Backup: an unsupported filesystem or unknown kind fails with SnapshotError. The kind is stated, not probed — an inconclusive probe would fall back to a live copy and hand out the torn backup the mode prevents. --shutdown is rejected alongside --snapshot rather than ignored, since nothing is stopped.
  • Refactor: backup/app.py splits into layout.py, policy.py and dumps.py along the lines it already had; 276 lines down to 124.
  • Tests: unit coverage for snapshot, layout, policy, volume and CLI validation. E2E cases drive real btrfs, zfs and ext4 on loop devices, one proving the loud refusal; another writes a MariaDB across the snapshot and requires the restored server to recover on its own with every committed row and none of the later ones. CI installs zfs and sets E2E_REQUIRE_FILESYSTEMS, so a missing kernel module fails the build instead of skipping a filesystem.

[3.1.4] - 2026-07-31

  • Backup: the post-stop volume pass now compares by content (--checksum), so it can no longer skip a file the hot pass had copied from a live source. Each volume is rsynced twice into the same destination — once with the container running, once after it is stopped — and the second pass used rsync's quick check (size plus whole-second mtime). A pre-allocated 16 MiB WAL segment never changes size, so when its last pre-stop write and postgres' shutdown checkpoint fell in the same whole second, the cold pass skipped it while still replacing global/pg_control, whose previous write was seconds earlier. The generation then paired a post-shutdown pg_control with a WAL segment still zeroed at the recorded checkpoint LSN, and restoring it crash-looped postgres with "invalid record length … expected at least 24, got 0" followed by "PANIC: could not locate a valid checkpoint record". backup_volume takes authoritative as a required keyword rather than an optional flag, so each of the four call sites states which pass it is; the hot passes keep the quick check. --ignore-times was rejected because it destroys the --link-dest hardlink dedup (measured 20/20 to 0/20, generation size doubled), and --modify-window=-1 because it makes correctness depend on the destination filesystem preserving sub-second mtimes, which degrades silently on NFS or ext3.
  • Cost: the quick check scales with file count, --checksum with bytes read on both sides, and it runs while the container is stopped. The extra stop time stays under a minute up to roughly 4 GB on spinning disk, 15 GB on a SATA SSD and 60 GB on NVMe; at 1 TB it is 17 minutes on NVMe and over three hours on spinning disk. No size threshold is built in, since a guessed one would drop the guarantee exactly where an unrestorable backup costs most — volumes at that scale want filesystem snapshots or pg_basebackup rather than two rsync passes over a live tree.

[3.1.3] - 2026-07-20

  • Restore: the postgres dump replay now runs under --single-transaction, so a concurrent writer on a live database can no longer interleave a row between the replay's table re-create and its COPY and trip a "duplicate key value violates unique constraint" abort under ON_ERROR_STOP. This is the discourse restore-drill race (mini_scheduler upserting scheduler_stats(id=1) mid-restore) that failed the whole restore. The --empty pre-clean stays multi-statement (\gexec, one DROP per statement) because a single DROP transaction exhausts max_locks_per_transaction on large schemas (e.g. gitlab).
  • Refactor: the --empty pre-clean SQL moves out of the inline Python string into src/baudolo/restore/db/empty_preclean.sql (loaded via dirname(file), declared as package-data so it ships in the wheel).
  • Tests: a unit test guards the single-transaction / multi-statement split (replay carries --single-transaction, pre-clean does not); a new e2e reproduces the live-writer race and asserts the restore survives it.

[3.1.2] - 2026-07-18

  • Restore: the postgres --empty pre-clean also drops user-owned text search configurations and dictionaries (pg_ts_config, pg_ts_dict), so a schema shipping a custom dictionary (e.g. taiga's english_stem_nostop) no longer aborts the replay with "duplicate key value violates unique constraint pg_ts_dict_dictname_index" under ON_ERROR_STOP.
  • Tests: the string-assertion unit test for the pre-clean SQL is replaced by real scenario data in the e2e: the seeded schema contains an overloaded f()/f(int) pair and the nostop dictionary plus configuration, and the restored database is queried to prove each survives the backup, pre-clean and replay cycle exactly once.

[3.1.1] - 2026-07-17

  • Restore: the postgres --empty pre-clean drops functions and procedures by their identity signature (pg_get_function_identity_arguments), so a schema that overloads a function name (e.g. discourse) no longer aborts the replay with "function name is not unique" under ON_ERROR_STOP. Identifier quoting moves from the outer DROP format into each object branch, since the name(args) compound must not be quoted as a whole; a unit test pins the per-branch %I quoting so future branches cannot regress unquoted.

[3.1.0] - 2026-07-15

  • Restore: the postgres --empty pre-clean emits one DROP per object and runs them via \gexec instead of a single DO-block, so large schemas (e.g. gitlab) no longer exhaust max_locks_per_transaction in one transaction. It also drops user-owned non-public schemas, so dumps that CREATE SCHEMA (e.g. discourse's discourse_functions) no longer abort on the already-existing schema under ON_ERROR_STOP.
  • Backup: --database-containers and --images-no-stop-required are now optional and default to an empty list, so a pure file backup needs no dummy arguments; an empty stop whitelist keeps the conservative stop-all behavior.
  • Tests: new e2e test restores --empty against a fully populated database containing a non-public schema and every dropped object class. make test runs the three suites concurrently after a single clean+build; E2E_TEST_PATTERN runs an e2e subset.

[3.0.0] - 2026-07-12

  • Backup: --images-no-stop-required and --images-no-backup-required now match a container's exact .Config.Image (full repo:tag, registry prefix included) instead of a substring, so a near-miss image name no longer flips the stop/skip decision. Callers must pass exact image references. Breaking.
  • Backup: renamed --hard-compose-restart to --hard-restart-projects (its value stays a list of compose project dir names). Breaking: the old flag name is removed.

[2.0.0] - 2026-07-12

  • Backup: renamed --docker-compose-hard-restart-required to --hard-compose-restart and changed its default from ["mailu"] to [] (nargs="*"). The compose down/up is now opt-in: compose hosts pass mailu explicitly, while swarm hosts pass nothing, since there the dir is a stack whose overlay network collides with compose up. Breaking: the old flag name is removed and the implicit mailu default is gone.
  • Backup: --backups-dir is now required (no /var/lib/backup/ default) so a run can never silently target the wrong backup root. Breaking.
  • Restore: volume files are rsynced directly into the target volume's mountpoint (resolved via docker volume inspect), mirroring the backup path; the alpine-rsync helper image and the --rsync-image flag are gone. The caller needs write access to the docker volume root (root on the host, baudolo's normal privilege). Breaking: the restore files subcommand no longer accepts --rsync-image.
  • Tests: the e2e suite tracks postgres:alpine (18+, mounted at /var/lib/postgresql) and mariadb:latest from a single source of truth.

[1.8.1] - 2026-07-12

  • Restore: the postgres empty mode also drops user-owned collations in public; dumps containing CREATE COLLATION (e.g. OpenProject's ICU collation versions_name) no longer abort the replay with 'collation already exists'.
  • Maintenance: base image bumped from python 3.11-slim to 3.14-slim.

[1.8.0] - 2026-07-11

Swarm-aware backups and replayable restores.

  • Backup: swarm task containers are never stopped or started manually anymore; they are skipped visibly and backed up hot, while the sql dump stays the consistent database backup.
  • Backup: a container that vanishes between listing and inspect no longer aborts the run; a failing inspect on a container that still exists keeps failing loudly.
  • Backup: pg_dump runs with the no-owner and no-privileges flags so dumps are replayable by the owning app user.
  • Restore: the mariadb empty mode drops all tables in one client session with FOREIGN_KEY_CHECKS disabled; FK-linked parent tables no longer abort the replay with ERROR 1451.
  • Restore: the postgres empty mode drops only current-user-owned objects, and the replay skips superuser-only dump lines without ever touching COPY data blocks.
  • Restore: the replay streams the dump through a temp file instead of buffering it in memory; multi-GB dumps no longer OOM the restore.
  • Tooling: the e2e runner reaches the DinD daemon via docker exec instead of a host-published unencrypted API port.
  • Tooling: new end-to-end test reproducing the swarm stop flake, plus unit tests for the restore filters and the swarm probes; the suite is 36 unit, 9 integration and 30 e2e tests.
  • Tooling: Dependabot with auto-merge for minor and patch updates.

[1.7.1] - 2026-05-26

  • 🔌 MariaDB SQL backups now connect over TCP loopback so the dump always matches the same wildcard-host grant the application uses — no more surprise ERROR 1045 Access denied when a localhost-bound auth row preempts.
  • 🧪 New regression and bug-repro tests pin the TCP behaviour and prove it under the exact preemption setup that caused the production failure on MariaDB 12.
  • 🩺 E2E test infrastructure: DinD bridge and inner daemon now default to MTU 1280 so registry pulls survive host paths with broken PMTUD (override via E2E_DIND_MTU).

[1.7.0] - 2026-02-07

  • 🚀 Backup jobs now support all valid Docker Compose file names case-insensitive and hassle-free.

[1.6.0] - 2026-02-06

  • Compose handling is now fully delegated to the Infinito.Nexus compose wrapper or plain docker compose, removing all custom env and file detection to ensure a single, consistent source of truth.

[1.5.0] - 2026-01-31

    • Make databases.csv optional: missing or empty files now emit warnings and no longer break backups
  • Fix Docker CLI compatibility by switching to docker-ce-cli and required build tools

[1.4.0] - 2026-01-31

  • Baudolo now restarts Docker Compose stacks in a wrapper-aware way (with a docker compose fallback), ensuring that all Compose overrides and env files are applied identically to the Infinito.Nexus workflow.

[1.3.0] - 2026-01-10

  • Empty databases.csv no longer causes baudolo-seed to fail

[1.2.0] - 2025-12-29

    • Introduced --dump-only-sql mode for reliable, SQL-only database backups (replaces --dump-only).
  • Database configuration in databases.csv is now strict and explicit (* or concrete database name only).
  • PostgreSQL cluster backups are supported via *.
  • SQL dumps are written atomically to avoid corrupted or empty files.
  • Backups are smarter and faster: ignored volumes are skipped early, file backups run only when needed.
  • Improved reliability through expanded end-to-end tests and safer defaults.

[1.1.1] - 2025-12-28

    • Backup: In --dump-only-sql mode, fall back to file backups with a warning when no database dump can be produced (e.g. missing databases.csv entry).

[1.1.0] - 2025-12-28

    • Backup: Log a warning and skip database dumps when no databases.csv entry is present instead of raising an exception; introduce module-level logging and apply formatting cleanups across backup/restore code and tests.
  • CLI: Switch to an FHS-compliant default backup directory (/var/lib/backup) and use a stable default repository name instead of dynamic detection.
  • Maintenance: Update mirror configuration and ignore generated .egg-info files.

[1.0.0] - 2025-12-27

  • Official Release 🥳