• v0.10.2 37d70786c3

    v0.10.2
    Some checks failed
    CI / Build / Publish / Shellcheck (push) Successful in 2s
    CI / Build / Publish / Validate CHANGELOG structure (push) Successful in 3s
    CI / Build / Publish / Bats unit tests (push) Successful in 11s
    CI / Build / Publish / PR build & smoke test (push) Has been skipped
    CI / Build / Publish / Host-driven integration (entrypoint + storage sidecars) (push) Successful in 2m7s
    CI / Build / Publish / Round-trip + scheduler smoke (push) Successful in 3m22s
    CI / Build / Publish / Build multi-arch and push (push) Successful in 2m28s
    CI / Build / Publish / Reconcile releases across registries (push) Successful in 1m23s
    CI / Build / Publish / Workflow summary (push) Failing after 0s
    Stable

    Cody Bryant released this 2026-07-21 20:11:18 -07:00 | 0 commits to main since this release

    Changed

    • The automatic recovery kit is no longer written as a locked-down root:600 file. It is already encrypted and lives inside the datastore, so on local and SFTP storage targets it now takes the same ownership and permissions as that storage's own Duplicacy files (matched from the storage's config file on local targets; inherited from the connecting user's umask on SFTP) rather than being a permissions outlier a backup or mirror user cannot read. Object-store targets (B2/S3) are unaffected — they have no POSIX permissions. Existing deployments correct their already-placed kit automatically on the first backup (or archiver recovery-kit) run after upgrade: the kit's placement-scheme version is folded into the change-detection fingerprint, so the one-time re-stamp fires even though the kit content is unchanged — no recovery-kit force needed.

    Dependencies

    • chore(deps): update actions/checkout action to v7.0.1
    • chore(deps): update dependency moby/moby to v29.6.2
    Downloads
  • v0.10.1 d1dc19a068

    v0.10.1
    All checks were successful
    CI / Build / Publish / Shellcheck (push) Successful in 2s
    CI / Build / Publish / Validate CHANGELOG structure (push) Successful in 1s
    CI / Build / Publish / PR build & smoke test (push) Has been skipped
    CI / Build / Publish / Round-trip + scheduler smoke (push) Successful in 3m14s
    CI / Build / Publish / Host-driven integration (entrypoint + storage sidecars) (push) Successful in 1m55s
    CI / Build / Publish / Build multi-arch and push (push) Successful in 2m1s
    CI / Build / Publish / Reconcile releases across registries (push) Successful in 54s
    CI / Build / Publish / Bats unit tests (push) Successful in 6s
    CI / Build / Publish / Workflow summary (push) Successful in 1s
    Stable

    Cody Bryant released this 2026-07-12 13:48:32 -07:00 | 5 commits to main since this release

    Changed

    • The backup and maintenance pipelines no longer serialize on per-storage locks — they run concurrently, relying on Duplicacy's own lock-free design. Its two-step fossil collection makes a non-exclusive check/prune safe alongside a copy reading or writing the same storage (verified against the Duplicacy v3.2.5 source: a copy reads a mid-prune fossil via a .fsl fallback and writes the destination snapshot last, so a concurrent non-exclusive prune can at worst make a copy abort loudly — never corrupt a storage, delete referenced data, or leave a partial secondary). The practical win: maintenance now runs on its own schedule without waiting for an in-progress copy to finish (previously a copy held the primary storage for its whole duration, so maintenance serialized behind it), and a copy never waits on maintenance. A copy leg that loses that rare race is retried once automatically before being reported as an error. The per-storage lock files (/var/lock/archiver-storage-*) and their healthcheck reporting are removed. Note: this relies on the maintenance pipeline never using prune -exclusive (it uses non-exclusive prune, and -exhaustive — which is safe — never implies -exclusive).
    Downloads
  • v0.10.0 764421f636

    v0.10.0
    All checks were successful
    CI / Build / Publish / Shellcheck (push) Successful in 2s
    CI / Build / Publish / Validate CHANGELOG structure (push) Successful in 1s
    CI / Build / Publish / PR build & smoke test (push) Has been skipped
    CI / Build / Publish / Round-trip + scheduler smoke (push) Successful in 4m0s
    CI / Build / Publish / Workflow summary (push) Successful in 1s
    CI / Build / Publish / Bats unit tests (push) Successful in 6s
    CI / Build / Publish / Host-driven integration (entrypoint + storage sidecars) (push) Successful in 1m51s
    CI / Build / Publish / Build multi-arch and push (push) Successful in 2m6s
    CI / Build / Publish / Reconcile releases across registries (push) Successful in 55s
    Stable

    Cody Bryant released this 2026-07-12 11:49:44 -07:00 | 9 commits to main since this release

    Breaking

    • Backups and maintenance are now two independent pipelines. The backup run is hooks → backup → copies, nothing else; storage check and prune moved to a separate maintenance pipeline with its own schedule, lock, and log (maintenance.log). Nightly-cost consequence: the multi-hour storage-wide chunk listings that used to run inside every backup (check on every storage plus prune -exhaustive on every storage — up to three full sftp/B2 enumerations per storage per night) are gone from the backup path entirely; secondaries are verified by the copy itself (its destination enumeration re-uploads anything missing, healing instead of just reporting). The two pipelines coordinate through per-storage locks, so a check/prune never overlaps a copy on the same storage (the copy-vs-prune concurrency duplicacy does not document as safe is excluded structurally); the waiting side logs who holds the storage. A maintenance overrun can never delay the next backup.
    • CRON_SCHEDULE was renamed to BACKUP_SCHEDULE, and the new MAINTENANCE_SCHEDULE drives the maintenance pipeline (unset = maintenance only runs via archiver maintenance). A container started with CRON_SCHEDULE set fails fast with a rename message — silently ignoring it would mean no scheduled backups. The scheduled crontab now invokes the synchronous verbs (archiver backup / archiver maintenance), so supercronic sees real job durations and adds its own overlap-skip on top of the pipeline locks. A scheduled deployment without MAINTENANCE_SCHEDULE gets a prominent startup warning, and healthcheck warns when any storage has gone more than 8 days without a successful check or prune.
    • ROTATE_BACKUPS was renamed to PRUNE_BACKUPS (old name still translated silently for old bundle configs, with a startup warning when set as an env var), joined by CHECK_BACKUPS (default true) so shared-storage deployments can disable either phase independently, and PRUNE_EXHAUSTIVE_FREQUENCY (off/daily/weekly/monthly, default monthly): the expensive -exhaustive chunk-listing garbage collection now runs on its own interval per storage instead of on every prune, tracked in a state file and fired belatedly at the next maintenance run if the schedule is sparser than the frequency. archiver maintenance exhaustive forces it.
    • archiver start and archiver restart were removed (they error with guidance): archiver backup --detach is the background form, with the visible already-running refusal. The prune|retain arguments to backup were removed likewise. archiver stop takes a target: backup, maintenance, or all (default).

    Added

    • archiver maintenance [exhaustive] — the maintenance pipeline verb: per storage, check -all -fossils -resurrect (when CHECK_BACKUPS) then prune -all with the retention policy (when PRUNE_BACKUPS), recording per-storage last-success timestamps that archiver status displays and healthcheck monitors. Also available to external schedulers as run maintenance (synchronous, exit code propagates).
    • Secondary copies now run in parallel. Each duplicacy copy to a secondary is launched concurrently (still direct children, so pause/stop keep working); on an upload-capped link one storage's bandwidth-free destination enumeration overlaps another's upload, so the copy phase approaches max(legs) instead of sum(legs).
    • Phase durations in the logs: each service backup, each copy, and each check/prune logs how long it took, so slow nights are diagnosable from the summary instead of by archaeology.

    Deprecated

    • Bundle mode remains supported through 0.10.x.

    Fixed

    • archiver backup --detach (the background form, formerly archiver start) now refuses visibly when a backup is already running (non-zero exit and the same "A backup is already running (PID …)" message the synchronous path prints). Previously the dispatcher backgrounded main.sh with its output discarded and printed the success line unconditionally, so a refused start looked like a successful one. A stale lock (dead PID) still starts normally.
    • The copy phase now holds the primary storage's lock (as source) for its whole duration, not just each destination's lock, so a maintenance prune can no longer fossilize chunks a concurrent copy is still reading from the primary — closing the one copy-vs-prune overlap the per-storage locks had left open.
    • The container clears any leftover pipeline lock and stop-flag files at startup, so a lock left by an unclean prior shutdown can neither fake a live holder (which would refuse every scheduled backup after a PID recycle) nor abort the first backup via a stale stop flag. A stale pipeline lock now reports as a healthcheck warning (self-healing, reaped on the next run) rather than flipping the container UNHEALTHY.

    Dependencies

    • chore(deps): update docker/login-action action to v4.4.0
    Downloads
  • v0.9.2 b402ac4270

    v0.9.2
    All checks were successful
    CI / Build / Publish / Shellcheck (push) Successful in 2s
    CI / Build / Publish / Validate CHANGELOG structure (push) Successful in 2s
    CI / Build / Publish / PR build & smoke test (push) Has been skipped
    CI / Build / Publish / Bats unit tests (push) Successful in 6s
    CI / Build / Publish / Host-driven integration (entrypoint + storage sidecars) (push) Successful in 1m50s
    CI / Build / Publish / Round-trip + scheduler smoke (push) Successful in 3m15s
    CI / Build / Publish / Build multi-arch and push (push) Successful in 5m0s
    CI / Build / Publish / Reconcile releases across registries (push) Successful in 1m17s
    CI / Build / Publish / Workflow summary (push) Successful in 1s
    Stable

    Cody Bryant released this 2026-07-10 20:23:33 -07:00 | 17 commits to main since this release

    Added

    • Automatic recovery kit: with the recovery_password secret present (at /run/secrets/recovery_password or via RECOVERY_PASSWORD_FILE), after each backup archiver assembles a kit holding the full effective configuration (every non-secret setting, every secret, and the keys — exactly what archiver migrate emits), generated recreation notes (RECREATE.txt: hostname, schedule, every container path that needs a mount, required capabilities), and the deployment manifests when mounted read-only at /opt/archiver/deployment (a compose bind, a Kubernetes ConfigMap — e.g. a kustomize configMapGenerator under Flux/Argo — or a NixOS bind; captured verbatim), encrypts it to that password (AES-256, pbkdf2), and uploads it as a plain file beside the duplicacy data on every storage target (archiver-recovery-kit-<hostname>.tar.enc plus a companion README with the decrypt command). archiver init now generates the recovery password, includes it in the emitted materials, and displays it once for the user's password manager, and the shipped compose template mounts both the secret and its own compose.yaml — so new deployments maintain a kit by default; existing deployments enable it with openssl rand -base64 24 > secrets/recovery_password plus a secrets: entry (archiver migrate prints this hint when unconfigured). Disaster recovery needs only one reachable storage location — even just a provider web UI — plus the single recovery password: openssl enc -d -aes-256-cbc -pbkdf2 -in <file> | tar -xvf - on any machine yields archiver.env + secrets/ + RECREATE.txt (+ deployment/), everything required to recreate the deployment (the recovery password itself is included, so the recreated deployment maintains its kit immediately). The kit is re-uploaded only when its content actually changes or a target is missing it; the fingerprint deliberately ignores runtime state (for example the prune|retain rotation override), so an unchanged config is a nightly no-op. archiver recovery-kit pushes on demand and archiver recovery-kit force re-pushes everywhere. Upload failures are logged and notified but never fail the backup, and failed targets are retried on the next run. The recovery password must be at least 8 characters and differ from STORAGE_PASSWORD (it is the only thing protecting the kit at rest, and the kit contains the storage password). Unlike the bundle, the kit is write-only: nothing at runtime ever reads it back, so it is never a boot dependency.
    • The shipped compose template now consumes the emitted archiver.env directly via env_file: instead of inlining values — nothing to transcribe at setup or recovery, and the file holds no secrets so it can be committed to git alongside the compose file (inlining under environment: remains equivalent).

    Fixed

    • Removed the /opt/archiver/bundle entry from the image's VOLUME declaration. Docker Compose carries the previous container's mount over for image-declared volume paths when recreating, so converting a bundle-mode deployment to env-native with up --force-recreate inherited the old bundle mount and crash-looped on the bundle-without-password fail-fast. Until you run an image with this fix, convert with docker compose down && docker compose up -d instead. Env-native containers also no longer accumulate a useless anonymous volume at that path.

    Dependencies

    • chore(deps): update actions/checkout action to v7
    • chore(deps): update docker/setup-qemu-action action to v4.2.0
    • chore(deps): update bats/bats docker tag to v1.13.0
    • chore(deps): update dependency moby/moby to v29.6.1
    • chore(deps): update docker/setup-buildx-action action to v4.2.0
    • chore(deps): update docker/metadata-action action to v6.2.0
    • chore(deps): update docker/build-push-action action to v7.3.0
    Downloads
  • v0.9.1 c84fbf7d28

    v0.9.1
    All checks were successful
    CI / Build / Publish / Shellcheck (push) Successful in 6s
    CI / Build / Publish / Validate CHANGELOG structure (push) Successful in 3s
    CI / Build / Publish / Bats unit tests (push) Successful in 13s
    CI / Build / Publish / PR build & smoke test (push) Has been skipped
    CI / Build / Publish / Round-trip + scheduler smoke (push) Successful in 5m18s
    CI / Build / Publish / Host-driven integration (entrypoint + storage sidecars) (push) Successful in 6m42s
    CI / Build / Publish / Build multi-arch and push (push) Successful in 5m20s
    CI / Build / Publish / Reconcile releases across registries (push) Successful in 2m38s
    CI / Build / Publish / Workflow summary (push) Successful in 1s
    Stable

    Cody Bryant released this 2026-07-10 14:58:22 -07:00 | 27 commits to main since this release

    Changed

    • archiver init now writes its output (the env-native/ materials and the escrow bundle.tar.enc) to a neutral /opt/archiver/setup directory — mount that for init (-v ./archiver-setup:/opt/archiver/setup). Bundle-mode deployments still mount the bundle at /opt/archiver/bundle at run time; only the init output location moved, so the primary setup flow is no longer branded by the transitional bundle path. init also no longer claims the bundle is required to run Archiver (it is the disaster-recovery escrow).
    • Image-build robustness: the three binary downloads (duplicacy, docker-cli, supercronic) retry on transient network errors instead of failing the whole build on a single reset, and the docker-cli archive is downloaded to a file before extraction.
    Downloads
  • v0.9.0 79d79d327f

    v0.9.0
    All checks were successful
    CI / Build / Publish / Validate CHANGELOG structure (push) Successful in 2s
    CI / Build / Publish / Shellcheck (push) Successful in 3s
    CI / Build / Publish / Round-trip + scheduler smoke (push) Successful in 2m44s
    CI / Build / Publish / Workflow summary (push) Successful in 1s
    CI / Build / Publish / Bats unit tests (push) Successful in 6s
    CI / Build / Publish / PR build & smoke test (push) Has been skipped
    CI / Build / Publish / Host-driven integration (entrypoint + storage sidecars) (push) Successful in 1m46s
    CI / Build / Publish / Build multi-arch and push (push) Successful in 3m51s
    CI / Build / Publish / Reconcile releases across registries (push) Successful in 1m21s
    Stable

    Cody Bryant released this 2026-07-09 21:49:05 -07:00 | 36 commits to main since this release

    Breaking

    • BUNDLE_PASSWORD is no longer read from the environment and must be provided as a file (a Docker or Kubernetes secret). Every existing deployment that passes it via environment: or env_file must migrate before upgrading: write the password to a file, mount it at /run/secrets/bundle_password (a Docker Compose or Swarm secrets: entry named bundle_password lands there automatically — see the secrets: block in the repo's compose.yaml), or point BUNDLE_PASSWORD_FILE at another path, and remove BUNDLE_PASSWORD from the environment. A container that still has it set fails fast at startup with this migration message rather than silently ignoring it (which would otherwise drop a bundle deployment into env-native mode and fail later with a confusing "no bundle" error). Why: the bundle password decrypts the entire bundle, including the RSA private key, and an environment variable leaks through docker inspect and /proc.

    Added

    • Env-native configuration as a dual-mode alternative to the encrypted bundle, so a deployment can be driven entirely by environment variables plus file-based secrets (for example a Kubernetes ConfigMap + Secret) with no bundle at all. Configuration is now resolved in layers of increasing precedence: the decrypted bundle's config.sh (optional baseline, still the cold-restore path), plain environment variables for non-secret settings, then files for secrets. Non-secret settings (SERVICE_DIRECTORIES, the STORAGE_TARGET_N_* non-secret fields, ROTATE_BACKUPS, PRUNE_KEEP, DUPLICACY_THREADS, NOTIFICATION_SERVICE) can be set as env vars and override the bundle. Secrets (STORAGE_PASSWORD, RSA_PASSPHRASE, PUSHOVER_USER_KEY, PUSHOVER_API_TOKEN, and each target's B2_ID/B2_KEY/S3_ID/S3_SECRET) are read only from files, never from a raw env var: each comes from <NAME>_FILE if set, otherwise /run/secrets/<lowercased name>, and a secret passed as a raw env var is purged, with a logged warning naming the file path to use instead. The RSA keypair (and optional SFTP key) are supplied the same way and materialized into /opt/archiver/keys at startup, with mounted key files overriding bundle-extracted keys. This lets you migrate off the bundle one value at a time: set an env var or mount a secret and it shadows the bundle, and once every value is shadowed you can drop the bundle. With no env or secret overrides and a bundle present, behavior is unchanged.
    • SERVICE_DIRECTORIES accepts a colon-delimited scalar (for example /srv/*/:/home/user/data/) in addition to the bundle's bash-array form, so it fits in a single env var or a YAML block. Newlines are also accepted as separators.
    • archiver migrate [OUTPUT_DIR] converts a bundle-based deployment to env-native materials: it writes the effective non-secret settings to an env file and every secret (plus the keys) to its own file, ready to load as a Docker Compose environment: block or a Kubernetes ConfigMap, and as Docker/Kubernetes secrets or openbao entries. The bundle is left untouched.
    • bundle export is now mode-agnostic: it serializes the effective (env-merged) configuration into the bundle, so a fully env-native deployment can regenerate a portable, self-contained encrypted bundle for cold restore / disaster recovery.
    • archiver init now also writes ready-to-deploy env-native materials to env-native/ in the bundle directory (archiver.env + secrets/, via the same serializer as archiver migrate), alongside the encrypted bundle. A new deployment can go straight to env-native without ever hand-transcribing configuration; the emitted files are plaintext secrets, so move them into a secret store and delete the directory. The shipped compose.yaml template now leads with the env-native service; bundle mode is the commented alternative.
    • Broad new test coverage, all in CI: secondary-storage copy path incl. restore-from-secondary and copy-failure reporting; multi-service glob expansion with per-service failure isolation; lock concurrency (busy refusal, stale-lock recovery); real-entrypoint docker stop during a backup; pause/resume/stop-while-paused against the live process tree; stop during a non-final service; per-service service-backup-settings.sh (filters, hook order, isolation); healthcheck states; restore extras (revision pinning, IGNORE_OWNERSHIP, restore-service.sh hook); malformed CRON_SCHEDULE fail-fast; real s3 (MinIO sidecar, TLS) and sftp (sshd sidecar) backup+restore runtime round-trips; and new bats suites for lockfile.sh, SERVICE_DIRECTORIES expansion, config serialization round-trips, and format_duration.

    Deprecated

    • Deploying from the encrypted bundle is now the transitional path: env-native configuration (environment variables plus file-based secrets) is the primary, first-class configuration source, and a future release will drop bundle support entirely. The bundle remains fully supported throughout 0.9.x, both for deployment and as the bundle export cold-restore artifact.

    Changed

    • Startup diagnostics for the file-only secret model: a mounted bundle whose password cannot be resolved now fails fast naming the expected password path (instead of a misleading "no bundle" error, or silently starting env-native and never backing up when keys are also mounted); an explicitly set BUNDLE_PASSWORD_FILE or <NAME>_FILE pointing at a missing file is a hard error instead of a silent fallback; secret files with Windows line endings (CRLF) are handled instead of failing later as a wrong password; and missing B2/S3 credentials name the secret file to provide.
    • The bundle password is no longer left in the container's process environment after the bundle import completes, and no longer appears on the openssl command line (visible in /proc while it runs) during bundle import/export. The ENV BUNDLE_PASSWORD="" stub left in the image (visible in docker inspect) is gone.
    • archiver bundle import now works from a plain docker exec (it resolves the default bundle path and the password file itself, matching bundle export).
    • The interactive archiver restore honors IGNORE_OWNERSHIP=1 like the non-interactive restore paths.
    • archiver migrate and archiver init write their env-native materials with restrictive permissions from the start (umask), and both warn that the output holds plaintext secrets to be moved into a secret store.
    • config-loader now fails fast with a clear message when STORAGE_PASSWORD is shorter than 8 characters (a Duplicacy storage-init requirement), instead of letting it surface later as an opaque "storage initialization failed".
    • bundle import now treats the SSH keypair as optional (only sftp targets need it); the RSA keypair is what it requires. This lets a bundle exported from an env-native deployment that has no SSH key re-import cleanly.
    • Restore now logs a warning (instead of doing it silently) when it would preserve original ownership but the container lacks CHOWN/FOWNER, so restored files would land owned by root. This makes it safe to run a backup-only deployment with just DAC_OVERRIDE and add CHOWN + FOWNER only when restoring. The restore is never blocked; set IGNORE_OWNERSHIP=1 to restore without preserving ownership.

    Fixed

    • A failed primary backup is now detected and reported. The backgrounded backup pipeline captured the log reader's PID instead of duplicacy's, so a duplicacy failure was invisible: the run logged the backup as completed, sent a success notification, and exited 0. A failed backup now logs an error, is counted in the completion notification, skips that service's copy/wrap-up, and makes archiver backup exit non-zero.
    • archiver stop requested during the final service's backup now actually stops the run. Previously that path swallowed the stop: the run continued into the storage check, prune -exhaustive, and copies to secondary storages, then recorded a successful completion. A stop during any service's backup now terminates duplicacy directly (previously it killed the log reader and waited for duplicacy to die of a broken pipe), skips the storage wrap-up, and records "Backup stopped" without counting the stop as an error.
    • When no service completes a backup, the storage wrap-up (check/prune) is skipped instead of silently running from whatever repository directory the last failed service left as the working directory.
    • Stale-lock recovery now actually re-takes the lock. Previously a run that cleaned up a dead-PID lock file proceeded without holding the lock itself, so stop/status/pause could not see it and a concurrent backup would have been admitted against the same storage.
    • A backup refused because one is already running now says so (A backup is already running (PID ...)) instead of exiting 1 silently.
    • The container's SIGTERM handler (docker stop) now waits for the interrupted backup to record its stop and release the lock before exiting, instead of racing PID-namespace teardown against that cleanup (which could SIGKILL it mid-flight and lose the stop record/notification).
    • archiver healthcheck no longer reports a healthy env-native deployment as UNHEALTHY: the absence of config.sh is only an error when there is no RSA key either. It also detected "run finished despite errors" by grepping for a log line that nothing ever emitted, so any transient error in the recent log window flipped the container UNHEALTHY; it now matches the real end-of-run marker.
    • Env-native SFTP deployments can now restore: the entrypoint places /run/secrets/ssh_public_key (override SSH_PUBLIC_KEY_FILE) alongside the private key, and archiver migrate exports it. The SFTP restore path requires both key halves; previously only the private key was placed, so backup worked but restore failed.

    Dependencies

    • chore(deps): update dependency aptible/supercronic to v0.2.47
    Downloads
  • v0.8.12 8f26cc2f93

    v0.8.12
    All checks were successful
    CI / Build / Publish / Shellcheck (push) Successful in 8s
    CI / Build / Publish / Validate CHANGELOG structure (push) Successful in 4s
    CI / Build / Publish / Bats unit tests (push) Successful in 7s
    CI / Build / Publish / PR build & smoke test (push) Has been skipped
    CI / Build / Publish / Round-trip + scheduler smoke (push) Successful in 3m24s
    CI / Build / Publish / Build multi-arch and push (push) Successful in 17m1s
    CI / Build / Publish / Reconcile releases across registries (push) Successful in 1m22s
    CI / Build / Publish / Workflow summary (push) Successful in 2s
    Stable

    Cody Bryant released this 2026-07-06 20:45:19 -07:00 | 55 commits to main since this release

    Changed

    • Replaced the in-container Debian cron daemon with supercronic for scheduled backups (CRON_SCHEDULE). supercronic runs jobs as the container user instead of forking with setgid, so the SETGID capability is no longer required and has been removed from the documented hardened cap set (compose.yaml, README.md) — the set is now DAC_OVERRIDE + CHOWN + FOWNER. It also passes the container environment straight through (Debian cron scrubs it). The entrypoint validates the schedule with supercronic -test (fail-fast on a malformed CRON_SCHEDULE) and runs supercronic backgrounded so the SIGTERM graceful-stop path is preserved. Added tzdata so a non-UTC TZ resolves. A new CI scheduler smoke test asserts a job fires under cap_drop: ALL with no SETGID. Action for hardened deployments: drop SETGID from your cap_add when you upgrade to this image.
    • Consolidated the per-storage-type credential and URL construction — previously duplicated across the primary-backup, add-copy, and restore code paths — into two shared helpers in config-loader.sh: build_storage_url and export_duplicacy_storage_secrets. No behavior change. Adds bats coverage of the DUPLICACY_<NAME>_* credential mapping across all four storage types (the surface the 0.8.10/0.8.11 do-spaces incidents traced to), so a single definition is now the source of truth for both backup and restore.

    Fixed

    • Restore silently lost original file ownership under the recommended hardened cap set: the example compose.yaml / README.md did cap_drop: ALL and added back only DAC_OVERRIDE + SETGID, but restore preserves ownership by default (-ignore-owner is opt-in) and Duplicacy recreates the original UID/GID via chown(), which requires CAP_CHOWN (and CAP_FOWNER to set mode/timestamps on other-UID files). DAC_OVERRIDE bypasses permission checks but not chown, so a restore completed with every file owned by root. Added CHOWN + FOWNER to the documented cap set. (Default Docker caps already include CHOWN; this only affected the hardened cap_drop: ALL template.)
    Downloads
  • v0.8.11 6939b087a4

    v0.8.11
    All checks were successful
    CI / Build / Publish / Shellcheck (push) Successful in 5s
    CI / Build / Publish / Validate CHANGELOG structure (push) Successful in 2s
    CI / Build / Publish / PR build & smoke test (push) Has been skipped
    CI / Build / Publish / Build multi-arch and push (push) Successful in 2m11s
    CI / Build / Publish / Reconcile releases across registries (push) Successful in 46s
    CI / Build / Publish / Workflow summary (push) Successful in 2s
    Stable

    Cody Bryant released this 2026-06-04 22:32:36 -07:00 | 67 commits to main since this release

    Fixed

    • The 0.8.10 do-spaces restore fix was still broken in auto-restore: sanitize_storage_name logs a "name was sanitized" WARN via log_message, and auto-restore.sh redefines log_message to also echo to stdout — so callers doing name="$(sanitize_storage_name …)" captured that log line into the name, re-corrupting DUPLICACY_<NAME>_*. The function now redirects its log_message to stderr, so only the sanitized name reaches stdout. Kept as one shared function (called by both backup and restore) so the sanitization is guaranteed identical on both paths.
    Downloads
  • v0.8.10 7ca9b341f6

    v0.8.10
    All checks were successful
    CI / Build / Publish / Shellcheck (push) Successful in 4s
    CI / Build / Publish / Validate CHANGELOG structure (push) Successful in 2s
    CI / Build / Publish / PR build & smoke test (push) Has been skipped
    CI / Build / Publish / Build multi-arch and push (push) Successful in 2m1s
    CI / Build / Publish / Reconcile releases across registries (push) Successful in 59s
    CI / Build / Publish / Workflow summary (push) Successful in 1s
    Stable

    Cody Bryant released this 2026-06-04 18:37:55 -07:00 | 69 commits to main since this release

    Fixed

    • Restore from a storage whose name contains a character invalid in a shell identifier (e.g. a hyphen, like do-spaces) failed: duplicacy-restore.sh built DUPLICACY_<NAME>_S3_ID/_SECRET/_PASSWORD from the raw name, so export rejected it ("not a valid identifier"), the credentials never reached duplicacy, and it fell through to an interactive prompt → EOF → that target was skipped. Now sanitizes the storage name (matching duplicacy-backup.sh) before building the env-var names and -storage-name.
    Downloads
  • v0.8.9 3717a0e5c9

    v0.8.9
    All checks were successful
    CI / Build / Publish / Validate CHANGELOG structure (push) Successful in 2s
    CI / Build / Publish / Shellcheck (push) Successful in 4s
    CI / Build / Publish / PR build & smoke test (push) Has been skipped
    CI / Build / Publish / Build multi-arch and push (push) Successful in 1m52s
    CI / Build / Publish / Reconcile releases across registries (push) Successful in 51s
    CI / Build / Publish / Workflow summary (push) Successful in 2s
    Stable

    Cody Bryant released this 2026-06-04 16:48:52 -07:00 | 71 commits to main since this release

    Fixed

    • auto-restore-all.sh shipped without its execute bit (mode 0644 in the repo, preserved into the image by COPY), so archiver auto-restore-all failed with "Permission denied" when the dispatcher exec'd it — breaking the tier-3 full-host restore path. Restored the script's exec bit and hardened the Dockerfile to chmod +x lib/scripts/*.sh so a missing source bit can't recur.
    Downloads