🎯 Stats powered by ClonePulse
GitHub: per2jensen/dar-backup-image
Docker Hub: per2jensen/dar-backup
dar-backup-image packages dar-backup, dar, PAR2, and their runtime dependencies into a versioned, auditable Docker image.
It has two equally important purposes:
- Run backups and restores today in a clean, isolated environment without installing
dar, Python tooling, or PAR2 on the host. - Preserve a known-working restore environment for the future by saving a versioned image alongside your archives.
The second use is deliberate: the image is a restore time capsule. If you need to recover an archive years from now, you can use the packaged toolchain preserved with the backup instead of hunting for compatible packages or reconstructing an old software environment.
For long-term archival use, save a specific versioned image rather than relying only on :latest. The mutable :latest tag is useful for current operations and receives regular security refreshes; a pinned and locally archived version is the artifact you want to retain with your backups.
A helper script, scripts/save-dar-backup-image.sh, is provided for this purpose. It selects the highest-numbered release or refresh in doc/build-history.json and saves that versioned image as a compressed tar archive. Its current integrity limitations are documented in Preserve the restore environment with your archives.
The image also works well as an everyday backup runner for cron jobs, systemd timers, and CI pipelines. Its default entrypoint is dar-backup; dar, par2, or an interactive shell can be invoked directly by overriding the entrypoint.
At its core, dar-backup wraps dar and PAR2 for reliable FULL, DIFF, and INCR backups. It validates archives, performs restore tests, manages catalog databases, and can generate redundancy files to protect archives against bit rot.
The image preserves documentation from the installed dar-backup
distribution. It works without network access, backup configuration, or
mounted volumes, so a saved image remains self-documenting years later.
IMAGE=per2jensen/dar-backup:latest
# Discover the documentation (a bare invocation does the same thing)
docker run --rm "$IMAGE" docs
docker run --rm "$IMAGE"
# Display a topic bundled by dar-backup
docker run --rm "$IMAGE" docs overview
docker run --rm "$IMAGE" docs getting-started
# Show paths, component versions, provenance, and project links
docker run --rm "$IMAGE" docs --path
docker run --rm "$IMAGE" info
# Verify every recorded documentation SHA-256
docker run --rm "$IMAGE" docs --verifyUse docs --list to see the topics available in a particular image. Topic
names follow the documentation actually present in its installation; for example,
the expanded dar_backup/doc/ collection first appears with dar-backup
1.1.11.
The stable filesystem location is /usr/share/doc/dar-backup/. It contains:
image/README.md— this image repository's exact build-context READMEREADME.md, the Markdown description embedded in package metadatadoc/, every file included underdar_backup/doc/ordar_backup/docs/licenses/and rawMETADATAINDEX.mdand the integrity/provenance recordMANIFEST.json
/usr/share/doc/dar-backup-image/README.md and /README.md link to the
embedded image operating guide for simple filesystem discovery. Use
docs image to print it and docs overview for the dar-backup overview.
The standalone executables dar-backup-image-docs and
dar-backup-image-info remain usable when the container entrypoint is
overridden. The OCI labels org.opencontainers.image.documentation,
org.dar-backup.documentation.command, and
org.dar-backup.documentation.path provide discovery without starting the
image.
The image contains man pages for dar and par2 which make the image self-documenting for all commands used to restore or backup.
- Long-term restore time capsule, preserve a known-working
dar-backup/dar/ PAR2 environment with your archives - Reproducible backup runner, no host installation of
dar, Python tooling, or PAR2 required - Self-documenting, all
dar-backup,dar-backup-image,darandpar2documentation is included and easily discoverable by future users - Versioned and auditable, released images are tested, scanned, signed, and accompanied by an SBOM
- Stateless and portable, archive the image itself and move it with your backup sets
- Built-in configuration, automatically loads
/etc/dar-backup/dar-backup.confunless overridden - Ready for automation, usable from cron, systemd timers, and CI pipelines
Current operation vs. long-term preservation
Use
:latestwhen you want the current tested and security-refreshed image.For disaster recovery years into the future, pin a version and save the image itself alongside your archives.
- dar-backup image — verifiable backups and a long-term restore time capsule
- dar-backup-image
- Table of Contents
- Preserve the restore environment with your archives
- Hands-on Demo:
dar-backupin a Container darversions- Recent releases uploaded to Docker Hub
- Release Pipeline and Supply Chain Security
- Understanding Volume Mounts and Backup Definitions
- Useful links
- License
- Docker Hub image repo
- Description
- Directory Layout and Permissions
- Environment Variables
- How to test
- Image Tags
- Volumes / Runtime Configuration
- Usage Example
- run-backup.sh
- 🔍 Discover Image Metadata
- Image deep diving
- Common
dar-backupcommands - Using the Makefile
- Roadmap
- Software this project benefits from
Long-term backups need more than durable data. They also need a practical way to run the software required to inspect, verify, repair, and restore that data.
A DAR archive is intentionally portable, and dar itself remains the essential restore tool. The container adds another layer of resilience by preserving a complete, known-working environment containing:
dardar-backup- PAR2 tooling
- Python and required runtime libraries
- the image's baked-in configuration
- build metadata identifying the exact component versions
For a backup intended to survive many years, preserve the image as data, not merely as a Docker Hub tag.
A typical workflow is:
# Pin a released image
VERSION=0.5.27
IMAGE=per2jensen/dar-backup:${VERSION}
# Pull the exact version
docker pull "$IMAGE"
# Save it as a portable compressed artifact
docker save "$IMAGE" | gzip > "dar-backup-image-${VERSION}.tar.gz"Years later, on a machine with a compatible container runtime:
gunzip -c dar-backup-image-0.5.27.tar.gz | docker load
docker run --rm --entrypoint dar per2jensen/dar-backup:0.5.27 --versionThe supplied scripts/save-dar-backup-image.sh
automates the pull and docker save operation. It is intended for cron or a
systemd timer and saves a new archive when build history gains a higher-numbered
release or weekly refresh.
The current helper is an availability tool, not a complete provenance archiver.
It does not yet compare the pulled tag with the recorded digest, run Cosign,
or preserve the build-history record, SBOM, signature, and attestation beside
the image. Cosign material stored in the registry is not included by
docker save. The helper does create and subsequently validate a SHA-256 file
for each compressed archive, and it validates an existing archive before
reporting success. Verify the immutable digest and Cosign identity as described
below and save the relevant evidence beside the archive.
When creating an archive manually with the commands above, generate and verify its checksum yourself:
sha256sum "dar-backup-image-${VERSION}.tar.gz" \
> "dar-backup-image-${VERSION}.tar.gz.sha256"
sha256sum --check "dar-backup-image-${VERSION}.tar.gz.sha256"Container images are architecture-specific. Record the image architecture with
docker image inspect --format '{{.Architecture}}' "$IMAGE" and ensure the
future recovery host can run it natively or through tested emulation.
The practical preservation model is therefore:
DAR archive slices
+
PAR2 recovery data
+
dar_manager catalogs
+
documentation
+
saved versioned dar-backup image
=
a self-contained recovery set
This does not mean Docker Hub should be treated as part of your disaster-recovery dependency chain. The opposite is the goal: save the image locally so recovery does not depend on Docker Hub, PyPI, Ubuntu repositories, the original host, or this GitHub repository still being available.
The signed image digest, SBOM, build history, and Rekor record provide provenance only when they are preserved and verified with the image; the locally saved image provides availability.
Curious how it all works in practice?
Check out the step-by-step demo, which walks through:
- A full backup from mounted directories
- Archive listing and contents inspection
- Selective file restore (e.g.,
.JPGonly) - Output logs, par2 generation, and verification
All performed using docker run, no host installation required.
Starting with dar-backup-image 0.5.15, dar (v2.7.18) is compiled from source rather than using Ubuntu 24.04’s older package.
The pinned dar release is compiled with the feature set verified by this
project, including zstd, lz4, Argon2, GPGME, and remote repository support.
The Dockerfile verifies the source tarball using Denis Corbin’s GPG key, checks all critical features, and only includes the built binary if everything passes.
To view the embedded dar version:
docker run -it --entrypoint /usr/local/bin/dar dar-backup:<tag> --versionExpected (abridged) output for tag 0.5.16, confirming core capabilities:
dar version 2.7.19, Copyright (C) 2002-2025 Denis Corbin
Using libdar 6.8.3 built with compilation time options:
gzip compression (libz) : YES
Strong encryption (libgcrypt): YES
Public key ciphers (gpgme) : YES
Large files support (> 2GB) : YES
Remote repository (libcurl) : YES (HTTPS, zstd, SSH, HTTP/2)Weekly image refreshes (
:latest) are not listed here, see the full audit trail in build-history.json.
| Tag | dar-backup |
dar |
Release date | Git Revision | Docker Hub | Note |
|---|---|---|---|---|---|---|
| 1.0.0 | 1.1.11 | 2.7.21 | 2026-09-19 | 3549149c17707d3134dfabfc3f83877edc32233a | tag:1.0.0 | - |
| 1.0.0-rc2 | 1.1.11 | 2.7.21 | 2026-09-18 | 3d4661aa9fb29d6459c05f740655af1aacf301d4 | tag:1.0.0-rc2 | - |
| 1.0.0-rc1 | 1.1.11 | 2.7.21 | 2026-09-06 | 9a27ba88a11ae340c9ea95ec72e6271269c5ff2e | tag:1.0.0-rc1 | - |
| 0.9.1 | 1.1.11 | 2.7.21 | 2026-08-26 | 5576be82b9fedf44df6c9e3d0eb6b6c274ec5274 | tag:0.9.1 | - |
| 0.9.0 | 1.1.11 | 2.7.21 | 2026-08-16 | cfaadb1 | tag:0.9.0 | - |
The table is generated from doc/build-history.json. Refresh it at any time
with python3 scripts/update_readme_releases.py, or verify it without writing
with python3 scripts/update_readme_releases.py --check.
Every image released to Docker Hub is produced by a fully automated GitHub Actions workflow, no manual docker push, no local machine involvement. The pipeline enforces a strict sequence of gates before any image becomes publicly available.
- Build, The
dar-backup:devimage is built using the repository's version pins. DAR is compiled from its GPG-verified source tarball; release images install the pinneddar-backuppackage from PyPI. - Test, The full pytest suite runs against the dev image. The pipeline halts if any test fails.
- Finalize, The tested dev image is converted to the release tag with corrected OCI version and reference labels. Its application filesystem is retained without rebuilding packages.
- Verify, Focused checks validate OCI labels, the embedded
dar-backup --version, the exact application revision, and the SHA-256 of/LICENSEbefore publication. The full pytest suite is not repeated against the relabeled image. - SBOM, Syft generates a CycloneDX JSON Software Bill of Materials from the local image before it leaves the runner.
- Vulnerability scan, Grype scans the SBOM and fails the release if any High or Critical vulnerability is found. Results are uploaded to the GitHub Security tab as SARIF.
- Candidate push, Only the immutable version tag is initially pushed to Docker Hub;
:lateststill identifies the previous known-good image. - Cosign signing, The candidate is signed by digest (not by mutable tag) using cosign keyless mode.
- SBOM attestation, The SBOM is attached to that digest as a signed in-toto attestation via cosign.
- Remote sanity check, The immutable digest is pulled back from Docker Hub and executed. Its
dar-backup --versionoutput and OCI image-version label must match exactly. - Promotion, Only the remotely verified digest is pushed as
:latest;:latestis then pulled and required to resolve to the signed digest. - Rollback, If the candidate push, signing, attestation, or remote sanity check fails, the attempted unverified version tag is removed and the previous
:latestremains untouched.
Manual releases and weekly image refreshes use the same tested publication
component and a shared concurrency lock, so their signing, rollback, remote
verification, :latest promotion, and always-run Docker credential cleanup
cannot drift apart or race.
Keyless signing means the project does not manage or store a long-lived signing private key in GitHub Secrets or on disk. Cosign generates an ephemeral key for the signing operation, and Fulcio issues a short-lived certificate bound to GitHub's OIDC identity for the workflow run.
Every signature is permanently recorded in Rekor, Sigstore's public, append-only transparency log. This means:
- The signing identity is public and auditable, the Rekor entry proves exactly which GitHub workflow, repository, branch, and run produced the signature.
- Signing events are tamper-evident, inclusion in the append-only log makes unexpected or inconsistent events auditable; it does not make compromised OIDC identities, workflows, or signing infrastructure impossible.
- No long-lived project key management burden, there is no persistent project signing key to rotate or retain.
- Anyone can verify identity and content, without an account or contacting the author. Verify an immutable digest; Docker Hub remains a distribution and availability dependency until the image and evidence are archived locally.
cosign verify per2jensen/dar-backup:<tag> \
--certificate-identity-regexp='^https://github\.com/per2jensen/dar-backup-image/\.github/workflows/(release|image-refresh)\.yml@refs/heads/main$' \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com"A successful verification proves the image was signed on main by this
repository's manual-release or weekly-refresh workflow. Verification should use
the immutable digest recorded in doc/build-history.json when auditing a
specific publication.
cosign verify-attestation per2jensen/dar-backup:<tag> \
--type cyclonedx \
--certificate-identity-regexp='^https://github\.com/per2jensen/dar-backup-image/\.github/workflows/(release|image-refresh)\.yml@refs/heads/main$' \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
| jq '.payload | @base64d | fromjson | .predicate'This retrieves and verifies the signed CycloneDX SBOM attached to the image, listing every package and library bundled inside.
Each release summary in the GitHub Actions tab includes a direct link to the Rekor entry for that release, for example:
https://search.sigstore.dev/?logIndex=1273042416
The entry records the signing certificate, the image digest that was signed, the GitHub workflow identity, the run URL, and the exact commit SHA, providing a complete, tamper-evident audit trail from source code to published image.
The -v flag that maps host directories into the container is the key decision that controls
how many backup definitions you can use, and which host paths are reachable at all.
In short:
- Map host
/→ container/dataread-only, the entire host tree is visible; multiple definitions can target different subtrees, but the backup destination and special filesystems can also reappear below/data. Use explicit exclusions. - Map a single subdirectory → container
/dataread-only, only that directory is visible inside the container. - Map several sources to distinct paths below
/dataread-only, multiple definitions work without exposing the full host; this is the preferred multi-source layout.
See doc/dar-backup-mount-scenarios.md for a full explanation with diagrams, worked examples, and a comparison table.
| Topic | Link |
|---|---|
dar-backup |
dar-backup on GitHub |
dar-backup-image |
dar-backup-image |
Docker Hub repo |
Docker Hub |
dar |
Disk ARchive |
dar-backup-image is licensed under GPL-3.0-or-later.
The complete license text is available in the repository LICENSE file
and is embedded in every image at /LICENSE. Print it without installing or starting
dar-backup:
Copyright and licensing information is also recorded per file using
SPDX and the REUSE specification.
Clonepulse files retain their MIT licence, while bundled upstream DAR source
archives retain their upstream GPL-2.0-or-later licensing. Run reuse lint and
python3 scripts/validate_license_headers.py to validate the complete policy.
docker run --rm --entrypoint cat per2jensen/dar-backup:latest /LICENSE
# The release and refresh workflows require this exact SHA-256 before publishing.
docker run --rm --entrypoint sha256sum \
per2jensen/dar-backup:latest /LICENSE
# 3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986 /LICENSEYou can see publicly available dar-backup docker images on Docker Hub.
Those fond of curl can do this:
curl -s https://hub.docker.com/v2/repositories/per2jensen/dar-backup/tags | jq '.results[].name'A minimal, Dockerized backup runner using dar (Disk ARchive) and dar-backup, ready for automated or manual archive creation and restore.
The image is tested by the full CI pipeline on pushes to main and on pull
requests targeting main. Weekly
refreshes keep :latest current with Ubuntu updates and reject images with any
known High or Critical vulnerability detected by the configured Grype scan.
This image includes:
- dar
- par2
- python3
- setpriv
- dar-backup (my
darPython based wrapper) - Clean Ubuntu 24.04 base; image size varies with the pinned packages and architecture
- Explicit non-root execution and privilege drop via
setpriv
The run-backup.sh script uses a standardized directory structure to ensure backups work consistently across environments. These directories are host-mounted into the container so that all backup archives, definitions, and restored files remain accessible outside Docker.
By default, the script expects (or creates) the following structure relative to WORKDIR:
$WORKDIR/backups→ Mounted as/backups(DAR archives and logs)$WORKDIR/backup.d→ Mounted as/backup.d(backup definitions; current preflight requires write access)$WORKDIR/data→ Mounted read-only as/data(source data to back up)$WORKDIR/restore→ Mounted as/restore(restored files)
The script resolves its directory paths in the following priority order:
- If an explicit directory environment variable is set (
DAR_BACKUP_DIR,DAR_BACKUP_D_DIR,DAR_BACKUP_DATA_DIR, orDAR_BACKUP_RESTORE_DIR), that value is used. - Otherwise, if
WORKDIRis set, each directory defaults to a subdirectory ofWORKDIR(e.g.,$WORKDIR/backups). - If
WORKDIRis unset or empty, the script exits before creating directories or running Docker.
This allows full flexibility: you can set WORKDIR once for a standard layout, or override specific directories individually.
The Docker runner does not claim support for FUSE-backed host paths. Such paths
are not covered by the integration tests and may be inaccessible to the Docker
daemon when it prepares a bind mount. This happens before the container starts,
so RUN_AS_UID and RUN_AS_GID do not grant the daemon access.
This limitation belongs to the Docker bind-mount boundary; it does not imply the
same limitation when running the PyPI dar-backup application directly on the
host.
Files created through run-backup.sh use the selected numeric host UID and GID:
-
By default,
RUN_AS_UIDandRUN_AS_GIDare set to your current UID and GID. -
These are passed to Docker via
--user "$RUN_AS_UID:$RUN_AS_GID". -
Running as
root(UID 0) is disallowed; the script will exit with an error. -
You can override these values for special cases, such as:
-
Group-based backups:
RUN_AS_GID=$(getent group backupgrp | cut -d: -f3)
Ensures all archives are group-writable. -
Service accounts:
RUN_AS_UID=1050 RUN_AS_GID=1050 ./scripts/run-backup.sh -t FULLMatches ownership for automated or scheduled jobs.
-
Here are three common configurations, depending on your use case:
For single-user systems:
WORKDIR=$HOME/dar-backup
$HOME/dar-backup/backups # DAR archives and logs
$HOME/dar-backup/backup.d # Backup definition files
$HOME/dar-backup/data # Source data to back up
$HOME/dar-backup/restore # Restored filesPermissions: Owned entirely by your user (default behavior).
For teams or shared servers, use a group to manage permissions:
WORKDIR=/srv/dar-backup
# Group ownership
chown -R :backupgrp /srv/dar-backup
chmod -R 2770 /srv/dar-backupEnvironment for group-based runs:
RUN_AS_UID=$(id -u)
RUN_AS_GID=$(getent group backupgrp | cut -d: -f3)All members of backupgrp can write backups while keeping data private.
For scheduled or service-based backups:
WORKDIR=/mnt/backupsOwned by a dedicated backup user (UID 1050:GID 1050)
Run with:
RUN_AS_UID=1050 RUN_AS_GID=1050 ./scripts/run-backup.sh -t FULLThis ensures consistent ownership for cron jobs or automated workflows.
For full environment variable documentation, see the header comments in scripts/run-backup.sh.
Here’s a quick reference for all environment variables used by the script:
| Variable | Default | Purpose |
|---|---|---|
IMAGE |
per2jensen/dar-backup:latest |
Docker image tag to use for the backup container. |
WORKDIR |
Required | Base directory for all backup-related paths. |
RUN_AS_UID |
Current user's UID | UID passed to Docker to avoid root-owned files. |
RUN_AS_GID |
Current user's GID | GID passed to Docker for correct file group ownership. |
DAR_BACKUP_DIR |
$WORKDIR/backups |
Host directory for DAR archives and logs (mounted at /backups). |
DAR_BACKUP_D_DIR |
$WORKDIR/backup.d |
Host directory for backup definitions (mounted at /backup.d; current preflight requires write access). |
DAR_BACKUP_DATA_DIR |
$WORKDIR/data |
Host source directory (mounted read-only at /data). |
DAR_BACKUP_RESTORE_DIR |
$WORKDIR/restore |
Host directory for restored files (mounted at /restore). |
DOCKER_PULL |
false |
Set exactly to true to pull IMAGE before running; true and false are the only valid values. |
For details on behavior, UID/GID handling, and usage examples, see the comments in scripts/run-backup.sh.
See dev.md for development-image build and test instructions.
All images are published to Docker Hub.
Every build, whether a release or a scheduled refresh, is recorded in build-history.json.
| Tag | Description | Docker Hub | Example Usage |
|---|---|---|---|
:latest |
Most recently verified release or refresh; mutable by design | ✅ Yes | docker pull per2jensen/dar-backup:latest |
:x.y.z |
Stable semantic-versioned release | ✅ Yes | docker pull per2jensen/dar-backup:1.0.0 |
:x.y.z-rcN |
Release candidate; not selected as a weekly refresh base | ✅ Yes | docker pull per2jensen/dar-backup:1.0.0-rc1 |
:x.y.z-N |
Numeric weekly refresh of stable release x.y.z |
✅ Yes | docker pull per2jensen/dar-backup:1.0.0-1 |
:dev |
Local development version; may be incomplete | ❌ No | docker run dar-backup:dev |
For normal day-to-day use, :latest is convenient because it tracks the most
recently verified publication. For reproducible operation and recovery, use a
versioned tag or, preferably, its immutable digest.
Publishing a release candidate also promotes its verified digest to :latest.
Weekly refreshes then pause while that RC is the newest build-history record.
Operators who require stable-only deployments must pin a stable version or digest
instead of following :latest during an RC window.
For long-term recovery, do not preserve only the :latest reference. Save a specific versioned image, for example :0.5.27, as a Docker/OCI archive alongside the backup set. A mutable registry tag is a locator; the saved image is the recovery artifact.
The refresh workflow runs every Saturday. It selects the newest build-history
record only when its tag is a stable x.y.z release or an existing numeric
x.y.z-N refresh. If the newest record is a prerelease such as x.y.z-rc1, the
workflow stops without publishing or moving :latest. For an eligible base,
each refresh:
- Rebuilds the exact application source commit recorded for that base, while picking up Ubuntu base-image and package updates
- Runs the full test suite, the pipeline halts if any test fails
- Refuses to reuse an existing Git or Docker Hub refresh tag and fails closed if remote tag availability cannot be established
- Requires the finalized image's OCI revision label to equal the complete recorded
application SHA and verifies the baked
/LICENSESHA-256 - Scans with Grype, the refresh is aborted if any High or Critical vulnerability is found
- Signs the image with cosign keyless signing and records the entry in the Rekor transparency log
- Attaches a signed CycloneDX SBOM attestation
- Publishes as both
:latestand a versioned refresh tag (e.g.:0.5.22-1,:0.5.22-2) - Atomically publishes the build-history commit and its annotated Git tag; the annotation records the immutable application SHA and signed image digest
- Updates the cosign badge,
failedmeans the most recent signing attempt was rejected and rolled back; the previously published:latestmay remain valid
The build-history.json file contains the full audit trail for every release and refresh, including digests, Rekor log entries, and Grype scan results.
The default dar-backup.conf baked into the image assumes the directories mentioned below.
The locations should be mounted with actual directories on your machine for backups.
| Directories in file system | Directories in container | Purpose |
|---|---|---|
| /some/dir/to/backup/ | /data |
Source directory for backup |
| /keep/backups/here/ | /backups |
dar archives and .par2 files are put here |
| /restore/tests/ | /restore |
Optional restore target |
| /backup/definitions/ | /backup.d |
Contains backup definition files |
The mapping between physical directories on your file system and the expected directories inside the container is performed by the -v /physical/dir:/container/dir options (see example below).
💡 How you mount
/datadetermines which backup definitions work. See Understanding Volume Mounts and Backup Definitions.
--slice sets the maximum size of each numbered DAR archive file. Choose it
primarily for storage, transfer, verification, and repair granularity:
- Smaller slices produce more
.darand PAR2 files, but each damaged or transferred unit is smaller. - Larger slices reduce file count, but make each PAR2 creation, verification, or repair operation cover a larger input and can increase both runtime and peak memory.
- Slice size is not a direct DAR memory reservation. DAR documents memory growth separately for compression block size and thread count; there is no supported rule such as “10 GiB slices require 10 GiB RAM.” See the DAR manual.
dar-backup runs PAR2 once per DAR slice. This bounds each PAR2 problem by one
slice rather than the complete archive, but it also means slice size materially
affects the largest PAR2 operation. The bundled PAR2 0.8.1 selects a default
memory limit from detected physical memory when no -m option is supplied. A
container memory limit may be lower than that detected value, so test the chosen
slice size under the same Docker memory constraints used in production and watch
for OOM termination.
The 5G, 7G, 10G, and 12G values elsewhere in this repository are examples, not universal recommendations. Start conservatively on memory-constrained hosts and measure a representative FULL backup plus PAR2 creation and repair before increasing the size. The large-scale test guide explains what its peak-memory measurements do and do not prove.
The tracked large-scale history can also be explored through the read-only
Datasette metrics dashboard. From a checkout, run
./run_metrics_dashboard.sh and open the localhost URL printed by the launcher.
For the host layout where /home is Btrfs and /data is ZFS, the
cross-filesystem test guide covers FULL backup,
PAR2 verification, restore comparison in both directions, non-root permission
testing, and its separate local JSONL history.
Determine whether you want to build an image yourself or use one from Docker Hub. Development-image build instructions are in dev.md.
# Use your own locally built image
export IMAGE=dar-backup:dev
# Or use the most recently verified publication from Docker Hub
export IMAGE=per2jensen/dar-backup:latest
# Or pin to a specific version
VERSION=0.5.25.1; export IMAGE=per2jensen/dar-backup:${VERSION}Now run dar-backup in the container
# Run it (from script or manually)
# Configuration
export DATA_DIR=/tmp/test-data # the data to backup
export BACKUP_DIR=/tmp/test-backups # the directory that keeps the backups
export RESTORE_DIR=/tmp/test-restore # the directory used for restore tests during backup verification
export BACKUP_D_DIR=/tmp/test-backup.d # the directory keeping the `backup definitions`
mkdir -p "$DATA_DIR" "$BACKUP_DIR" "$RESTORE_DIR" "$BACKUP_D_DIR"
cat > "$BACKUP_D_DIR/default" <<'EOF'
-am
-R /data
-z5
-n
--slice 7G
--cache-directory-tagging
EOF
docker run --rm \
-e RUN_AS_UID="$(id -u)" \
-e RUN_AS_GID="$(id -g)" \
-v "$DATA_DIR":/data:ro \
-v "$BACKUP_DIR":/backups \
-v "$RESTORE_DIR":/restore \
-v "$BACKUP_D_DIR":/backup.d \
"$IMAGE" \
-F --log-stdoutThe image automatically uses /etc/dar-backup/dar-backup.conf unless you override it.
To use another config file you have multiple options:
- Modify the baked-in and build a new image.
- Use
--configto select another file, for example/backups/dar-backup.conf. With the mounts above, place that file at$BACKUP_DIR/dar-backup.confon the host. - Let DAR_BACKUP_CONFIG point to a config file.
The container uses setpriv to drop root privileges. Pass
both -e RUN_AS_UID="$(id -u)" and -e RUN_AS_GID="$(id -g)" when
invoking Docker directly. run-backup.sh supplies both automatically.
This script runs a backup using a dar-backup Docker image.
It runs a backup based on the specified type (FULL, DIFF, INCR) with the following features:
Uses the baked-in dar-backup.conf; see the
dar-backup configuration documentation.
Uses the .darrc file from the PyPI package added to the image, see image details here
.darrc contents
It prints log messages to standard output.
Expected directory structure when running this script:
WORKDIR/
├── backups/ # Where backups are stored
├── backup.d/ # Backup definitions
├── data/ # Data to backup
└── restore/ # Where restored files will be placed
WORKDIR is required and becomes the base directory. The script fails clearly
when it is unset or empty.
These directories are host-mounted into the container so your data and archives remain accessible:
| Host Directory (default) | Container Mount | Purpose |
|---|---|---|
$WORKDIR/backups |
/backups |
DAR archives and log files |
$WORKDIR/backup.d |
/backup.d |
Backup definitions; current preflight requires write access |
$WORKDIR/data |
/data (read-only) |
Source data for the backup |
$WORKDIR/restore |
/restore |
Destination for restore tests |
You can override any of these paths by setting the environment variables:
DAR_BACKUP_DIR, DAR_BACKUP_D_DIR, DAR_BACKUP_DATA_DIR, DAR_BACKUP_RESTORE_DIR.
If an override is unset, its corresponding directory below WORKDIR is used.
More info on backup definitions in general
View supplied default backup definition
If IMAGE is not set, the script defaults to
per2jensen/dar-backup:latest.
See the available images on Docker Hub.
If RUN_AS_UID or RUN_AS_GID is not set, it defaults to the current
user's numeric UID or GID. Running the helper as root is not allowed.
The run-backup.sh script supports selecting a specific backup definition file, which allows you to maintain multiple dataset or policy definitions.
Each backup definition file resides in:
$DAR_BACKUP_D_DIR (default: $WORKDIR/backup.d)
and is automatically mounted into the container at /backup.d.
Note on daily backups per definition:
dar-backupwill only create oneFULL, oneDIFF, and oneINCRbackup per definition per calendar day.
- You can run all three types (FULL → DIFF → INCR) on the same day.
- A second run of the same type (e.g., another FULL) will be skipped to avoid overwriting the existing archive.
- To force a new run, use
cleanupso DAR slices, PAR2 data, and catalogue state are handled consistently. Do not delete only the.darfiles.
To specify a backup definition, use the -d or --backup-definition option:
WORKDIR=/path/to/workdir ./scripts/run-backup.sh -t FULL -d my-backup-definitionThis instructs dar-backup to load:
$DAR_BACKUP_D_DIR/my-backup-definition
instead of the default definition (default).
If no -d option is supplied, the script falls back to the default definition.
The run-backup.sh script generates a minimal default definition file at:
$DAR_BACKUP_D_DIR/default
if none exists.
The script passes the chosen definition to dar-backup using:
--backup-definition "<name>"The script constructs a Bash array so an optional definition remains one safely quoted argument:
DOCKER_ARGS=( "$BACKUP_FLAG" "--log-stdout" "--verbose" )
if [[ -n "$BACKUP_DEF" ]]; then
DOCKER_ARGS+=( "--backup-definition" "$BACKUP_DEF" )
fi
docker run ... "$IMAGE" "${DOCKER_ARGS[@]}"This:
- Adds
--backup-definition "<name>"ifBACKUP_DEFis non-empty. - Skips it entirely if no
-dwas provided (dar-backup then uses the default definition).
- Create a new definition file:
mkdir -p "$HOME/dar-backup/backup.d"
cat > "$HOME/dar-backup/backup.d/projects" <<'EOF'
-R /data/projects
-z5
-am
--slice 5G
EOF- After creating a FULL backup for the definition, run a differential backup:
WORKDIR=$HOME/dar-backup ./scripts/run-backup.sh -t DIFF -d projectsThe backup will:
- Store archives in
$HOME/dar-backup/backups. - Use
/backup.d/projectsas the definition. - Retain ownership based on
RUN_AS_UIDandRUN_AS_GID.
Why is my backup skipped?
Only oneFULL, oneDIFF, and oneINCRbackup can be created per definition per day.
If a run is skipped, use the documentedcleanupcommand; do not remove only the.darfiles and leave associated state behind.Permission issues on host files?
EnsureRUN_AS_UIDandRUN_AS_GIDmatch the desired owner.
If unsure, runid -uandid -gto get your UID and GID.Definition not found?
Make sure yourbackup.d/<name>file exists (or let the script auto-createdefault).
WORKDIR=/path/to/your/workdir \
IMAGE=per2jensen/dar-backup:latest \
./scripts/run-backup.sh -t FULL -d backup_definitionLearn what's inside the dar-backup image: program versions, build metadata, and available versions.
Run the image with different entrypoints to check the bundled versions of dar-backup, dar, and par2:
VERSION=0.5.25.1; IMAGE=per2jensen/dar-backup:${VERSION}
# dar-backup version
docker run --rm --entrypoint "dar-backup" "$IMAGE" -v
# dar version
docker run --rm --entrypoint dar "$IMAGE" --version
# par2 version
docker run --rm --entrypoint par2 "$IMAGE" --version
# Or get them all in one go:
docker run --rm --entrypoint "" "$IMAGE" \
bash -c "dar-backup -v; dar --version; par2 --version"docker pull per2jensen/dar-backup:latest
docker inspect per2jensen/dar-backup:latest | jq '.[0].Config.Labels'Inspect the provenance-critical labels without relying on a stale example:
docker inspect per2jensen/dar-backup:latest \
| jq '.[0].Config.Labels | {
image_version: .["org.opencontainers.image.version"],
source_revision: .["org.opencontainers.image.revision"],
base_digest: .["org.opencontainers.image.base.digest"],
dar_backup_version: .["org.dar-backup.version"],
dar_version: .["org.dar.version"],
install_source: .["org.dar-backup.install-source"],
license: .["org.opencontainers.image.licenses"]
}'Current release and refresh workflows require source_revision to be the full
application commit SHA and install_source to be pypi.
# Show first 100 available tags
curl -s 'https://hub.docker.com/v2/repositories/per2jensen/dar-backup/tags?page_size=100' \
| jq -r '.results[].name' | sort -VAlthough dar-backup is the primary CLI inside the container, you can also run dar directly from the image to take manual backups or inspect archives, perfect for advanced workflows or testing.
Here's a minimal example of how to use dar directly:
export DATA_DIR=/tmp/test-data
export BACKUP_DIR=/tmp/test-backups
export VERSION=0.5.25.1
export IMAGE=per2jensen/dar-backup:${VERSION}
mkdir -p "$DATA_DIR" "$BACKUP_DIR"
touch "$DATA_DIR/TEST.txt"
docker run --rm \
-v "$DATA_DIR":/data:ro \
-v "$BACKUP_DIR":/backups \
--entrypoint dar \
"$IMAGE" -c /backups/myarchive -R /dataExample output
No terminal found for user interaction. All questions will be assumed a negative answer (less destructive choice), which most of the time will abort the program.
--------------------------------------------
1 inode(s) saved
including 0 hard link(s) treated
0 inode(s) changed at the moment of the backup and could not be saved properly
0 byte(s) have been wasted in the archive to resave changing files
0 inode(s) with only metadata changed
0 inode(s) not saved (no inode/file change)
0 inode(s) failed to be saved (filesystem error)
0 inode(s) ignored (excluded by filters)
0 inode(s) recorded as deleted from reference backup
--------------------------------------------
Total number of inode(s) considered: 1
--------------------------------------------
EA saved for 0 inode(s)
FSA saved for 1 inode(s)
--------------------------------------------This shows that even without dar-backup, you can still invoke dar manually, helpful for debugging, recovery scenarios, or power-user workflows.
Tip: You can also run PAR2 directly with
--entrypoint par2when needed.
These are arguments to the image's default dar-backup entrypoint. Supply the
same mounts and UID/GID settings used for backups:
RUNTIME_ARGS=(
-e RUN_AS_UID="$(id -u)"
-e RUN_AS_GID="$(id -g)"
-v "$DATA_DIR":/data:ro
-v "$BACKUP_DIR":/backups
-v "$RESTORE_DIR":/restore
-v "$BACKUP_D_DIR":/backup.d
)docker run --rm "${RUNTIME_ARGS[@]}" "$IMAGE" --full-backupdocker run --rm "${RUNTIME_ARGS[@]}" "$IMAGE" --differential-backupdocker run --rm "${RUNTIME_ARGS[@]}" "$IMAGE" --incremental-backupdocker run --rm "${RUNTIME_ARGS[@]}" "$IMAGE" --listdocker run --rm "${RUNTIME_ARGS[@]}" "$IMAGE" --list-contents ARCHIVE_NAMEdocker run --rm "${RUNTIME_ARGS[@]}" "$IMAGE" --restore ARCHIVE_NAMESee dev.md for the canonical development-image build and test
instructions, including how to install dar-backup from a local wheel.
After publishing a release, test the exact image on Docker Hub (ignoring local builds):
make IMAGE=per2jensen/dar-backup:0.5.15 test-pulledThis:
Pulls the image from Docker Hub.
Runs the full pytest suite (no local build).
Releases are fully automated via the Manual Docker Release GitHub Actions workflow, no local docker push required.
Qualify the exact committed release candidate locally before pushing it to
main:
test -z "$(git status --porcelain)" || exit 1
make dev-nuke
make verify-dev-image
make IMAGE=dar-backup:dev test-nobuild
docker run --rm dar-backup:dev docs --verifyThis validates:
- The image builds from scratch without cached layers
- Provenance and component metadata match exactly
- The complete test suite passes against that exact image
- The embedded documentation bundle is intact
For an optional additional local finalization, license, SBOM, and vulnerability scan check, run:
make FINAL_VERSION="$(cat IMAGE_VERSION)" finalThis is not a simulation of the remote release transaction. Publication, signing, attestation, rollback, promotion, and housekeeping are exercised only by the GitHub workflow.
When ready, trigger the release by dispatching the workflow from GitHub Actions
(workflow_dispatch). The workflow will:
- Lock the build to the full commit selected from
mainand validateIMAGE_VERSION - Build, test, and scan, hard gate on High/Critical vulnerabilities
- Push
per2jensen/dar-backup:VERSIONas a candidate - Sign the candidate and attach its SBOM attestation
- Roll back a failed candidate without moving the previous
:latest - Promote the successfully signed digest to
:latest - Atomically publish build history, documentation, evidence, and the source-pointing Git tag
- Create the GitHub Release with non-empty SBOM and SARIF assets
Note: Do NOT manually create the git tag before triggering the workflow, it is created automatically after all steps succeed.
The complete operator procedure, failure-state table, source policy, RC acceptance gate, and GitHub Release recovery procedure are in doc/Release.md.
Follow dev.md to build and test a PyPI-sourced development image. Before release, qualify the clean committed candidate locally:
make dev-nuke
make verify-dev-image
make IMAGE=dar-backup:dev test-nobuildFINAL_VERSION defaults to dev for ordinary development. Release-related
Make targets reject that default and require an exact match with the committed
IMAGE_VERSION file. Weekly refreshes use the separate guarded
refresh-final-noscan target, which permits only BASE_VERSION-N where the
base comes from the latest doc/build-history.json entry. The same history
record supplies the application Git revision, which is resolved and checked
out as a full commit SHA. Refreshes intentionally do not consult
IMAGE_VERSION.
Trigger the release:
- Set
IMAGE_VERSIONto the new version, updateChangelog.md, commit and push - Dispatch the Manual Docker Release workflow from GitHub Actions
Verify the published image:
make IMAGE=per2jensen/dar-backup:x.y.z test-pulled1.0.0-rc2 is the final planned release candidate. Version 1.0.0 will follow
after RC2 passes the complete release-candidate acceptance gate.
- Integrate DAR 2.8
- Use Ubuntu 26.04 instead of 24.04 as base image and for local development
- Investigate and document compatibility between 1.0 and 1.1 backup archives and restore environments (1.0 uses ubuntu 24.04 / dar-2.7, 1.1 will use ubuntu 26.04 / dar-2.8)
- Investigate DAR's encryption features
- Possibly implement a dar supported encryption scheme
- Investigate deterministic generated test cases so test runs can be reproduced across versions and during debugging