Imported from ZoopaMario/podman-compose (
duplicati/AGENTS.md). Install upstream withnpx skills add ZoopaMario/podman-compose --skill duplicati. Copyright stays with the author.
Duplicati Backup Agent Notes
Scope
Applies to duplicati/ only. Purpose: keep per-stack backups predictable and safe with a rootful Duplicati container while other stacks remain rootless under systemd --user.
Environment Snapshot (non-secret)
- User runtime model:
- user:
orangepi(uid=1000,gid=1000) XDG_RUNTIME_DIR=/run/user/1000
- user:
- Host/runtime:
- Linux kernel:
6.6.89-cixonaarch64 - Podman:
4.3.1 - podman-compose:
1.0.3 - systemd:
252
- Linux kernel:
- Operational implication:
- Duplicati runs rootful via sudo (podman/podman-compose).
- Other stacks remain rootless, with storage under
%h/.local/share/containers/storage. - on-demand sockets live under
%t/ondemand(/run/user/1000/ondemand) - Do not run a rootless Duplicati instance in parallel.
Source Of Truth
- Runtime behavior:
duplicati/bin/backup
- Per-stack declarations:
duplicati/stacks/*.sh
- Operator docs:
duplicati/README.mdsystemd/README.md
- Scheduler units:
systemd/duplicati-backup.servicesystemd/duplicati-backup.timer
- On-demand activation units:
systemd/zoopa-ondemand@.socketsystemd/zoopa-ondemand@.servicesystemd/zoopa-ondemand-stop@.servicesystemd/zoopa-ondemand-dir.service
Current Backup Automation State
- Automated schedule exists via timer:
OnCalendar=*-*-* 03:30:00Persistent=true
- Automated target scope is explicit and intentionally narrow:
systemd/duplicati-backup.serviceruns:%h/podman-compose/duplicati/bin/backup cryptpad
- Policy in this repo:
- manually validate each new app stack first
- then append the stack name to the
ExecStartargument list - do not switch automation to
backup allunless explicitly requested
Core Orchestrator Model (bin/backup)
- One stack config file per stack:
stacks/<stack>.sh. - CLI supports:
listall- explicit stack/unit args
- Arguments are normalized by stripping path,
-stack.service,.service. - Global lock:
- lock file at
${XDG_RUNTIME_DIR:-/tmp}/duplicati-backup.lock - prevents concurrent runs.
- lock file at
- Duplicati health gate:
- container must be running (rootful podman)
duplicati-server-util list-backupsmust succeed.
- Stack execution flow:
- Optional freeze of on-demand socket/service.
- Stop stack unit.
- Wait for project containers to stop by label
io.podman.compose.project=<PROJECT_LABEL>. - Run configured Duplicati jobs.
- Wait for completion.
- Restore stack based on restore policy.
- Optionally restore on-demand socket.
- Run optional verification hook.
- Backup completion logic is conservative:
- prefers
run --waitwhen supported - otherwise polls status
- treats explicit idle (
Active task: None|Emptyor idle text) as success - unknown status format does not auto-pass; it keeps waiting until timeout.
- prefers
- Timeout defaults:
- enqueue timeout (
DUPLICATI_RUN_TIMEOUT):25s - max backup wait (
DUPLICATI_WAIT_TIMEOUT):21600s(6h) - per-stack start/stop waits default to
180sunless overridden. - podman command override (
PODMAN_CMD):sudo -n podmanby default
- enqueue timeout (
- Cleanup trap:
- best-effort restoration of stack and on-demand socket state after failures.
Stack Config Contract (duplicati/stacks/*.sh)
- Required:
STACK_NAME(fallback filename)UNIT(fallback<stack>-stack.service)PROJECT_LABEL(fallback<stack>)DUPLICATI_JOBS(non-empty)
- Optional:
STOP_TIMEOUT,START_TIMEOUTRESTORE_POLICY=previous|always(defaultprevious)FREEZE_ONDEMAND=yes|nowithONDEMAND_SOCKETand optionalONDEMAND_SERVICE- hooks:
stack_pre_stop,stack_post_stopstack_pre_backup,stack_post_backupstack_pre_start,stack_post_startstack_verify
- Existing examples:
cryptpad.sh:RESTORE_POLICY=previousFREEZE_ONDEMAND=yes- freezes
zoopa-ondemand@cryptpad.socketandzoopa-ondemand@cryptpad.service
nextcloud.sh:- maintenance mode toggle via
occaround stop/start
- maintenance mode toggle via
On-demand Interaction Model
- Reverse proxy integration:
nginxmounts/run/user/1000/ondemandas/sockets:ro
- On-demand stacks can be identified by filename pattern in
systemd/:cryptpad-stack-ondemand.envhomarr-stack-ondemand.envromm-stack-ondemand.env
- Operational behavior:
.socketlistens on%t/ondemand/%i.sock- first request triggers
.service, which can start%i-stack.service - idle helper may stop stack if started by on-demand marker
- backup flow may freeze/unfreeze socket to avoid surprise reactivation mid-backup
Storage Layout Conventions (backup-relevant)
- Common persistent roots used in compose files:
${DATA_ROOT:-/mnt/data}for app data on shared storage${LOCAL_ROOT:-/srv}for local-disk state
- Duplicati stack mounts:
- local state DB:
${LOCAL_ROOT:-/srv}/duplicati/data->/data - source tree root:
${DATA_ROOT:-/mnt/data}mounted read-only - backup destination:
/mnt/backup - rootless named volumes path read-only:
${HOST_HOME}/.local/share/containers/storage/volumes->/podman-volumes
- local state DB:
- Implication:
- if an app uses named volumes for DB state, include corresponding volume path data in backup scope (directly or via dumps).
Backup-Relevant Stack Inventory (current repo)
cryptpad:- app data bind mounts under
${DATA_ROOT}/cryptpad/* - no separate DB container in compose.
- app data bind mounts under
nextcloud:- app data/config at
${LOCAL_ROOT}/nextcloud/*and${DATA_ROOT}/nextcloud/data - MariaDB data in named volume
nextcloud-db-data - Redis configured as cache (
appendonly no, no snapshot save).
- app data/config at
forgejo:- persistent paths under
${DATA_ROOT}/forgejo/{conf,data,runner-data} - no separate DB service in current compose.
- persistent paths under
vaultwarden:- persistent path
${DATA_ROOT}/vaultwarden:/data.
- persistent path
jellyfin:- config/cache under
${LOCAL_ROOT}/jellyfin/{config,cache} - media under
${DATA_ROOT}/MEDIAbind mount.
- config/cache under
shlink:- MariaDB data at
${DATA_ROOT}/shlink/db.
- MariaDB data at
immich:- uploads at
${UPLOAD_LOCATION} - Postgres data in named volume
immich-db-data - model cache in named volume
model-cache(rebuildable but expensive).
- uploads at
open-webui:- app backend data at
${DATA_ROOT}/open-webui/owui - qdrant data at
${DATA_ROOT}/open-webui/qdrant - mcpo config at
${DATA_ROOT}/open-webui/mcpo - Postgres data in named volume
owui-postgres-data - valkey configured non-persistent.
- app backend data at
romm:- persistent content/config/assets under
${DATA_ROOT}/romm/*and media library path - MariaDB data in named volume
mysql_data - redis data in named volume
romm_redis_data(cache-ish but currently persisted).
- persistent content/config/assets under
litellm:- Postgres data in external named volume
litellm-db-data.
- Postgres data in external named volume
openqa/prod:- Postgres bind mount
/srv/openqa/prod/postgres - additional persistent dirs:
./share,./db,./pool,./testresults,./images, config dirs.
- Postgres bind mount
parabol:- Postgres bind mount
/srv/parabol/postgres/pgdata - additional config bind mount under
/mnt/data/parabol/config.
- Postgres bind mount
pihole:- persistent bind
${LOCAL_ROOT}/pihole/etc-pihole - unbound state named volume
unbound_state.
- persistent bind
grafana:${DATA_ROOT}/grafana/data${DATA_ROOT}/prometheus/data- config mounts under
${DATA_ROOT}/grafana/confand${DATA_ROOT}/prometheus.
homarr:- persistent
${DATA_ROOT}/homarr/appdata.
- persistent
fmd-server:- persistent bind
/srv/fmd-server/fmddata/db.
- persistent bind
collabora:- no persistent volume declared in current compose.
nginx:- certs/config under
${DATA_ROOT}/nginx/* - reverse-proxy content mounts for Nextcloud/CryptPad.
- certs/config under
Reliability Notes For Future Stack Onboarding
- DB-backed apps:
- prefer consistent DB dumps/hooks where app docs require them
- if relying on cold backups (stack stop + volume copy), verify restore procedure for that DB engine.
- Cache services:
- valkey/redis instances often intentionally non-persistent here
- do not classify cache-only data as mandatory restore data.
- NFS/local split:
- project docs explicitly warn against placing container storage on NFS
- bind-mounted persistent data on NFS is used intentionally.
- Rootless nuance:
- several compose files run container user
"0"with rootless mapping semantics.
- several compose files run container user
Validation And CI Gates
- Local checks used in repo:
yamllint -c .yamllint.yaml .python scripts/check_env_example_coverage.pybash scripts/validate_compose.shsystemd-analyze verify systemd/*.service systemd/*.socket
- CI (
.github/workflows/ci.yml,.forgejo/workflows/ci.yml) enforces the same quality gates.
Add/Change Stack Checklist
- Confirm official app backup requirements and consistency expectations.
- Identify mandatory persistent paths and DB data locations from compose/systemd.
- Create/verify Duplicati jobs with stable names.
- Implement/update
duplicati/stacks/<stack>.sh. - Validate mapping:
systemctl --user status <unit> --no-pagerpodman ps --format '{{.Names}} {{.Labels}}' | grep io.podman.compose.project=<label>
- Run manual test:
~/podman-compose/duplicati/bin/backup <stack>
- Review per-stack log:
duplicati/logs/backup-<stack>-YYYY-MM-DD.log
- Only after manual success, append stack arg in:
systemd/duplicati-backup.serviceExecStart=...
- Reload and keep timer enabled:
systemctl --user daemon-reloadsystemctl --user enable --now duplicati-backup.timer
Security / Secret Handling
- Never read or print:
.env*.env*stack.env
- Never print:
- credentials, tokens, secret keys, passphrases
- remote URLs containing embedded authentication
- When reporting logs/config:
- summarize and redact sensitive values.
