Automated encrypted backups with deduplication to local disk, SFTP, BackBlaze B2, and S3 storage. Leverages Duplicacy CLI v3.2.5 to follow the 3-2-1 Backup Strategy while removing the complexity of manual configuration.
Primary repository: This project is developed at forgejo.bryantserver.com/SisyphusMD/archiver. The GitHub copy is a read-only mirror.
Archiver automates backing up directories to multiple remote storage locations with encryption and deduplication. Configure once, then backups run automatically on a schedule.
Each directory gets backed up independently, with optional pre/post-backup scripts for service-specific needs (like database dumps). Backups are encrypted at rest and deduplicated across all your directories to save storage space.
Supports local disk, SFTP (Synology NAS, etc.), BackBlaze B2, and S3-compatible storage.
BUNDLE_PASSWORD is no longer read from the environment. The bundle password is now a file-based secret: mount it at /run/secrets/bundle_password (a Compose or Swarm secrets: entry named bundle_password, as in the compose template), or point BUNDLE_PASSWORD_FILE at another path. A container that still sets BUNDLE_PASSWORD via environment: or env_file fails fast at startup with a migration message.
To upgrade an existing deployment: write the password to a file (for example ./secrets/bundle_password), add the secrets: entries from the compose template, and remove BUNDLE_PASSWORD from the environment. This password decrypts the entire bundle, including the RSA private key, and an environment variable leaks through docker inspect and /proc — a file does not. See Configuration Sources for the full file-based secrets model, and Migrating a bundle to env-native to move the rest of the configuration out of the bundle too (optional).
Direct installation on host systems is no longer supported. All deployments must now run inside a container (Docker, Podman, Kubernetes, etc.).
If you're currently running Archiver v0.6.5 or earlier directly on your host system, see the Legacy to Docker Migration Guide for step-by-step upgrade instructions.
New users: Continue reading below to get started.
- Encrypted & Deduplicated: Block-level deduplication minimizes storage, RSA encryption secures data
- Multiple Backends: Local disk, SFTP, BackBlaze B2, S3-compatible storage
- Automated Rotation: Configurable retention policies (keep daily, weekly, monthly snapshots)
- Service Integration: Pre/post-backup scripts, custom restore procedures
- Notifications: Pushover alerts for successes and failures
- Easy Restoration: Interactive restore script to recover specific revisions
- A container runtime — Docker, Podman, or Kubernetes. The rest of this README uses Docker commands as the default; translate them to your runtime as needed.
- Docker Compose (optional, for easier management of a long-lived container)
Prepare at least one storage location before running init. Expand the sections below for setup instructions.
Click to expand local disk setup instructions
Local disk storage is the simplest and fastest option, ideal as your primary backup target. Backups can then be copied from local storage to remote locations (SFTP, B2, S3) for off-site redundancy.
- A local directory path (e.g.,
/mnt/backup/storage) - Sufficient disk space for your backups
- Proper read/write permissions
-
Create the backup directory:
sudo mkdir -p /mnt/backup/storage
-
Set appropriate permissions:
sudo chown -R $USER:$USER /mnt/backup/storage sudo chmod 755 /mnt/backup/storage
-
Verify the directory is accessible:
ls -la /mnt/backup/storage
Tip: For the 3-2-1 backup strategy, use local disk as your primary storage target for fast backups, then configure additional remote storage targets to copy backups off-site automatically.
Click to expand Synology setup instructions
- Login to Synology DSM Web UI (usually
http://<nas-ip>:5000) - Open Control Panel → File Services → FTP tab
- Enable SFTP service (not FTP/FTPS)
- Default port 22 is fine
- Click Apply
- Control Panel → User & Group → Create
- Set Name and Password
- Assign to appropriate Groups
- Grant shared folder permissions
- Under Application Permissions, allow SFTP
- Complete and click Done
- Control Panel → Shared Folder → Create
- Name the folder and configure settings
- Don't enable WriteOnce (incompatible with backups)
- If using BTRFS, enable data checksum
- Grant Read/Write access to backup user
Generate SSH key first (init script can do this), then:
- Control Panel → User & Group → Advanced
- Enable user home service
- Open File Station → homes folder → your user folder
- Create
.sshfolder if it doesn't exist - Upload or create
authorized_keysfile containing your public key (ssh-ed25519 AAAA...)
Click to expand BackBlaze B2 setup instructions
- Create account or sign in
- My Settings → Enable B2 Cloud Storage
- Buckets → Create a Bucket
- Choose unique Bucket Name
- Files: Private
- Encryption: Enable
- Object Lock: Disable
- Lifecycle: Keep all versions (default)
- Application Keys → Add New
- Name the key
- Allow access to your bucket
- Type: Read and Write
- Enable List All Bucket Names
- Click Create New Key
- Save the keyID and applicationKey (shown only once)
Click to expand S3 setup instructions
S3 providers vary, but you'll need:
- Bucket Name (globally unique)
- Endpoint (e.g.,
s3.amazonaws.comors3.us-east-1.wasabisys.com) - Region (optional, provider-specific, e.g.,
us-east-1) - Access Key ID (with read/write permissions)
- Secret Access Key
Create these through your S3 provider's console (AWS, Wasabi, Backblaze S3 API, etc.)
Click to expand Pushover setup instructions
- Create account or sign in
- Note your User Key from the dashboard
- Add a device to receive notifications
- Create an Application/API Token
- Name your app and agree to terms
- Save the API Token/Key
You'll enter the User Key and API Token during init.
Container image: Examples below pull from
forgejo.bryantserver.com/sisyphusmd/archiver. The same image is also published toghcr.io/sisyphusmd/archiverif you prefer that registry — just substitute the registry hostname in anyimage:ordocker runline.
Skip this step if you already have configuration — env-native materials (archiver.env + secret files) or a bundle (bundle.tar.enc / export-*.tar.enc) from a previous installation.
For new installations, run initialization interactively (the mount is just an output directory for the generated materials):
docker run -it --rm \
-v ./archiver-setup:/opt/archiver/setup \
forgejo.bryantserver.com/sisyphusmd/archiver:0.10.2 initThis writes your configuration into archiver-setup/:
env-native/—archiver.env(non-secret settings) plussecrets/(one file per secret, including the keys). This is what you deploy with: a Composeenvironment:block +secrets:, or a Kubernetes ConfigMap + Secret. The files are plaintext — move them into your secret store and deleteenv-native/afterwards.bundle.tar.enc— an encrypted, self-contained copy of the same configuration (transitional bundle mode / manual cold-restore copy — see the commented alternative in compose.yaml).
init also generates and displays the recovery password — save it in your password manager; it is the one credential you personally keep. Once the deployment is running, archiver automatically maintains an encrypted recovery kit of the full configuration on every storage target, and that password recovers everything (see Automatic Recovery Kit).
Create compose.yaml (env-native, the primary mode). Place the emitted archiver.env next to it — the compose file loads it directly via env_file:, so there is nothing to transcribe, and since it holds no secrets it can be committed to git alongside the compose file. Point the secrets: files at where you moved env-native/secrets/:
services:
archiver:
container_name: archiver
image: forgejo.bryantserver.com/sisyphusmd/archiver:0.10.2
restart: unless-stopped
stop_grace_period: 2m # Allow time for graceful shutdown and cleanup
hostname: backup-server # forms the Duplicacy snapshot ID (<hostname>-<service>); keep it stable,
# and set it to the ORIGINAL value when restoring on a new machine
cap_drop:
- ALL
cap_add:
- DAC_OVERRIDE # Backup + restore: read/write data owned by other UIDs
- CHOWN # Restore only (drop for backup-only): recreate files under their original owner
- FOWNER # Restore only (drop for backup-only): set mode/timestamps on files owned by other UIDs
security_opt:
- no-new-privileges:true
environment:
BACKUP_SCHEDULE: "0 3 * * *" # backups: daily at 3am (omit for manual mode)
MAINTENANCE_SCHEDULE: "0 13 * * *" # check+prune: daily at 1pm, opposite the backup window
TZ: "America/New_York" # Timezone for scheduled backups and timestamps (default: UTC)
env_file:
- ./archiver.env # non-secret settings, exactly as emitted by init/migrate
# (no secrets inside, so safe to commit to git; values can
# also be inlined under environment: instead)
secrets: # each lands at /run/secrets/<name>
- storage_password
- rsa_passphrase
- rsa_private_key
- rsa_public_key
- recovery_password # enables the automatic recovery kit (init generates it)
# - ssh_private_key # only for sftp targets
# - ssh_public_key # only for sftp targets
# - storage_target_1_b2_id # only for b2 targets
# - storage_target_1_b2_key
# - storage_target_1_s3_id # only for s3 targets
# - storage_target_1_s3_secret
volumes:
- ./archiver-logs:/opt/archiver/logs # Persistent logs (optional)
- /path/to/host/backup-dir:/mnt/backup-dir # Data to backup (must match SERVICE_DIRECTORIES)
- /path/to/host/backup/storage:/mnt/backup/storage # Local storage target
- ./compose.yaml:/opt/archiver/deployment/compose.yaml:ro # captured into the recovery kit
# - /var/run/docker.sock:/var/run/docker.sock # For docker exec in scripts (optional)
# - /path/to/host/restore-dir:/mnt/restore-dir # Restore location (will be prompted)
secrets:
storage_password: { file: ./secrets/storage_password }
rsa_passphrase: { file: ./secrets/rsa_passphrase }
rsa_private_key: { file: ./secrets/rsa_private_key }
rsa_public_key: { file: ./secrets/rsa_public_key }
recovery_password: { file: ./secrets/recovery_password }
# ssh_private_key: { file: ./secrets/ssh_private_key }
# ssh_public_key: { file: ./secrets/ssh_public_key }To deploy from the encrypted bundle instead (transitional / cold-restore path), use the commented bundle-mode service in compose.yaml: mount ./archiver-bundle:/opt/archiver/bundle and provide the bundle password as the bundle_password secret file.
Update paths, then start:
docker compose up -dIf your backup scripts need to control other containers (e.g., docker exec for database dumps), mount the container runtime socket:
volumes:
# Docker socket
- /var/run/docker.sock:/var/run/docker.sock
# Podman socket (mount as docker.sock so the docker CLI works)
# - /run/podman/podman.sock:/var/run/docker.sockSecurity Warning: This grants root-level access to the Docker daemon. The container can start/stop/delete any container or access any data. Only use if necessary.
If your restore scripts need to manage host services (e.g., systemctl mask/stop/start to orchestrate restores), mount the systemd D-Bus socket and unit directory, and set SYSTEMCTL_FORCE_BUS=1:
environment:
SYSTEMCTL_FORCE_BUS: "1" # Required: forces systemctl to use D-Bus instead of private socket
volumes:
- /run/dbus/system_bus_socket:/run/dbus/system_bus_socket # systemctl start/stop/status
- /etc/systemd/system:/etc/systemd/system # systemctl mask/unmask/enable/disableSecurity Warning: This grants the container full control over host systemd services. It can start, stop, mask, unmask, or restart any service on the host. Only use if your restore scripts require service orchestration.
If your restore scripts use ZFS snapshots for pre-restore safety, the ZFS device node is also needed:
volumes:
- /dev/zfs:/dev/zfsSecurity Warning: This grants the container access to all ZFS pools on the host. It can create, destroy, or modify any dataset or snapshot. Only use if your restore scripts take ZFS snapshots.
We recommend dropping all capabilities and adding back only what Archiver needs. The example compose.yaml above includes this by default.
services:
archiver:
cap_drop:
- ALL
cap_add:
- DAC_OVERRIDE # Backup + restore: write to directories owned by other UIDs
- CHOWN # Restore only (drop for backup-only): recreate files under their original owner
- FOWNER # Restore only (drop for backup-only): set mode/timestamps on files owned by other UIDs
security_opt:
- no-new-privileges:true # Prevent privilege escalation| Capability | When Required | Why |
|---|---|---|
DAC_OVERRIDE |
Always (with cap_drop: ALL) |
Archiver runs as root but writes to service data directories that may be owned by other users (e.g., UID 1000) |
CHOWN |
Restore with original ownership (the default) | Restore recreates files as their original UID/GID via chown(), which requires CAP_CHOWN. Without it, restored files land owned by root (Archiver logs a warning when this happens) |
FOWNER |
Restore with original ownership (the default) | Lets Archiver set permissions/timestamps on restored files owned by other UIDs |
Backup-only least privilege: CHOWN and FOWNER are used only by restore, so a container that only takes scheduled backups can drop both and run with just DAC_OVERRIDE. Add them back when you need to restore with original ownership. Restoring without them does not fail: the data is restored correctly, but files land owned by root and Archiver logs a warning. To restore without preserving ownership on purpose, set IGNORE_OWNERSHIP=1.
Note: no-new-privileges is a kernel security option, not a Linux capability. It is compatible with DAC_OVERRIDE, CHOWN, and FOWNER, and is recommended for defense in depth. Scheduled backups (BACKUP_SCHEDULE) no longer need SETGID — Archiver uses supercronic, which runs jobs as the container user rather than forking with setgid like Debian's cron.
The stop_grace_period: 2m setting allows the container time to complete cleanup when stopped. When docker compose down or docker stop is called, Archiver will:
- Complete any running pre-backup hooks
- Run post-backup hooks to restore services (e.g., restart databases, remove snapshots)
- Terminate gracefully
If your post-backup hooks take longer than 2 minutes, increase this value accordingly.
| Variable | Required | Description |
|---|---|---|
BACKUP_SCHEDULE |
No | Standard 5-field cron expression for the backup pipeline (empty = manual mode) |
MAINTENANCE_SCHEDULE |
No | Cron expression for the maintenance pipeline (check + prune); unset = maintenance only runs via archiver maintenance |
TZ |
No | Timezone for scheduled backups and log timestamps (default: UTC) |
SYSTEMCTL_FORCE_BUS |
No | Set to 1 to enable systemctl access to host services via D-Bus socket (requires socket mounts, see above) |
The bundle password is not an environment variable. It is a file-based secret, read from /run/secrets/bundle_password (or the path in BUNDLE_PASSWORD_FILE); a container that still sets BUNDLE_PASSWORD in its environment fails fast at startup. Existing deployments that set it via environment: or env_file must move the value to that file and remove the env var. The rest of Archiver's configuration (service directories, storage targets, secrets, rotation) can also be supplied as environment variables and file-based secrets instead of, or on top of, the bundle. See Configuration Sources.
The entrypoint selects one of three modes based on the first container argument:
| Mode | How it's invoked | Behavior |
|---|---|---|
init |
docker run ... archiver:<tag> init |
Interactive setup: generates env-native materials, the recovery password, and an encrypted bundle. Exits when done. |
| default (daemon) | docker run ... archiver:<tag> (no args) |
Loads configuration (env-native and/or bundle), then either runs supercronic (if BACKUP_SCHEDULE and/or MAINTENANCE_SCHEDULE is set) or idles on tail -f /dev/null so you can docker exec in. |
run |
docker run ... archiver:<tag> run <subcommand> |
Loads configuration (env-native and/or bundle), then execs a single non-interactive subcommand and exits with that subcommand's exit code. Designed for Kubernetes Jobs / init containers and other CI flows. |
run mode only accepts subcommands whose exit codes form a meaningful contract: auto-restore, auto-restore-all, snapshot-exists, healthcheck, backup, and maintenance (synchronous paths intended for external schedulers — see Running a Backup from an External Scheduler). Any other subcommand is rejected with exit code 2.
forgejo.bryantserver.com/sisyphusmd/archiver:0.10.2- exact version (recommended; this line always names the current release)MAJOR.MINOR(e.g.0.9) - receives patch updates automaticallyMAJOR(e.g.0) - receives minor and patch updates automatically
The settings below define what to backup and where. Supply them as environment variables plus file-based secrets (the primary, env-native mode), through the encrypted bundle's config.sh (transitional), or a mix of the two (see Configuration Sources below). Env-native settings are edited wherever they live — your compose file, ConfigMap, or secret store; for editing a bundle's config.sh, see Editing Configuration.
The primary mode is env-native: environment variables carry the non-secret settings and files under /run/secrets carry the secrets — config stays under version control (compose file / ConfigMap) and secrets stay in a secret store. Underneath, an optional, transitional baseline can exist. In increasing precedence:
- The encrypted bundle (
config.sh+ keys, decrypted frombundle.tar.enc) — optional baseline and manual cold-restore copy: mount it and provide its password at/run/secrets/bundle_passwordand it becomes the baseline again. - Environment variables for non-secret settings, which override the bundle.
- Files for secrets, which override the bundle.
With no bundle at all, configuration is fully env-native — this is the recommended deployment. With a bundle and no overrides, behavior is exactly as it was pre-0.9.0. Because the layers stack, an existing bundle deployment can migrate one value at a time: set an env var or mount a secret file, confirm the backup still runs, and repeat until nothing depends on the bundle.
Non-secret settings (plain env vars). These override the bundle when set: SERVICE_DIRECTORIES, the non-secret STORAGE_TARGET_N_* fields (NAME, TYPE, LOCAL_PATH, SFTP_URL, SFTP_PORT, SFTP_USER, SFTP_PATH, B2_BUCKETNAME, S3_BUCKETNAME, S3_ENDPOINT, S3_REGION), CHECK_BACKUPS, PRUNE_BACKUPS, PRUNE_KEEP, PRUNE_EXHAUSTIVE_FREQUENCY, DUPLICACY_THREADS, and NOTIFICATION_SERVICE. As an env var, SERVICE_DIRECTORIES is a colon-delimited list rather than a bash array, for example SERVICE_DIRECTORIES=/srv/*/:/home/user/data/ (newlines also work, so a YAML block scalar is fine). The bundle's bash-array form is still read.
Secrets (files only). Secrets are never read from a plain env var (one would leak through /proc and docker inspect, and Archiver purges any it finds). Each secret is read from a file: <NAME>_FILE if set, otherwise /run/secrets/<lowercased name>. The secrets are BUNDLE_PASSWORD (the bundle decryption password, read from /run/secrets/bundle_password or BUNDLE_PASSWORD_FILE), STORAGE_PASSWORD, RSA_PASSPHRASE, PUSHOVER_USER_KEY, PUSHOVER_API_TOKEN, and each target's B2_ID, B2_KEY, S3_ID, and S3_SECRET. For example, STORAGE_PASSWORD reads /run/secrets/storage_password and STORAGE_TARGET_1_B2_KEY reads /run/secrets/storage_target_1_b2_key. STORAGE_PASSWORD must be at least 8 characters (a Duplicacy requirement). Because /run/secrets is the native mount path for Docker and Kubernetes secrets, a Compose or Swarm secrets: entry named to match (for example bundle_password) is picked up with no extra configuration.
Keys (files). Keys are always files under /opt/archiver/keys. In env-native mode (no bundle) the RSA keypair must be provided as files at /run/secrets/rsa_private_key and /run/secrets/rsa_public_key (override the paths with RSA_PRIVATE_KEY_FILE / RSA_PUBLIC_KEY_FILE). The SFTP keypair is optional, for sftp targets, at /run/secrets/ssh_private_key and /run/secrets/ssh_public_key (override with SSH_PRIVATE_KEY_FILE / SSH_PUBLIC_KEY_FILE; restore needs both halves). When a bundle is also present, mounted key files override the bundle's keys.
Starting env-native from scratch (no bundle, no archiver init)? Generate the RSA keypair yourself — Duplicacy needs the traditional PKCS#1 PEM format, and the passphrase must match your rsa_passphrase secret:
openssl genrsa -aes256 -passout pass:YOUR_RSA_PASSPHRASE -traditional -out rsa_private_key 2048
openssl rsa -in rsa_private_key -passin pass:YOUR_RSA_PASSPHRASE -pubout -out rsa_public_key(For sftp targets, also ssh-keygen -t ed25519 -N "" -f ssh_private_key, which writes ssh_private_key and ssh_private_key.pub — supply the latter as ssh_public_key.)
What you must not lose (disaster recovery). To restore after losing the host you need, stored somewhere that does not burn down with it: STORAGE_PASSWORD (unlocks the Duplicacy storage), RSA_PASSPHRASE + rsa_private_key (decrypt the file data), your storage-target settings (archiver.env or equivalents), and for sftp targets the SSH keypair. Missing any of the first three means the backups are permanently undecryptable. The Automatic Recovery Kit keeps all of it on every storage target for you — one password in your password manager covers everything. (A manual alternative remains: archiver bundle export produces bundle.tar.enc, which with its password carries the same material.)
To move an existing bundle deployment to env-native without hand-transcribing anything, run archiver migrate inside the container and copy the result out (the default output directory /opt/archiver/migrate is inside the container, not on the host):
docker exec archiver archiver migrate
docker cp archiver:/opt/archiver/migrate ./archiver-migrate
docker exec archiver rm -rf /opt/archiver/migrateThe copied files hold your secrets in plaintext — move them into your secret store, then delete the plain copies.
It writes the effective configuration as ready-to-use materials:
archiver.env: the non-secret settings asKEY=value, for a Composeenvironment:block or a Kubernetes ConfigMap.secrets/: one file per secret plus the RSA/SSH keys, to load as Docker secrets, a Kubernetes Secret, or openbao entries mounted under/run/secrets.
Load those, start the container without the bundle, and you are fully env-native. When converting an in-place Compose deployment, recreate the container with docker compose down && docker compose up -d rather than up --force-recreate: on images through 0.9.1 (which declare /opt/archiver/bundle as a volume) Compose carries the previous container's bundle mount into the recreated one, which then fails fast with bundle found ... but no bundle password. The move is reversible: bundle export is mode-agnostic, so from an env-native deployment you can regenerate a portable encrypted bundle at any time for cold restore.
Directories to backup, colon-delimited. Use * for subdirectories:
SERVICE_DIRECTORIES=/srv/*/:/home/user/data/
# /srv/*/ -> each subdirectory becomes its own repository
# /home/user/data/ -> a single repository(Newlines work as separators too, so a YAML block scalar is fine. A legacy bundle config.sh may still declare it as a bash array — both forms are read.)
Define multiple storage locations (local disk, SFTP, B2, S3):
Note: Storage names should only contain letters, numbers, and underscores. Other characters will be automatically sanitized (e.g.,
my-storage→my_storage).
# Primary storage (required)
STORAGE_TARGET_1_NAME="local"
STORAGE_TARGET_1_TYPE="local"
STORAGE_TARGET_1_LOCAL_PATH="/mnt/backup/storage"
# Secondary storage (optional)
STORAGE_TARGET_2_NAME="nas"
STORAGE_TARGET_2_TYPE="sftp"
STORAGE_TARGET_2_SFTP_URL="192.168.1.100"
STORAGE_TARGET_2_SFTP_PORT="22"
STORAGE_TARGET_2_SFTP_USER="backup"
STORAGE_TARGET_2_SFTP_PATH="/volume1/backups"
# Tertiary storage (optional)
STORAGE_TARGET_3_NAME="backblaze"
STORAGE_TARGET_3_TYPE="b2"
STORAGE_TARGET_3_B2_BUCKETNAME="my-bucket"
STORAGE_TARGET_3_B2_ID="keyID"
STORAGE_TARGET_3_B2_KEY="applicationKey"
# Quarternary storage (optional)
STORAGE_TARGET_4_NAME="hetzner"
STORAGE_TARGET_4_TYPE="s3"
STORAGE_TARGET_4_S3_BUCKETNAME="my-bucket"
STORAGE_TARGET_4_S3_ENDPOINT="endpoint"
STORAGE_TARGET_4_S3_REGION="none"
STORAGE_TARGET_4_S3_ID="id"
STORAGE_TARGET_4_S3_SECRET="secret"STORAGE_PASSWORD="encryption-password-for-duplicacy-storage"
RSA_PASSPHRASE="passphrase-for-rsa-private-key"Your configuration and secrets are the keys to every backup: lose them and the backups are unreadable. The recovery kit automates keeping them safe. It activates when the recovery_password secret is present (at /run/secrets/recovery_password, or via RECOVERY_PASSWORD_FILE) — the shipped compose template includes it.
Where the password comes from. archiver init generates it, includes it in the emitted secrets, and displays it once — save it in your password manager; it is the one credential you personally keep. An existing deployment enables the kit with one command plus a secrets: entry:
openssl rand -base64 24 > secrets/recovery_password # save a copy in your password managerIt 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); archiver refuses a violating configuration. To rotate it, replace the secret file and update your password manager — the next backup re-encrypts and re-uploads everywhere automatically.
What it does. After each backup, archiver assembles the kit — 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 your deployment manifests if mounted (below) — 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, with a companion README.txt holding the decrypt command. It is not inside duplicacy's storage format, so you can download it from any provider web UI or file browser with no tooling. The kit is re-encrypted and re-uploaded only when its content actually changes (or a new storage target appears); an unchanged config is a nightly no-op. (The name includes the hostname, so deployments sharing a storage target must have distinct hostnames — they already need that for distinct snapshot IDs.)
Capturing your deployment manifest. The kit cannot see files outside the container, so it cannot magically include your compose file or Kubernetes YAML — instead, anything mounted (read-only) at /opt/archiver/deployment/ is captured verbatim into the kit. Each runtime uses its native mechanism:
- Docker/Podman Compose:
- ./compose.yaml:/opt/archiver/deployment/compose.yaml:ro(in the shipped template — the manifest captures itself). - Kubernetes (incl. Flux/Argo GitOps): put the manifests in a ConfigMap and mount it at
/opt/archiver/deployment. With kustomize, aconfigMapGeneratorentry (files: [archiver.yaml]) keeps the ConfigMap — and therefore the kit — current on every git change automatically. - NixOS / systemd-managed containers: bind the service definition, e.g.
"/etc/nixos/services/archiver.nix:/opt/archiver/deployment/archiver.nix:ro".
In GitOps setups your manifest is already replicated in git; the kit's copy is for the total-loss case where the git host is gone too. Without a mounted manifest the kit still stands alone: RECREATE.txt lists every fact archiver knows about how the container must be put together, and the compose template covers the rest.
Recovery happens on any machine with stock openssl — no archiver, no other files, just the recovery password from your password manager: reach any one of your storage locations, download the kit, and run
openssl enc -d -aes-256-cbc -pbkdf2 -in archiver-recovery-kit-<hostname>.tar.enc | tar -xvf -It prompts for the password and yields archiver.env + secrets/ + RECREATE.txt (+ deployment/ with your manifests) — everything needed to recreate the deployment (the recovery password itself is included, so the recreated deployment maintains its kit immediately). From there, restore your data with archiver restore as usual.
archiver recovery-kit uploads on demand (useful right after setup); archiver recovery-kit force re-uploads everywhere even if unchanged. An upload failure is logged and notified but never fails the backup; the failed target is retried on the next run. The kit is write-only: unlike the bundle, nothing ever reads it back at runtime, so it is never a boot dependency.
On B2, each update creates a new file version; old versions age out per your bucket lifecycle rules (they are ciphertext, so lingering versions are harmless).
Storage verification and retention run as their own pipeline, on their own schedule, so they can never extend or block a backup run:
MAINTENANCE_SCHEDULE="0 13 * * *" # container env var (compose/K8s), NOT config.sh; unset = only via 'archiver maintenance'
CHECK_BACKUPS="true" # verify each storage (duplicacy check -all -fossils -resurrect)
PRUNE_BACKUPS="true" # enforce retention on each storage
PRUNE_KEEP="-keep 0:180 -keep 30:30 -keep 7:7 -keep 1:1"
PRUNE_EXHAUSTIVE_FREQUENCY="monthly" # off | daily | weekly | monthlyThe two pipelines run concurrently and rely 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 — so neither blocks the other: a backup never waits on maintenance, and maintenance runs on its schedule regardless of an in-progress copy. In the rare case a copy leg loses a race with a concurrent prune (a retryable error, never corruption or a partial copy, since Duplicacy writes the destination snapshot last), the copy is retried once automatically.
Default retention policy keeps:
- All backups younger than 1 day old
- 1 backup every 1 day for backups older than 1 day (
-keep 1:1) - 1 backup every 7 days for backups older than 7 days (
-keep 7:7) - 1 backup every 30 days for backups older than 30 days (
-keep 30:30) - Delete all backups older than 180 days (
-keep 0:180)
Format: -keep n:m means keep 1 snapshot every n days if the snapshot is at least m days old.
A normal prune is snapshot-metadata work (fast); -exhaustive additionally lists every chunk on the storage to garbage-collect orphans — expensive on remote storages (a full sftp listing can take hours) while orphans are rare, so it runs on its own interval. PRUNE_EXHAUSTIVE_FREQUENCY is evaluated per storage at each maintenance run: once the interval has elapsed since the last exhaustive success, that run's prune includes -exhaustive. An infrequent maintenance schedule simply fires it belatedly at the next opportunity. Force it any time with archiver maintenance exhaustive.
If multiple archiver deployments back up to the same storage target, only ONE should maintain it to avoid duplicate work and prune races:
- Set
PRUNE_BACKUPS="false"andCHECK_BACKUPS="false"in all but one deployment - The designated deployment maintains all snapshot IDs on the shared storage via the
-allflag
See the Duplicacy prune documentation for more details on the two-step fossil collection algorithm.
DUPLICACY_THREADS="10"Number of parallel upload/download threads for duplicacy operations. (Default: 4)
NOTIFICATION_SERVICE="Pushover"
PUSHOVER_USER_KEY="userKey"
PUSHOVER_API_TOKEN="apiToken"With BACKUP_SCHEDULE/MAINTENANCE_SCHEDULE set, the pipelines run automatically. Without them, run commands manually.
docker exec -it archiver archiver logs
docker logs --tail 20 -f archiverdocker exec archiver archiver status
docker exec archiver archiver healthcheckdocker exec archiver archiver backup --detach # run a backup in the background
docker exec archiver archiver logs # follow it
docker exec archiver archiver maintenance # run check + prune nowdocker exec archiver archiver pause
docker exec archiver archiver resume
docker exec archiver archiver stopdocker exec -it archiver archiver bundle export
docker exec -it archiver archiver bundle importarchiver backup # Run the backup pipeline now (synchronous, exit code propagates)
archiver backup --detach # Run it in the background (follow with 'archiver logs')
archiver maintenance # Run per-storage check + prune now (synchronous)
archiver maintenance exhaustive # Same, forcing the full-listing exhaustive prune
archiver stop [backup|maintenance|all] # Stop a pipeline gracefully (default all: both pipelines)
archiver stop --immediate # Stop immediately (skip cleanup); combine with a target
archiver pause # Pause backup (experimental)
archiver resume # Resume paused backup (experimental)
archiver logs # Follow backup logs
archiver status # Both pipelines' state + per-storage last check/prune ages
archiver bundle export # Create encrypted config/keys bundle
archiver bundle import # Import from encrypted bundle
archiver restore # Restore data from backup (interactive)
archiver auto-restore # Restore one snapshot from backup (non-interactive, env-driven)
archiver auto-restore-all # Restore every service in one pass (non-interactive)
archiver snapshot-exists # Check if a snapshot exists on any storage target
archiver migrate [DIR] # Write the effective config as env-native materials (env + secret files)
archiver recovery-kit [force] # Upload the encrypted recovery kit to every storage target now
archiver healthcheck # Check system health (Docker HEALTHCHECK uses this; on Kubernetes wire it as an exec probe)
archiver help # Show helpFor most users, a long-lived container with BACKUP_SCHEDULE set is the simplest way to get scheduled backups — the in-container scheduler runs archiver backup on schedule, and you don't have to manage anything. Skip this section unless you specifically need to drive scheduling from outside the container.
If your environment already owns scheduling — e.g., a Kubernetes CronJob, a GitHub Actions scheduled workflow, a systemd timer on the host, or any other platform that spawns a short-lived container per run and expects a meaningful exit code — use the entrypoint's run backup mode instead. It loads the configuration (env-native or bundle), runs a backup synchronously, and exits with the backup's result code. The container terminates when the backup finishes; your scheduler then reports success or failure based on the exit code.
Exit codes:
0— backup completed1— lock contention (another backup already in progress) or catastrophic startup failure- non-zero — see stderr / logs for details
Example: one-shot docker run (env-native: archiver.env + secrets/ as emitted by init or migrate)
docker run --rm \
--env-file /path/to/archiver.env \
-v /path/to/secrets:/run/secrets:ro \
-v /path/to/host/backup-dir:/mnt/backup-dir \
forgejo.bryantserver.com/sisyphusmd/archiver:0.10.2 run backup(Bundle mode instead: replace the first two lines with -v /path/to/bundle/dir:/opt/archiver/bundle and -v /path/to/bundle_password:/run/secrets/bundle_password:ro.) The same pattern drives maintenance from an external scheduler: run maintenance blocks through the per-storage check + prune and propagates its exit code.
Example: Kubernetes CronJob
apiVersion: batch/v1
kind: CronJob
metadata:
name: archiver
spec:
schedule: "0 3 * * *"
concurrencyPolicy: Forbid
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: archiver
image: forgejo.bryantserver.com/sisyphusmd/archiver:0.10.2
args: ["run", "backup"]
envFrom:
- configMapRef: { name: archiver-config } # the archiver.env keys (non-secret settings)
volumeMounts:
- { name: archiver-secrets, mountPath: /run/secrets, readOnly: true }
- { name: backup-dir, mountPath: /mnt/backup-dir }
volumes:
- name: archiver-secrets
secret: { secretName: archiver-secrets } # storage_password, rsa_passphrase, rsa_private_key, rsa_public_key, ...
- name: backup-dir
persistentVolumeClaim: { claimName: backup-data }Create the ConfigMap and Secret straight from what init or migrate emitted: kubectl create configmap archiver-config --from-env-file=archiver.env and kubectl create secret generic archiver-secrets --from-file=secrets/. (Bundle mode also works: mount the bundle tar as a Secret at /opt/archiver/bundle plus a bundle_password key under /run/secrets.)
The Pod lives for the duration of one backup and exits. If the backup fails, the Pod exits non-zero and Kubernetes marks the Job failed — the usual CronJob semantics apply.
Why not
--detachhere? A detached backup returns exit0immediately, before any real work happens — fine interactively, useless to an external scheduler that needs a meaningful exit code.run backupblocks until the backup finishes.
Before restoring, ensure you have a volume mount for the restore destination directory. The restore script will interactively prompt you for:
- Which storage target to restore from
- Snapshot ID to restore
- Local directory path (where to restore the files)
- Which revision to restore
# Check status first (ensure no backup is running)
docker exec archiver archiver status
# Run interactive restore
docker exec -it archiver archiver restoreThe restore destination can be any path accessible within the container. If you need to restore to a new location not currently mounted, add a volume mount and restart the container first.
For a one-time restore without modifying your running container, start a temporary container and exec the interactive restore into it:
docker run -d --name archiver-restore \
--env-file /path/to/archiver.env \
-v /path/to/secrets:/run/secrets:ro \
-v /path/to/restore/destination:/mnt/restore \
forgejo.bryantserver.com/sisyphusmd/archiver:0.10.2
docker exec -it archiver-restore archiver restore
docker rm -f archiver-restoreRestoring from a bundle instead (the cold-restore path): replace the first two option lines with -v /path/to/bundle/dir:/opt/archiver/bundle and -v /path/to/bundle_password:/run/secrets/bundle_password:ro.
When prompted for the local directory path during restore, enter the container path (e.g., /mnt/restore). The restored files will appear on your host at /path/to/restore/destination.
Snapshot IDs are <hostname>-<service directory basename> (e.g. backup-server-nextcloud). When restoring on a different machine, run the container with hostname: set to the ORIGINAL value or pass the full SNAPSHOT_ID explicitly.
For automated disaster recovery flows (e.g. Kubernetes init containers), Archiver exposes two non-interactive commands driven by environment variables. Exit codes are the machine-readable answer; stdout is informational.
Probes every configured storage target for SNAPSHOT_ID and short-circuits on the first hit. Useful to gate a restore on whether a backup is actually available.
| Env Var | Required | Description |
|---|---|---|
SNAPSHOT_ID |
Yes | Snapshot ID to look up |
Exit codes:
0— snapshot exists on at least one target (printsEXISTS)1— no target has the snapshot (printsNOT FOUND)2— all targets unreachable or invalid env (printsUNDETERMINED)3— an Archiver backup is in progress; check skipped
Iterates storage targets in configured order and restores from the first target that has the requested snapshot. Once a restore begins, it does not fall through to another target — a failure at that point exits 1.
| Env Var | Required | Description |
|---|---|---|
SNAPSHOT_ID |
Yes | Snapshot ID to restore |
LOCAL_DIR |
Yes | Destination directory inside the container |
REVISION |
No | Specific revision number, or latest (default) |
STORAGE_TARGET |
No | Pin to a single target by name or numeric id |
OVERWRITE |
No | Non-empty enables -overwrite |
DELETE_EXTRA |
No | Non-empty enables -delete |
HASH_COMPARE |
No | Non-empty enables -hash |
IGNORE_OWNERSHIP |
No | Non-empty enables -ignore-owner |
RUN_RESTORE_SERVICE |
No | Non-empty runs ./restore-service.sh after a successful file restore (DB reload, stack restart); its exit code propagates |
RESTORE_THREADS |
No | Override download thread count (default matches DUPLICACY_THREADS) |
Exit codes:
0— snapshot restored (and, ifRUN_RESTORE_SERVICEset,restore-service.shsucceeded)1— snapshot not found on any reachable target, the restore itself failed, orrestore-service.shfailed2— all targets unreachable, or invalid env3— an Archiver backup is in progress; restore skipped
Example (gate-and-restore against a running container):
docker exec \
-e SNAPSHOT_ID=myservice \
archiver archiver snapshot-exists \
&& docker exec \
-e SNAPSHOT_ID=myservice \
-e LOCAL_DIR=/mnt/restore \
archiver archiver auto-restoreFor Kubernetes Jobs, init containers, or one-shot docker run invocations, use the entrypoint's run mode (see Container Modes). The configuration is loaded (env-native or bundle), the subcommand runs, and the container's exit code equals the subcommand's exit code:
# Probe whether a backup is available
docker run --rm \
--env-file /path/to/archiver.env \
-v /path/to/secrets:/run/secrets:ro \
-e SNAPSHOT_ID=myservice \
forgejo.bryantserver.com/sisyphusmd/archiver:0.10.2 run snapshot-exists
# Restore a snapshot into a mounted destination
docker run --rm \
--env-file /path/to/archiver.env \
-v /path/to/secrets:/run/secrets:ro \
-e SNAPSHOT_ID=myservice \
-e LOCAL_DIR=/mnt/restore \
-e OVERWRITE=1 \
-v /path/to/restore/destination:/mnt/restore \
forgejo.bryantserver.com/sisyphusmd/archiver:0.10.2 run auto-restore(Bundle mode: swap the --env-file + secrets mount for -v /path/to/bundle/dir:/opt/archiver/bundle and -v /path/to/bundle_password:/run/secrets/bundle_password:ro.)
In Kubernetes this is typically an init container on the workload pod: probe with run snapshot-exists, and if a backup exists, run run auto-restore to seed the data volume before the main container starts. The exit-code contract means the pod's restartPolicy and init-container failure handling behave as expected.
Create service-backup-settings.sh in any service directory:
#!/bin/bash
# Custom file filters
DUPLICACY_FILTERS_PATTERNS=(
"+*.txt"
"-*.tmp"
"+*"
)
# Run before backup
service_specific_pre_backup_function() {
echo "Dumping database..."
docker exec postgres-container pg_dump -U user dbname > backup.sql
}
# Run after backup
service_specific_post_backup_function() {
echo "Cleaning up..."
rm -f backup.sql
}Create restore-service.sh in any service directory to run post-restore tasks:
#!/bin/bash
# Runs after restoration completes
echo "Importing database..."
docker exec postgres-container psql -U user -d dbname -f /backup/dump.sql
echo "Setting permissions..."
chown -R 1000:1000 /mnt/restored-data
echo "Starting services..."
docker compose up -d- Legacy to Docker Migration - Migrating from v0.3.2-v0.6.5 to Docker-only v0.7.0
- Uninstalling Legacy Installation - Removing legacy installation after migration
- Editing Configuration - How to edit config in Docker environment
- Local Storage Setup - Adding local disk as primary backup target
- SSH Key Management - Creating and managing SSH keys for SFTP
Archiver is free and open-source software licensed under GNU AGPL-3.0.
Archiver uses the Duplicacy CLI v3.2.5 binary as an external tool. Duplicacy is licensed separately under its own terms:
- Free for personal use and commercial trials
- Requires a CLI license for non-trial commercial use ($50/computer/year from duplicacy.com)
What counts as commercial use? Backing up files related to employment or for-profit activities.
Note: Restore and management operations (restore, check, copy, prune) never require a license. Only the backup command requires a license for commercial use.
If you're using Archiver commercially, please purchase a Duplicacy CLI license to support the project that makes this tool possible