• 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