Installation¶
pip install 'django-offsite-backup[s3,telegram]'
Backups in an S3 bucket with Object Lock, messages through the Telegram Bot API.
pip install 'django-offsite-backup[s3,aiogram]'
Messages through the bot your project already runs with django-aiogram; no token in these settings.
pip install django-offsite-backup
A local directory or Docker volume as the store, messages to the log. No driver at all — and no immutability either; see Stores: directory.
pipx install 'django-offsite-backup[cli,s3,telegram]'
The offsite-backup command, a TOML setting, the same passes — see
Without Django.
Requires Python 3.10–3.14, and Django 5.2+ for the Django layer (a Django project has it already). Every pair in that range runs in CI, and an integration job runs a whole cycle — backup, restore into a clean database, row counts of every table compared — against PostgreSQL 16 and 18 and an S3 server with Object Lock, and another against MySQL 8.4 and MariaDB 11.4.
Extras¶
The base install pulls nothing — not even Django, which a Django project brings itself and a plain server does not need. Every backend is named by a dotted path in the settings, imported only when a pass first needs it, and its driver is an extra:
| extra | pulls | for |
|---|---|---|
django |
django>=5.2 |
the Django layer — the app, the commands, the system checks — where the project does not already pin Django |
cli |
tomli>=2.0 on Python 3.10 only |
the offsite-backup command's TOML setting (Without Django) |
s3 |
boto3>=1.34 |
offsite_backup.stores.s3.S3Store, offsite_backup.media.s3.S3Source |
telegram |
httpx2>=2.0 |
offsite_backup.notifiers.telegram.TelegramNotifier |
webhook |
httpx2>=2.0 |
offsite_backup.notifiers.webhook.WebhookNotifier |
aiogram |
django-aiogram>=5.2 |
offsite_backup.notifiers.django_aiogram.TopicNotifier |
pyrage |
pyrage>=1.2.3 |
offsite_backup.encryption.pyrage.Pyrage — age without the age binary |
all |
all of the above |
The MySQL/MariaDB, SQLite, Docker-volume and Django-storage backends need no extra: the
first two run the database's own client or the standard library, the Docker one the
docker CLI, and a Django storage brings its own package (django-storages and its driver).
Nothing is inferred from what happens to be installed. When a configured backend's driver
is missing, manage.py check says so with the install line (E002):
?: (offsite_backup.E002) OFFSITE_BACKUP['STORE']['BACKEND'] is S3Store, but its driver is not installed.
HINT: pip install "django-offsite-backup[s3]"
Binaries¶
A pass streams through command-line tools rather than through Python memory. They are not Python dependencies; install them in the image that runs the backup:
| binary | needed for | checked by |
|---|---|---|
pg_dump |
PostgresSource — its major version must be at least the server's |
E003, and E004 under check --deploy |
zstd |
COMPRESS (on by default) and DECOMPRESS (restoring) — whatever program they name |
E003 |
age |
AgeCli, both for sealing and for restoring — not needed with Pyrage |
E003 |
psql, pg_restore |
restoring a database (backup_restore --apply, the drill), verifying a custom-format dump, and every question asked of the server (is the target empty, row counts, its version); shipped with pg_dump in every PostgreSQL client package |
E003 |
mysqldump, mysql — or mariadb-dump, mariadb |
MySQLSource, by its CLIENT: dumping, restoring, and the drill's questions (is the target empty, row counts) |
E003 |
docker |
VolumeSource, with the daemon's socket |
E003, and E012 under check --deploy |
tar is not needed: volumes and the configuration archive are written with Python's
tarfile. Measured on 613 MB in 2400 files, tarfile took 0.27 s against 0.72 s for the
tar CLI, and it is the only option for media streamed from S3, which has no files on disk
to hand to tar.
On Debian-based images:
RUN apt-get update \
&& apt-get install -y --no-install-recommends age zstd postgresql-common \
&& /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y \
&& apt-get install -y --no-install-recommends postgresql-client-18 \
&& rm -rf /var/lib/apt/lists/*
Match postgresql-client-<N> to your server's major version, or newer: pg_dump refuses
to dump a server newer than itself.
Add the app¶
INSTALLED_APPS = [
...,
'offsite_backup',
]
The app registers the system checks and imports nothing else: no backend, no driver, no network until a pass runs. It has no models and no migrations.
A first pass¶
-
Make a key pair on your machine, not the server, and keep the private half in your password manager (Encryption):
age-keygen -o backup-acme-prod.key # Public key: age1... -
Create the bucket with Object Lock, and a default COMPLIANCE rule of 13 days (S3 providers).
-
Put the settings in
settings.py, reading every credential from the environment (Settings):import os OFFSITE_BACKUP = { 'PREFIX': 'acme/prod', 'ENV': 'prod', 'STORE': { 'BACKEND': 'offsite_backup.stores.s3.S3Store', 'ENDPOINT_URL': os.environ.get('BACKUP_S3_ENDPOINT', ''), 'REGION': os.environ.get('BACKUP_S3_REGION', ''), 'BUCKET': os.environ.get('BACKUP_S3_BUCKET', ''), 'ACCESS_KEY': os.environ.get('BACKUP_S3_ACCESS_KEY', ''), 'SECRET_KEY': os.environ.get('BACKUP_S3_SECRET_KEY', ''), }, 'ENCRYPTION': { 'BACKEND': 'offsite_backup.encryption.age_cli.AgeCli', 'RECIPIENTS': os.environ.get('BACKUP_AGE_RECIPIENT', ''), }, 'DATABASES': {'BACKEND': 'offsite_backup.databases.postgres.PostgresSource'}, 'CONFIG': { 'BACKEND': 'offsite_backup.config.paths.PathsSource', 'PATHS': ['/app/deploy'], }, 'NOTIFY': [ { 'BACKEND': 'offsite_backup.notifiers.telegram.TelegramNotifier', 'TOKEN': os.environ.get('BACKUP_TELEGRAM_TOKEN', ''), 'CHAT_ID': os.environ.get('BACKUP_TELEGRAM_CHAT_ID', ''), }, ], } -
Check, then run:
python manage.py check python manage.py check --deploy # reaches the bucket and the database; the mounts a pass reads python manage.py backup_offsite # backup_offsite: 2 objects, 18734112 bytes up -
Restore it once, now, before you need to — Restore.
Then schedule the nightly pass and the morning check (Scheduling) and put the deploy pass in front of your migrations (Deploy integration).