176 lines
8.0 KiB
YAML
176 lines
8.0 KiB
YAML
# shopdb-flask BACKUP overlay - opt-in, per-site.
|
|
#
|
|
# WHY a separate overlay: backups are not part of the base stack every site must
|
|
# run. A facility turns them on by adding this file to its compose invocation:
|
|
#
|
|
# docker compose -f docker-compose.yml -f docker-compose.backup.yml \
|
|
# --profile backup up -d
|
|
#
|
|
# That brings up THREE cooperating pieces:
|
|
# - backup-store : a resident, isolated restic REST server (the third
|
|
# container). Holds every snapshot in its own volume and is
|
|
# reachable ONLY over the compose network - never published
|
|
# to the host - so the app can touch backups solely through
|
|
# the backup/restore utility, not with a shell or db creds.
|
|
# - backup-watcher : a resident poller that actuates the "Back up now" sentinel
|
|
# the api drops, running the one-shot against backup-store.
|
|
# This is what makes the in-app button produce a real backup
|
|
# given the api container cannot run docker.
|
|
# - backup : the same one-shot as a manual `run --rm backup` for sites
|
|
# that prefer a host scheduler (cron / Task Scheduler).
|
|
#
|
|
# WHAT each run captures, as one recoverable set:
|
|
# - db.sql.gz : mysqldump --single-transaction of the whole DB (users,
|
|
# auth, ledger, every plugin table). InnoDB-consistent,
|
|
# non-blocking.
|
|
# - instance/ : restic snapshot of /app/instance (deduped, encrypted),
|
|
# the files MySQL rows point at.
|
|
# - manifest.json: core+plugin Alembic revisions and image tag at capture
|
|
# time - the stamp that makes a restore a CHECKED operation.
|
|
#
|
|
# Retention (restic-native): --keep-daily 5 --keep-weekly 4 --keep-monthly 6,
|
|
# satisfying "any day in the last 5" plus a cheap long-horizon tail.
|
|
#
|
|
# DESTINATION defaults to the third container via RESTIC_REPOSITORY:
|
|
# third container : rest:http://backup-store:8000/ (default, ships with app)
|
|
# AWS S3 : s3:s3.amazonaws.com/shopdb-<site>-backups
|
|
# Azure Blob : azure:shopdbbackups:<site>
|
|
# SMB share : mount the share on the host, point at a local path
|
|
# Credentials come from the site's env/secret store, never committed - same
|
|
# pattern as the collector key.
|
|
|
|
# The THIRD container: an isolated backup store the app can only reach through
|
|
# the backup/restore utility, never with a shell or the db credentials. It runs
|
|
# restic's REST server over the compose network; the only thing that speaks to
|
|
# it is the backup watcher/one-shot via RESTIC_REPOSITORY=rest:http://... . Its
|
|
# data lives in its own named volume (backupstore_data), so it is container
|
|
# agnostic - later a pull/migration utility can point at this same REST endpoint
|
|
# from anywhere without touching the app or the database.
|
|
services:
|
|
backup-store:
|
|
image: restic/rest-server:0.13.0
|
|
profiles: ["backup"]
|
|
restart: unless-stopped
|
|
environment:
|
|
# Private (no public /create) so repos are provisioned by the backup
|
|
# client's `restic init`, and append-only OFF here because the same
|
|
# utility both writes and prunes; isolation comes from this being the
|
|
# only service on the network with access, not from client trust.
|
|
DISABLE_AUTHENTICATION: "true"
|
|
OPTIONS: "--no-auth"
|
|
volumes:
|
|
- backupstore_data:/data
|
|
# Deliberately NOT published to the host: reachable only over the compose
|
|
# network by service name, so nothing outside the stack can touch backups.
|
|
expose:
|
|
- "8000"
|
|
healthcheck:
|
|
test: ["CMD", "sh", "-c", "wget -q -O - http://127.0.0.1:8000/ >/dev/null 2>&1 || true"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
|
|
# Resident watcher: actuates the "Back up now" sentinel the api drops. This is
|
|
# the piece that makes the UI button run a real backup, since the api itself
|
|
# cannot run docker. It shares the base image with the one-shot and calls the
|
|
# same backup.sh.
|
|
backup-watcher:
|
|
image: shopdb-backup:local
|
|
build:
|
|
context: .
|
|
dockerfile: scripts/backup/Dockerfile.backup
|
|
profiles: ["backup"]
|
|
restart: unless-stopped
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
backup-store:
|
|
condition: service_started
|
|
environment:
|
|
# DB access - same credentials the api uses, reaching db over the compose
|
|
# network by service name.
|
|
MYSQL_HOST: db
|
|
MYSQL_DATABASE: shopdb_flask
|
|
MYSQL_USER: shopdb
|
|
MYSQL_PASSWORD: ${MYSQL_PASSWORD:?MYSQL_PASSWORD must be set}
|
|
# Destination is the third container by default: restic's REST backend on
|
|
# the isolated backup-store. A site can still override RESTIC_REPOSITORY
|
|
# in .env to send offsite instead, but out of the box backups land in the
|
|
# dedicated container.
|
|
RESTIC_REPOSITORY: ${RESTIC_REPOSITORY:-rest:http://backup-store:8000/}
|
|
RESTIC_PASSWORD: ${RESTIC_PASSWORD:?RESTIC_PASSWORD must be set}
|
|
# Optional cloud creds, passed through when set (empty otherwise).
|
|
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-}
|
|
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-}
|
|
AZURE_ACCOUNT_NAME: ${AZURE_ACCOUNT_NAME:-}
|
|
AZURE_ACCOUNT_KEY: ${AZURE_ACCOUNT_KEY:-}
|
|
# Site tag stamped into every snapshot + manifest.
|
|
SHOPDB_SITE: ${SHOPDB_SITE:-shopdb}
|
|
SHOPDB_IMAGE_TAG: ${SHOPDB_IMAGE:-source}
|
|
SHOPDB_BACKUPS_ROOT: /backups
|
|
BACKUP_POLL_SECONDS: ${BACKUP_POLL_SECONDS:-10}
|
|
volumes:
|
|
# The watcher services both backup (read) and restore (write) requests, so
|
|
# the instance tree is mounted read-write here. Restore has to lay the
|
|
# files half of a set back down next to the DB rows that reference it; the
|
|
# backup half still only ever reads. The manual one-shot below stays :ro.
|
|
- instance_data:/app/instance
|
|
# Alembic chain, read-only, so the manifest can stamp the schema revision
|
|
# the running code is at.
|
|
- ./migrations:/app/migrations:ro
|
|
- ./plugins:/app/plugins:ro
|
|
# Shared backups volume: holds the fast local db.sql.gz + manifest and the
|
|
# .backup-requested sentinel the api drops. The api mounts this same volume
|
|
# (see docker-compose.yml) so the UI can enumerate sets and request runs.
|
|
- backup_data:/backups
|
|
- ./scripts/backup:/opt/backup:ro
|
|
entrypoint: ["/opt/backup/watch.sh"]
|
|
|
|
# Manual one-shot, unchanged usage: `docker compose ... run --rm backup`.
|
|
# Kept for host-scheduler / cron sites that prefer a scheduled pass over the
|
|
# in-app button. Shares everything with the watcher but runs once and exits.
|
|
backup:
|
|
image: shopdb-backup:local
|
|
build:
|
|
context: .
|
|
dockerfile: scripts/backup/Dockerfile.backup
|
|
profiles: ["backup"]
|
|
restart: "no"
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
backup-store:
|
|
condition: service_started
|
|
environment:
|
|
MYSQL_HOST: db
|
|
MYSQL_DATABASE: shopdb_flask
|
|
MYSQL_USER: shopdb
|
|
MYSQL_PASSWORD: ${MYSQL_PASSWORD:?MYSQL_PASSWORD must be set}
|
|
RESTIC_REPOSITORY: ${RESTIC_REPOSITORY:-rest:http://backup-store:8000/}
|
|
RESTIC_PASSWORD: ${RESTIC_PASSWORD:?RESTIC_PASSWORD must be set}
|
|
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-}
|
|
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-}
|
|
AZURE_ACCOUNT_NAME: ${AZURE_ACCOUNT_NAME:-}
|
|
AZURE_ACCOUNT_KEY: ${AZURE_ACCOUNT_KEY:-}
|
|
SHOPDB_SITE: ${SHOPDB_SITE:-shopdb}
|
|
SHOPDB_IMAGE_TAG: ${SHOPDB_IMAGE:-source}
|
|
SHOPDB_BACKUPS_ROOT: /backups
|
|
volumes:
|
|
- instance_data:/app/instance:ro
|
|
- ./migrations:/app/migrations:ro
|
|
- ./plugins:/app/plugins:ro
|
|
- backup_data:/backups
|
|
- ./scripts/backup:/opt/backup:ro
|
|
entrypoint: ["/opt/backup/backup.sh"]
|
|
|
|
# Give the api read/write on the shared backups volume so the UI can list sets
|
|
# and drop the "Back up now" sentinel the watcher actuates. This extends the
|
|
# api service defined in docker-compose.yml (merged by compose).
|
|
api:
|
|
volumes:
|
|
- backup_data:/backups
|
|
|
|
volumes:
|
|
backup_data:
|
|
backupstore_data:
|