Files
docker-volume-backup/CHANGELOG.md

590 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
## [7.0.1] - 2026-08-18
- Restore: *--empty* no longer aborts on a database that carries an extension.
The pre-clean picked its candidates by owner, on the stated assumption that
extension members are superuser-owned and would therefore never be selected.
That holds only when a superuser installed the extension: a role that
installs one itself owns its functions, so they were listed for a one-by-one
*DROP* that postgres refuses — *cannot drop function
vector_in(cstring,oid,integer) because extension vector requires it*. Under
*ON_ERROR_STOP* that ends the whole restore, which is how a generation of an
application declaring the *vector* extension became unreplayable. Membership
now comes from *pg_depend* rather than from ownership; each branch carries
its oid and classid so one *NOT EXISTS* covers all seven instead of seven
separate predicates, and the schema branch is guarded too because an
extension can own a schema. Skipping the members suffices — the dump's
*CREATE EXTENSION IF NOT EXISTS* finds the surviving extension either way.
## [7.0.0] - 2026-08-18
Breaking:
- Backup: *mariadb* and *mysql* join the suffix tokens, so a container named
*<app>-mariadb* or *<app>-mysql* now resolves to the instance *<app>* 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.<id>*, 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.<task>* 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 — *<app>-database* from compose and *<app>_database.1.<task>* 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 *<app>-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
*<instance>.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
*<data root>/.baudolo-<tag>*, 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 *<subject>/.zfs/snapshot/<tag>*. 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 *<volume>/files/_data/...* while the live path lands at
*<volume>/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 🥳