-
v0.10.2
StableSome checks failedCI / Build / Publish / Shellcheck (push) Successful in 2sCI / Build / Publish / Validate CHANGELOG structure (push) Successful in 3sCI / Build / Publish / Bats unit tests (push) Successful in 11sCI / Build / Publish / PR build & smoke test (push) Has been skippedCI / Build / Publish / Host-driven integration (entrypoint + storage sidecars) (push) Successful in 2m7sCI / Build / Publish / Round-trip + scheduler smoke (push) Successful in 3m22sCI / Build / Publish / Build multi-arch and push (push) Successful in 2m28sCI / Build / Publish / Reconcile releases across registries (push) Successful in 1m23sCI / Build / Publish / Workflow summary (push) Failing after 0sreleased this
2026-07-21 20:11:18 -07:00 | 0 commits to main since this releaseChanged
- The automatic recovery kit is no longer written as a locked-down
root:600file. 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'sconfigfile 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 (orarchiver 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 — norecovery-kit forceneeded.
Dependencies
- chore(deps): update actions/checkout action to v7.0.1
- chore(deps): update dependency moby/moby to v29.6.2
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- The automatic recovery kit is no longer written as a locked-down
-
v0.10.1
StableAll checks were successfulCI / Build / Publish / Shellcheck (push) Successful in 2sCI / Build / Publish / Validate CHANGELOG structure (push) Successful in 1sCI / Build / Publish / PR build & smoke test (push) Has been skippedCI / Build / Publish / Round-trip + scheduler smoke (push) Successful in 3m14sCI / Build / Publish / Host-driven integration (entrypoint + storage sidecars) (push) Successful in 1m55sCI / Build / Publish / Build multi-arch and push (push) Successful in 2m1sCI / Build / Publish / Reconcile releases across registries (push) Successful in 54sCI / Build / Publish / Bats unit tests (push) Successful in 6sCI / Build / Publish / Workflow summary (push) Successful in 1sreleased this
2026-07-12 13:48:32 -07:00 | 5 commits to main since this releaseChanged
- 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/prunesafe alongside acopyreading or writing the same storage (verified against the Duplicacy v3.2.5 source: a copy reads a mid-prune fossil via a.fslfallback 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 usingprune -exclusive(it uses non-exclusive prune, and-exhaustive— which is safe — never implies-exclusive).
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- 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
-
v0.10.0
StableAll checks were successfulCI / Build / Publish / Shellcheck (push) Successful in 2sCI / Build / Publish / Validate CHANGELOG structure (push) Successful in 1sCI / Build / Publish / PR build & smoke test (push) Has been skippedCI / Build / Publish / Round-trip + scheduler smoke (push) Successful in 4m0sCI / Build / Publish / Workflow summary (push) Successful in 1sCI / Build / Publish / Bats unit tests (push) Successful in 6sCI / Build / Publish / Host-driven integration (entrypoint + storage sidecars) (push) Successful in 1m51sCI / Build / Publish / Build multi-arch and push (push) Successful in 2m6sCI / Build / Publish / Reconcile releases across registries (push) Successful in 55sreleased this
2026-07-12 11:49:44 -07:00 | 9 commits to main since this releaseBreaking
- 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 plusprune -exhaustiveon 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_SCHEDULEwas renamed toBACKUP_SCHEDULE, and the newMAINTENANCE_SCHEDULEdrives the maintenance pipeline (unset = maintenance only runs viaarchiver maintenance). A container started withCRON_SCHEDULEset 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 withoutMAINTENANCE_SCHEDULEgets a prominent startup warning, and healthcheck warns when any storage has gone more than 8 days without a successful check or prune.ROTATE_BACKUPSwas renamed toPRUNE_BACKUPS(old name still translated silently for old bundle configs, with a startup warning when set as an env var), joined byCHECK_BACKUPS(default true) so shared-storage deployments can disable either phase independently, andPRUNE_EXHAUSTIVE_FREQUENCY(off/daily/weekly/monthly, default monthly): the expensive-exhaustivechunk-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 exhaustiveforces it.archiver startandarchiver restartwere removed (they error with guidance):archiver backup --detachis the background form, with the visible already-running refusal. Theprune|retainarguments tobackupwere removed likewise.archiver stoptakes a target:backup,maintenance, orall(default).
Added
archiver maintenance [exhaustive]— the maintenance pipeline verb: per storage,check -all -fossils -resurrect(whenCHECK_BACKUPS) thenprune -allwith the retention policy (whenPRUNE_BACKUPS), recording per-storage last-success timestamps thatarchiver statusdisplays and healthcheck monitors. Also available to external schedulers asrun maintenance(synchronous, exit code propagates).- Secondary copies now run in parallel. Each
duplicacy copyto 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, formerlyarchiver 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 backgroundedmain.shwith 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
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- 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 (
-
v0.9.2
StableAll checks were successfulCI / Build / Publish / Shellcheck (push) Successful in 2sCI / Build / Publish / Validate CHANGELOG structure (push) Successful in 2sCI / Build / Publish / PR build & smoke test (push) Has been skippedCI / Build / Publish / Bats unit tests (push) Successful in 6sCI / Build / Publish / Host-driven integration (entrypoint + storage sidecars) (push) Successful in 1m50sCI / Build / Publish / Round-trip + scheduler smoke (push) Successful in 3m15sCI / Build / Publish / Build multi-arch and push (push) Successful in 5m0sCI / Build / Publish / Reconcile releases across registries (push) Successful in 1m17sCI / Build / Publish / Workflow summary (push) Successful in 1sreleased this
2026-07-10 20:23:33 -07:00 | 17 commits to main since this releaseAdded
- Automatic recovery kit: with the
recovery_passwordsecret present (at/run/secrets/recovery_passwordor viaRECOVERY_PASSWORD_FILE), after each backup archiver assembles a kit holding the full effective configuration (every non-secret setting, every secret, and the keys — exactly whatarchiver migrateemits), 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 kustomizeconfigMapGeneratorunder 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.encplus a companion README with the decrypt command).archiver initnow 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 owncompose.yaml— so new deployments maintain a kit by default; existing deployments enable it withopenssl rand -base64 24 > secrets/recovery_passwordplus asecrets:entry (archiver migrateprints 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 yieldsarchiver.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 theprune|retainrotation override), so an unchanged config is a nightly no-op.archiver recovery-kitpushes on demand andarchiver recovery-kit forcere-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 fromSTORAGE_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.envdirectly viaenv_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 underenvironment:remains equivalent).
Fixed
- Removed the
/opt/archiver/bundleentry from the image'sVOLUMEdeclaration. 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 withup --force-recreateinherited the old bundle mount and crash-looped on the bundle-without-password fail-fast. Until you run an image with this fix, convert withdocker compose down && docker compose up -dinstead. 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
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Automatic recovery kit: with the
-
v0.9.1
StableAll checks were successfulCI / Build / Publish / Shellcheck (push) Successful in 6sCI / Build / Publish / Validate CHANGELOG structure (push) Successful in 3sCI / Build / Publish / Bats unit tests (push) Successful in 13sCI / Build / Publish / PR build & smoke test (push) Has been skippedCI / Build / Publish / Round-trip + scheduler smoke (push) Successful in 5m18sCI / Build / Publish / Host-driven integration (entrypoint + storage sidecars) (push) Successful in 6m42sCI / Build / Publish / Build multi-arch and push (push) Successful in 5m20sCI / Build / Publish / Reconcile releases across registries (push) Successful in 2m38sCI / Build / Publish / Workflow summary (push) Successful in 1sreleased this
2026-07-10 14:58:22 -07:00 | 27 commits to main since this releaseChanged
archiver initnow writes its output (theenv-native/materials and the escrowbundle.tar.enc) to a neutral/opt/archiver/setupdirectory — mount that for init (-v ./archiver-setup:/opt/archiver/setup). Bundle-mode deployments still mount the bundle at/opt/archiver/bundleat 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
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
v0.9.0
StableAll checks were successfulCI / Build / Publish / Validate CHANGELOG structure (push) Successful in 2sCI / Build / Publish / Shellcheck (push) Successful in 3sCI / Build / Publish / Round-trip + scheduler smoke (push) Successful in 2m44sCI / Build / Publish / Workflow summary (push) Successful in 1sCI / Build / Publish / Bats unit tests (push) Successful in 6sCI / Build / Publish / PR build & smoke test (push) Has been skippedCI / Build / Publish / Host-driven integration (entrypoint + storage sidecars) (push) Successful in 1m46sCI / Build / Publish / Build multi-arch and push (push) Successful in 3m51sCI / Build / Publish / Reconcile releases across registries (push) Successful in 1m21sreleased this
2026-07-09 21:49:05 -07:00 | 36 commits to main since this releaseBreaking
BUNDLE_PASSWORDis no longer read from the environment and must be provided as a file (a Docker or Kubernetes secret). Every existing deployment that passes it viaenvironment:orenv_filemust migrate before upgrading: write the password to a file, mount it at/run/secrets/bundle_password(a Docker Compose or Swarmsecrets:entry namedbundle_passwordlands there automatically — see thesecrets:block in the repo'scompose.yaml), or pointBUNDLE_PASSWORD_FILEat another path, and removeBUNDLE_PASSWORDfrom 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 throughdocker inspectand/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, theSTORAGE_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'sB2_ID/B2_KEY/S3_ID/S3_SECRET) are read only from files, never from a raw env var: each comes from<NAME>_FILEif 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/keysat 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_DIRECTORIESaccepts 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 Composeenvironment:block or a Kubernetes ConfigMap, and as Docker/Kubernetes secrets or openbao entries. The bundle is left untouched.bundle exportis 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 initnow also writes ready-to-deploy env-native materials toenv-native/in the bundle directory (archiver.env+secrets/, via the same serializer asarchiver 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 shippedcompose.yamltemplate 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 stopduring a backup; pause/resume/stop-while-paused against the live process tree; stop during a non-final service; per-serviceservice-backup-settings.sh(filters, hook order, isolation); healthcheck states; restore extras (revision pinning,IGNORE_OWNERSHIP,restore-service.shhook); malformedCRON_SCHEDULEfail-fast; real s3 (MinIO sidecar, TLS) and sftp (sshd sidecar) backup+restore runtime round-trips; and new bats suites forlockfile.sh,SERVICE_DIRECTORIESexpansion, config serialization round-trips, andformat_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 exportcold-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_FILEor<NAME>_FILEpointing 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
/procwhile it runs) during bundle import/export. TheENV BUNDLE_PASSWORD=""stub left in the image (visible indocker inspect) is gone. archiver bundle importnow works from a plaindocker exec(it resolves the default bundle path and the password file itself, matchingbundle export).- The interactive
archiver restorehonorsIGNORE_OWNERSHIP=1like the non-interactive restore paths. archiver migrateandarchiver initwrite 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-loadernow fails fast with a clear message whenSTORAGE_PASSWORDis shorter than 8 characters (a Duplicacy storage-init requirement), instead of letting it surface later as an opaque "storage initialization failed".bundle importnow 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 justDAC_OVERRIDEand addCHOWN+FOWNERonly when restoring. The restore is never blocked; setIGNORE_OWNERSHIP=1to 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 backupexit non-zero. archiver stoprequested 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/pausecould 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 healthcheckno longer reports a healthy env-native deployment as UNHEALTHY: the absence ofconfig.shis 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(overrideSSH_PUBLIC_KEY_FILE) alongside the private key, andarchiver migrateexports 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
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
v0.8.12
StableAll checks were successfulCI / Build / Publish / Shellcheck (push) Successful in 8sCI / Build / Publish / Validate CHANGELOG structure (push) Successful in 4sCI / Build / Publish / Bats unit tests (push) Successful in 7sCI / Build / Publish / PR build & smoke test (push) Has been skippedCI / Build / Publish / Round-trip + scheduler smoke (push) Successful in 3m24sCI / Build / Publish / Build multi-arch and push (push) Successful in 17m1sCI / Build / Publish / Reconcile releases across registries (push) Successful in 1m22sCI / Build / Publish / Workflow summary (push) Successful in 2sreleased this
2026-07-06 20:45:19 -07:00 | 55 commits to main since this releaseChanged
- Replaced the in-container Debian
crondaemon with supercronic for scheduled backups (CRON_SCHEDULE). supercronic runs jobs as the container user instead of forking withsetgid, so theSETGIDcapability is no longer required and has been removed from the documented hardened cap set (compose.yaml,README.md) — the set is nowDAC_OVERRIDE+CHOWN+FOWNER. It also passes the container environment straight through (Debian cron scrubs it). The entrypoint validates the schedule withsupercronic -test(fail-fast on a malformedCRON_SCHEDULE) and runs supercronic backgrounded so the SIGTERM graceful-stop path is preserved. Addedtzdataso a non-UTCTZresolves. A new CI scheduler smoke test asserts a job fires undercap_drop: ALLwith noSETGID. Action for hardened deployments: dropSETGIDfrom yourcap_addwhen 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_urlandexport_duplicacy_storage_secrets. No behavior change. Adds bats coverage of theDUPLICACY_<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.mddidcap_drop: ALLand added back onlyDAC_OVERRIDE+SETGID, but restore preserves ownership by default (-ignore-owneris opt-in) and Duplicacy recreates the original UID/GID viachown(), which requiresCAP_CHOWN(andCAP_FOWNERto set mode/timestamps on other-UID files).DAC_OVERRIDEbypasses permission checks but notchown, so a restore completed with every file owned by root. AddedCHOWN+FOWNERto the documented cap set. (Default Docker caps already includeCHOWN; this only affected the hardenedcap_drop: ALLtemplate.)
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Replaced the in-container Debian
-
v0.8.11
StableAll checks were successfulCI / Build / Publish / Shellcheck (push) Successful in 5sCI / Build / Publish / Validate CHANGELOG structure (push) Successful in 2sCI / Build / Publish / PR build & smoke test (push) Has been skippedCI / Build / Publish / Build multi-arch and push (push) Successful in 2m11sCI / Build / Publish / Reconcile releases across registries (push) Successful in 46sCI / Build / Publish / Workflow summary (push) Successful in 2sreleased this
2026-06-04 22:32:36 -07:00 | 67 commits to main since this releaseFixed
- The 0.8.10 do-spaces restore fix was still broken in
auto-restore:sanitize_storage_namelogs a "name was sanitized" WARN vialog_message, andauto-restore.shredefineslog_messageto also echo to stdout — so callers doingname="$(sanitize_storage_name …)"captured that log line into the name, re-corruptingDUPLICACY_<NAME>_*. The function now redirects itslog_messageto 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
-
Source code (ZIP)
8 downloads
-
Source code (TAR.GZ)
9 downloads
- The 0.8.10 do-spaces restore fix was still broken in
-
v0.8.10
StableAll checks were successfulCI / Build / Publish / Shellcheck (push) Successful in 4sCI / Build / Publish / Validate CHANGELOG structure (push) Successful in 2sCI / Build / Publish / PR build & smoke test (push) Has been skippedCI / Build / Publish / Build multi-arch and push (push) Successful in 2m1sCI / Build / Publish / Reconcile releases across registries (push) Successful in 59sCI / Build / Publish / Workflow summary (push) Successful in 1sreleased this
2026-06-04 18:37:55 -07:00 | 69 commits to main since this releaseFixed
- Restore from a storage whose name contains a character invalid in a shell identifier (e.g. a hyphen, like
do-spaces) failed:duplicacy-restore.shbuiltDUPLICACY_<NAME>_S3_ID/_SECRET/_PASSWORDfrom the raw name, soexportrejected 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 (matchingduplicacy-backup.sh) before building the env-var names and-storage-name.
Downloads
-
Source code (ZIP)
8 downloads
-
Source code (TAR.GZ)
12 downloads
- Restore from a storage whose name contains a character invalid in a shell identifier (e.g. a hyphen, like
-
v0.8.9
StableAll checks were successfulCI / Build / Publish / Validate CHANGELOG structure (push) Successful in 2sCI / Build / Publish / Shellcheck (push) Successful in 4sCI / Build / Publish / PR build & smoke test (push) Has been skippedCI / Build / Publish / Build multi-arch and push (push) Successful in 1m52sCI / Build / Publish / Reconcile releases across registries (push) Successful in 51sCI / Build / Publish / Workflow summary (push) Successful in 2sreleased this
2026-06-04 16:48:52 -07:00 | 71 commits to main since this releaseFixed
auto-restore-all.shshipped without its execute bit (mode 0644 in the repo, preserved into the image byCOPY), soarchiver auto-restore-allfailed with "Permission denied" when the dispatcherexec'd it — breaking the tier-3 full-host restore path. Restored the script's exec bit and hardened the Dockerfile tochmod +x lib/scripts/*.shso a missing source bit can't recur.
Downloads
-
Source code (ZIP)
9 downloads
-
Source code (TAR.GZ)
8 downloads