A label-driven backup system for Docker containers using restic
Project description
dorestic
A label-driven backup system for Docker containers using restic.
Containers opt in to backups by adding labels to their compose file. A host-side
Python script discovers opted-in containers, runs lifecycle hooks (e.g. database
dumps), resolves volume mounts to host paths, and spawns a disposable
restic/restic:latest container for each backup scope. The restic container has
no Docker socket and can only read the specific paths mounted into it.
Installation
uv tool install dorestic
Or directly from a git repository:
uv tool install git+https://github.com/USER/dorestic.git
Quick Start
-
Generate an example config:
dorestic --init ~/.config/dorestic/
Edit
~/.config/dorestic/config.ymland setrepositoryandpassword_file. -
Create the password file:
echo -n 'your-restic-password' > /etc/backup/restic-password chmod 600 /etc/backup/restic-password
-
Add labels to any container you want backed up:
services: my-db: image: postgres:17 volumes: - ./pgdata:/var/lib/postgresql/data labels: backup.enable: "true" backup.container.paths: "/var/lib/postgresql/data" backup.container.on_start: "pg_dumpall -U postgres --clean > /var/lib/postgresql/data/dump.sql" backup.container.on_complete: "rm -f /var/lib/postgresql/data/dump.sql" backup.host.paths: ".@1"
-
Run a backup:
dorestic
-
(Optional) Add to cron:
0 3 * * * dorestic
Configuration
Config is loaded from the first location found:
./config.yml(current directory)$XDG_CONFIG_HOME/dorestic/config.yml(default:~/.config/dorestic/config.yml)
Or pass a path explicitly: dorestic /path/to/config.yml
Run dorestic --init [PATH] to write an example config with documentation.
repository: /mnt/backup/backup1
password_file: /etc/backup/restic-password
# restic_image: restic/restic:latest
# on_start: /path/to/on_start.sh
# on_complete: /path/to/on_complete.sh
# retention:
# daily: 7
# weekly: 4
# monthly: 12
# host_groups:
# - tag: documents
# paths: [/mnt/fileserver/share]
# exclude: ["*.tmp"]
The password file is mounted read-only into the restic container via
RESTIC_PASSWORD_FILE — the password never appears on the command line or in
ps output.
| Field | Required | Description |
|---|---|---|
repository |
Yes | Restic repository path |
password_file |
Yes | Path to a file containing the restic password |
restic_image |
No | Docker image for restic (default: restic/restic:latest) |
on_start |
No | Command to run before the backup starts. If it exits non-zero, the backup is aborted. |
on_complete |
No | Command to run after the entire backup. Env: $DORESTIC_EXIT_CODE, $DORESTIC_LOGFILE |
retention |
No | Snapshot retention policy (default: 7 daily, 4 weekly, 12 monthly) |
host_groups |
No | Host-only backup groups (see below) |
Architecture
Host (dorestic) Restic container (docker run --rm)
┌───────────────────────────────┐ ┌────────────────────────────────┐
│ Load config.yml │ │ No Docker socket │
│ Acquire process lock │ │ No host filesystem access │
│ Discover labeled containers │ │ Only explicitly mounted paths │
│ Run on_start hooks │ spawn │ Data paths mounted read-only │
│ Resolve volume mounts → paths │ ────► │ Password via mounted file │
│ docker cp for unmounted paths │ │ Runs restic backup │
│ Expand depth-limited paths │ │ Deleted on completion (--rm) │
│ Run on_complete hooks │ └────────────────────────────────┘
│ Forget/prune/check │
│ Run on_complete callback │
└───────────────────────────────┘
The backup script runs on the host and interacts with Docker via the Python SDK.
For each backup scope, it spawns a fresh restic/restic:latest container with
docker run --rm, mounting only the necessary data paths (read-only) and the
restic repository. The container is automatically removed on completion.
Labels
Each container can define two backup scopes — container (paths inside the container, resolved via volume mounts) and host (paths relative to the compose project directory).
| Label | Required | Description |
|---|---|---|
backup.enable |
Yes | "true" to opt in |
backup.container.paths |
No | Comma-separated container-internal paths to back up |
backup.container.exclude |
No | Comma-separated restic exclude patterns for container scope |
backup.container.on_start |
No | Command run inside the container before container backup. Env: $DORESTIC_TAG |
backup.container.on_complete |
No | Command run inside the container after container backup. Env: $DORESTIC_TAG, $DORESTIC_EXIT_CODE |
backup.container.shell |
No | Shell used for container hooks (default: sh) |
backup.host.paths |
No | Comma-separated paths relative to the compose project directory |
backup.host.exclude |
No | Comma-separated restic exclude patterns for host scope |
backup.host.on_start |
No | Command run on the host before host backup. Env: $DORESTIC_TAG |
backup.host.on_complete |
No | Command run on the host after host backup. Env: $DORESTIC_TAG, $DORESTIC_EXIT_CODE |
backup.suppress-mount-warning |
No | "true" to silence warnings when container paths fall back to docker cp (see below) |
Container paths
Paths in backup.container.paths are resolved to host paths by inspecting the
container's volume mounts. The longest prefix match is used when multiple mounts
overlap.
If a path has no matching volume mount, dorestic cannot resolve it to a host
path directly. Instead, it falls back to docker cp to extract the data to a
temporary staging directory before backing it up. This is slower and uses extra
disk space, so dorestic logs a warning when it happens. If this is intentional
(e.g. the container stores data outside any mounted volume), set
backup.suppress-mount-warning to "true" to silence the warning.
Host paths and depth
backup.host.paths are resolved relative to the container's compose project
directory (com.docker.compose.project.working_dir). Append @N to limit
depth:
| Value | Meaning |
|---|---|
.@1 |
Top-level files in the compose dir (compose file, .env, etc.) |
. |
Entire compose directory, recursive |
../shared-config@2 |
Sibling directory, 2 levels deep |
Lifecycle
For each container, the two scopes (container and host) are independent:
container.on_start ─→ container backup ─→ container.on_complete
host.on_start ──────→ host backup ──────→ host.on_complete
- Each scope runs independently — a failing
container.on_startdoes not affect the host scope, and vice versa. - If
on_startfails, the backup for that scope is skipped, buton_completestill runs with the failure code. on_completefailures log a warning but don't affect the backup's exit code.- Hooks run inside the target container via
docker exec, so they have access to that container's tools (e.g.pg_dumpallin a postgres image).
Snapshots
Each container gets up to two tagged restic snapshots per run:
<name>:container— container-internal paths (resolved to host via mounts)<name>:host— host/compose directory paths
# List all snapshots
restic snapshots
# List snapshots for a specific container scope
restic snapshots --tag my-db:container
# List host-scope snapshots
restic snapshots --tag my-db:host
# Restore a specific scope
restic restore --tag my-db:container latest --target /tmp/restore/
Retention policy is configurable in config.yml. Default: 7 daily, 4 weekly,
12 monthly. Applied per tag via --group-by tags.
Host Backup Groups
Host-only backup groups (not tied to any container) are configured in the
host_groups section of config.yml:
host_groups:
- tag: documents
paths:
- /mnt/fileserver/share
- /mnt/fileserver/share-private
exclude:
- "*.tmp"
- tag: docker-config
paths:
- /mnt/fileserver/docker
exclude:
- "aosp-mirror/mirror"
on_start: /config/pre-docker-backup.sh
on_complete: /config/notify-documents.sh
| Field | Required | Description |
|---|---|---|
tag |
Yes | Restic tag for this group's snapshot |
paths |
Yes | List of absolute host paths to back up |
exclude |
No | List of restic exclude patterns |
on_start |
No | Command to run before backup. Env: $DORESTIC_TAG. Failure skips the backup. |
on_complete |
No | Command to run after backup. Env: $DORESTIC_TAG, $DORESTIC_EXIT_CODE |
Examples
Database with dump
services:
database:
image: postgres:17
volumes:
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
labels:
backup.enable: "true"
backup.container.paths: "/var/lib/postgresql/data"
backup.container.exclude: "*.log"
backup.container.on_start: "pg_dumpall -U postgres --clean > /var/lib/postgresql/data/dump.sql"
backup.container.on_complete: "rm -f /var/lib/postgresql/data/dump.sql"
backup.host.paths: ".@1"
The dump file is created by on_start, included in the restic snapshot, then
cleaned up by on_complete — regardless of whether the backup succeeded.
Application with uploads
services:
app:
image: myapp:latest
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
labels:
backup.enable: "true"
backup.container.paths: "/usr/src/app/upload"
backup.container.exclude: "*.tmp,thumbs/"
backup.host.paths: ".@1"
Exclude pattern tips
- Patterns use restic's native
--excludesyntax, interpreted relative to the backup root(s). - Container and host scopes are separate restic invocations —
backup.host.excludenever affects the container scope or other containers. - Do not compress or encrypt files before handing them to restic. Restic already does both, and pre-processed data destroys chunk-level deduplication.
Dependencies
- Python 3.12+
dockerPython SDKpyyaml- Docker daemon with
restic/restic:latestimage available
Testing
uv run pytest tests/ -v
Tests require a running Docker daemon and the restic/restic:latest image.
Unit tests (no Docker) can be run in isolation:
uv run pytest tests/test_unit.py -v
File Structure
dorestic/
├── src/dorestic/
│ ├── __init__.py # Public API re-exports
│ ├── __main__.py # Entry point for python -m dorestic
│ ├── cli.py # Argument parsing and --init
│ ├── models.py # Dataclasses and constants
│ ├── config.py # Config file loading and validation
│ ├── config.yml.example # Bundled example config (used by --init)
│ ├── restic.py # Restic container invocation
│ ├── docker.py # Docker container operations
│ ├── paths.py # Path resolution utilities
│ └── backup.py # Backup orchestration
├── tests/
│ ├── conftest.py # Fixtures and test infrastructure
│ ├── test_unit.py # Pure function tests (no Docker)
│ ├── test_docker.py # Docker integration tests
│ └── test_backup.py # Backup execution and lifecycle tests
├── pyproject.toml
├── pyrightconfig.json
└── .gitignore
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dorestic-0.4.4.tar.gz.
File metadata
- Download URL: dorestic-0.4.4.tar.gz
- Upload date:
- Size: 31.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0fe75998dce92dd0780802596412182f47b01736eb440e7a631d42a4afb5b387
|
|
| MD5 |
5a87948b284efc68c3e794572c3b8671
|
|
| BLAKE2b-256 |
6306224bcf6d551a3871974237ab3d2776d3e6a39a29cf32715c1bf908a5b6c8
|
Provenance
The following attestation bundles were made for dorestic-0.4.4.tar.gz:
Publisher:
publish.yml on cornejo/dorestic
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dorestic-0.4.4.tar.gz -
Subject digest:
0fe75998dce92dd0780802596412182f47b01736eb440e7a631d42a4afb5b387 - Sigstore transparency entry: 2126257228
- Sigstore integration time:
-
Permalink:
cornejo/dorestic@156fca40acaa374cc55cf17c2ca19be1e7aecdbe -
Branch / Tag:
refs/tags/v0.4.4 - Owner: https://github.com/cornejo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@156fca40acaa374cc55cf17c2ca19be1e7aecdbe -
Trigger Event:
push
-
Statement type:
File details
Details for the file dorestic-0.4.4-py3-none-any.whl.
File metadata
- Download URL: dorestic-0.4.4-py3-none-any.whl
- Upload date:
- Size: 21.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fa41c579397dc26ce4a7e863a2e8d8625e33abe2c47d7b8031427fb15d9f8b9a
|
|
| MD5 |
caaeb8374affdbf25b920fbff603ff6f
|
|
| BLAKE2b-256 |
718c220d340909a419088bc9e73f0d721dada0710d271a8fce5e0277618e2f01
|
Provenance
The following attestation bundles were made for dorestic-0.4.4-py3-none-any.whl:
Publisher:
publish.yml on cornejo/dorestic
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dorestic-0.4.4-py3-none-any.whl -
Subject digest:
fa41c579397dc26ce4a7e863a2e8d8625e33abe2c47d7b8031427fb15d9f8b9a - Sigstore transparency entry: 2126257366
- Sigstore integration time:
-
Permalink:
cornejo/dorestic@156fca40acaa374cc55cf17c2ca19be1e7aecdbe -
Branch / Tag:
refs/tags/v0.4.4 - Owner: https://github.com/cornejo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@156fca40acaa374cc55cf17c2ca19be1e7aecdbe -
Trigger Event:
push
-
Statement type: