Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

zodb-backup

PyPI PyPI - Python Version

GitHub contributors GitHub Repo stars

CI

Container-native backup and restore for ZODB FileStorage and blobstorage, aimed at Plone/Zope deployments running under Docker and Docker Swarm.

The container runs once and exits, so scheduling belongs to your orchestrator (swarm-cronjob, a systemd timer, a Kubernetes CronJob). Everything is configured through environment variables.

Status: alpha. Backup, restore, retention and hooks work, and the container is tested against a live ZEO server while it is being written to. What is not proven yet: no release has been published, hard-linked blob backups are untested on NFS and Gluster, and nobody has yet restored a real production database with it. Rehearse your restore before relying on it.

Credits

zodb-backup is directly inspired by, and largely modelled on, collective.recipe.backup by Reinout van Rees, Maurits van Rees and contributors — in its own words, "sensible defaults around bin/repozo". That recipe carries about two decades of hard-won operational knowledge about backing up ZODB. This project ports that behaviour to an environment-variable-driven CLI for one-shot containers; the buildout plumbing is left behind.

See docs/provenance.md for exactly which behaviours were adopted, which were changed, and why.

Commands

zodb-backup backup                  # incremental (full with --full / FULL=true)
zodb-backup snapshot                # full backup into the snapshot locations
zodb-backup restore [DATE]          # restore latest, or the state at DATE
zodb-backup snapshot-restore [DATE]
zodb-backup list                    # available backups, timestamps, sizes
zodb-backup verify                  # repozo verify pass

DATE uses repozo semantics: UTC, yyyy-mm-dd[-hh[-mm[-ss]]].

Restore asks for a literal yes. Containers are usually non-interactive, so pass --yes or set ASSUME_YES=true — without a TTY and without either, the command fails with a clear message rather than hanging.

Configuration

Variable Default Notes
DATAFS /data/filestorage/Data.fs mount read-only for backup, read-write for restore
BLOBSTORAGE /data/blobstorage empty means filestorage-only
BACKUP_LOCATION /backups/filestorage
BLOB_BACKUP_LOCATION /backups/blobstorage
SNAPSHOT_LOCATION /backups/snapshots
BLOB_SNAPSHOT_LOCATION /backups/blobstoragesnapshots
FULL false force a full filestorage backup
QUICK true repozo --quick; disabling it reads 3–4× the database size per run
GZIP true repozo --gzip
KEEP 2 full backups to keep; 0 keeps everything
BACKUP_BLOBS true
ONLY_BLOBS false
ARCHIVE_BLOB false tar archive instead of an rsync tree
COMPRESS_BLOB false only valid with ARCHIVE_BLOB=true
USE_RSYNC true false falls back to a plain directory copy
RSYNC_OPTIONS (empty) extra rsync arguments, shell-quoted
PRE_COMMAND (empty) failure aborts the backup
POST_COMMAND (empty) e.g. push to off-site storage
ASSUME_YES false non-interactive restore
DEBUG / QUIET false mutually exclusive

Booleans accept true/1/yes/on and false/0/no/off, case-insensitively. Anything else is a configuration error — a typo will never be read as "false".

Exit codes: 0 success, 1 operational failure, 2 configuration error.

Running the container

The image expects the ZODB data at /data and writes backups to /backups, matching the layout of the official plone/plone-zeo image.

docker run --rm \
  -v zeo-data:/data:ro \
  -v zeo-backups:/backups \
  -e KEEP=7 \
  ghcr.io/simplesconsultoria/zodb-backup:latest backup

Mount the data read-only. The tool never writes there, and the mount makes that a guarantee rather than a promise.

Two things to get right:

  • The container must run as the uid that owns the data. It runs as uid 500 by default, which is what the official Plone images use, so alongside plone/plone-zeo there is nothing to configure. If your data is owned by someone else, pass --user.

  • The backups volume must be writable by that uid. Docker creates named volumes owned by root, so a fresh one is not writable by a non-root container. Fix it once when you create it:

    docker run --rm -v zeo-backups:/backups busybox chown -R 500:500 /backups
    

    If you skip this the tool exits 2 and tells you exactly this.

Scheduling with swarm-cronjob

The container runs once and exits, so scheduling belongs to your orchestrator:

services:
  backup:
    image: ghcr.io/simplesconsultoria/zodb-backup:latest
    command: ["backup"]
    volumes:
      - zeo-data:/data:ro
      - zeo-backups:/backups
    environment:
      KEEP: "7"
    deploy:
      mode: replicated
      replicas: 0
      restart_policy:
        condition: none
      labels:
        - "swarm.cronjob.enable=true"
        - "swarm.cronjob.schedule=0 30 3 * * *"
        - "swarm.cronjob.skip-running=true"

With local volumes, pin the backup job and the ZEO service to the same node (deploy.placement.constraints) — otherwise the job may be scheduled somewhere the data does not exist.

Restore runbook

Restoring replaces live data, so take the database offline first. This procedure is executed by the integration tests, not just documented.

  1. Stop the writers. Scale the backend and ZEO to zero, so nothing is holding the storage open or committing during the restore.

    docker service scale myproject_backend=0 myproject_zeo=0
    
  2. Restore. Containers have no terminal, so ASSUME_YES=true is required; without it the command refuses rather than hanging on a prompt.

    docker run --rm \
      -v zeo-data:/data \
      -v zeo-backups:/backups:ro \
      -e ASSUME_YES=true \
      ghcr.io/simplesconsultoria/zodb-backup:latest restore
    

    Note /data is mounted read-write here, unlike during a backup, and /backups read-only. To restore an earlier state, pass a date: restore 2026-03-01-02-00-00.

  3. Bring the stack back up.

    docker service scale myproject_zeo=1 myproject_backend=2
    

To rehearse without risk, restore into a different empty volume and point a throwaway ZEO server at it. That is exactly what the integration tests do.

Retention

KEEP counts full filestorage backups, not files. KEEP=2 keeps the two most recent full backups together with every incremental belonging to them — an incremental is worthless without the full backup it applies to. Blob backups are not counted separately: one is kept for as long as there is a filestorage backup it can be restored alongside, and removed once it is older than the oldest surviving full backup. KEEP=0 keeps everything.

Rotation happens only after a backup has succeeded, so a failed run never deletes the backup that is still your newest good one.

Hooks

PRE_COMMAND runs before anything is written; if it fails, the run is abandoned with nothing on disk. POST_COMMAND runs after the backup is complete and is where off-site replication belongs:

POST_COMMAND="rclone sync /backups remote:zodb-backups"

A failing POST_COMMAND makes the process exit non-zero — an off-site copy that did not happen is worth alerting on — but the backup already written is left untouched. Both hooks run through a shell, so pipes and && work as expected.

What is NOT backed up

Being honest about scope, in the spirit of the original recipe:

  • RelStorage / SQL backends — out of scope; use pg_dump or equivalent.
  • The rest of your deployment — configuration, secrets, uploaded files outside the blobstorage, your container images.
  • Off-site copies — use POST_COMMAND to invoke rclone, restic or borg.
  • Encryption — backups are written unencrypted.

Warnings

  • Never pack the database during a backup window. Correct backups rely on FileStorage being append-only for the duration of the run; a concurrent zeopack breaks that assumption.
  • Hard-link-based blob backups need a filesystem that supports them. Behaviour on NFS and Gluster volumes is untested — prefer ARCHIVE_BLOB=true there until it is.
  • rsync decides what to copy from file size and modification time, so a blob rewritten to the same length within the same second would be missed and the stale copy hard-linked instead. ZODB never does this — blob files are immutable and a new revision becomes a new file — but if something outside ZODB writes into your blobstorage, set RSYNC_OPTIONS=--checksum. That reads every byte on every run, which is what the hard-link design exists to avoid, so do not enable it by reflex.

Development

uv sync
make test      # unit tests
make lint      # ruff + mypy
make format

Requires Python 3.14+ and uv.

License

GPL-2.0-only, matching collective.recipe.backup. See LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

zodb_backup-1.0.0a1.tar.gz (61.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

zodb_backup-1.0.0a1-py3-none-any.whl (38.1 kB view details)

Uploaded Python 3

File details

Details for the file zodb_backup-1.0.0a1.tar.gz.

File metadata

  • Download URL: zodb_backup-1.0.0a1.tar.gz
  • Upload date:
  • Size: 61.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for zodb_backup-1.0.0a1.tar.gz
Algorithm Hash digest
SHA256 991b638d6e22664dddd10e500430c403f4b34204dd10b0c48684ee7d7ea391d6
MD5 beae7bb9b6d7df0718e91be986d7e427
BLAKE2b-256 6f6480b88c8f8442ae380371f8a6405cfd989aacbd42a1d1312403a847f605a8

See more details on using hashes here.

File details

Details for the file zodb_backup-1.0.0a1-py3-none-any.whl.

File metadata

  • Download URL: zodb_backup-1.0.0a1-py3-none-any.whl
  • Upload date:
  • Size: 38.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for zodb_backup-1.0.0a1-py3-none-any.whl
Algorithm Hash digest
SHA256 b5aad103984a1906eef7fdfa530a78631524c67f1276bb9e658c590ffedc9402
MD5 2129fc0808cc8e1282709661a7baa8e1
BLAKE2b-256 52cd87d6ff33dd0cf10a11878724e9508e5efbd95b1a94569f0a3cfd5d247b63

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page