From 892601f65bf78e5139d4e2e03d92b17586962f99 Mon Sep 17 00:00:00 2001 From: Josh Bairstow Date: Wed, 3 Jun 2026 00:55:03 +1000 Subject: [PATCH] feat(backup): add forgejo dump systemd timer + wire into runbook Daily 'forgejo dump' (tar.zst) to /opt/backups/forgejo with 14-day retention, via a oneshot service + timer. Maintenance section now documents both backup layers (Codeberg push-mirror for git, dump timer for full state). Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 18 +++++++++++++++--- backup/forgejo-backup.service | 10 ++++++++++ backup/forgejo-backup.sh | 34 ++++++++++++++++++++++++++++++++++ backup/forgejo-backup.timer | 11 +++++++++++ 4 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 backup/forgejo-backup.service create mode 100755 backup/forgejo-backup.sh create mode 100644 backup/forgejo-backup.timer diff --git a/README.md b/README.md index 9fb9ac2..8374a15 100644 --- a/README.md +++ b/README.md @@ -267,9 +267,21 @@ No changes to this repo. In the new project: ## Maintenance / hardening - **Backups:** the `forgejo_data` volume is now the source of truth (repos, DB, - registry blobs). Run `docker compose exec forgejo forgejo dump` on a schedule, - and set a **push-mirror** on each repo to a cloud host (GitLab/GitHub) for - off-box git backup. + registry blobs). Two layers: + - *Off-box git:* a **push-mirror** of each repo to Codeberg (repo → Settings → + Mirror Settings → Add Push Mirror; HTTPS URL + a `write:repository` token). + - *Full dump:* the systemd timer in [`backup/`](backup/) runs `forgejo dump` + daily and keeps 14 days under `/opt/backups/forgejo`. Install on the VPS: + ```sh + sudo install -m 0755 backup/forgejo-backup.sh /usr/local/bin/forgejo-backup.sh + sudo cp backup/forgejo-backup.{service,timer} /etc/systemd/system/ + sudo systemctl daemon-reload && sudo systemctl enable --now forgejo-backup.timer + sudo systemctl start forgejo-backup.service # test once + ls -lh /opt/backups/forgejo # confirm an archive landed + systemctl list-timers forgejo-backup.timer # confirm next run scheduled + ``` + Copy the dumps off-box too (they're useless if the VPS dies). Add + `--skip-package` in the script to exclude registry blobs if dumps get large. - **Registry GC:** per-commit `:sha` tags accumulate. Enable Forgejo's package cleanup rules / run periodic GC so the VPS disk doesn't fill. - **Certs:** never delete the `caddy_data` volume — losing it re-issues every diff --git a/backup/forgejo-backup.service b/backup/forgejo-backup.service new file mode 100644 index 0000000..b50b02e --- /dev/null +++ b/backup/forgejo-backup.service @@ -0,0 +1,10 @@ +# Oneshot unit invoked by forgejo-backup.timer. Install both into +# /etc/systemd/system/ and `systemctl enable --now forgejo-backup.timer`. +[Unit] +Description=Forgejo dump backup +After=docker.service +Requires=docker.service + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/forgejo-backup.sh diff --git a/backup/forgejo-backup.sh b/backup/forgejo-backup.sh new file mode 100755 index 0000000..6cbc844 --- /dev/null +++ b/backup/forgejo-backup.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# ---------------------------------------------------------------------------- +# forgejo-backup.sh — dump the live Forgejo instance and copy the archive to +# the host. Run on the VPS via forgejo-backup.timer (see this dir). +# +# `forgejo dump` produces a single archive of the database, repositories, +# config, and the data dir (attachments + the container registry blobs). It is +# safe to run against a live instance. +# +# Install: +# sudo install -m 0755 forgejo-backup.sh /usr/local/bin/forgejo-backup.sh +# ---------------------------------------------------------------------------- +set -euo pipefail + +CONTAINER=forgejo +BACKUP_DIR=/opt/backups/forgejo +RETAIN_DAYS=14 +STAMP="$(date +%Y%m%d-%H%M%S)" +NAME="forgejo-${STAMP}.tar.zst" + +mkdir -p "$BACKUP_DIR" + +# Dump as the git user inside the container (writes to /tmp), then copy out. +# Add --skip-package to exclude the registry blobs if dumps get too large +# (they grow with per-commit :sha image tags — see registry GC in the README). +docker exec -u git "$CONTAINER" \ + forgejo dump --type tar.zst --skip-log --file "/tmp/${NAME}" +docker cp "${CONTAINER}:/tmp/${NAME}" "${BACKUP_DIR}/${NAME}" +docker exec "$CONTAINER" rm -f "/tmp/${NAME}" + +# Retention: drop dumps older than RETAIN_DAYS. +find "$BACKUP_DIR" -maxdepth 1 -name 'forgejo-*.tar.zst' -mtime +"${RETAIN_DAYS}" -delete + +echo "Backup written: ${BACKUP_DIR}/${NAME}" diff --git a/backup/forgejo-backup.timer b/backup/forgejo-backup.timer new file mode 100644 index 0000000..7074ec9 --- /dev/null +++ b/backup/forgejo-backup.timer @@ -0,0 +1,11 @@ +# Daily Forgejo backup at 03:30 server time. Persistent=true runs a missed +# backup after the box was off at the scheduled time. +[Unit] +Description=Daily Forgejo dump backup + +[Timer] +OnCalendar=*-*-* 03:30:00 +Persistent=true + +[Install] +WantedBy=timers.target