Bacula Forgejo
Back up every Forgejo repository, wiki, issue, release, and database on the schedule Bacula already runs — no plugins, no daemons, no surprises.
What it is
Forgejo backup the way Bacula already works
bacula-forgejo adds first-class Forgejo support to an existing Bacula deployment — Community or Enterprise. The package installs two binaries: forgejo-backup runs as a ClientRunBeforeJob hook and pulls all Forgejo data into a local staging directory; forgejo-restore runs as a ClientRunAfterJob hook and pushes recovered data back to a live Forgejo instance.
The Bacula FileDaemon then sweeps the staging directory just as it sweeps any other path. No Bacula plugins are required, no schema changes, and no new daemons. Your existing FileSets, pools, retention policies, and schedules continue to apply — and because no enterprise-only features are used, the tool works identically on Bacula Community and Bacula Enterprise.
Restore comes in two modes: api mode stages every object individually so you can recover a single repository, a single release asset, or a single user without touching anything else; dump mode uses forgejo admin dump for the fastest possible full-instance disaster recovery. A remote_mode flag caps concurrency and disables filesystem-level operations, making the tool safe to run against hosted Forgejo instances such as Codeberg without an SSH connection to the server.
# Staging layout after a full backup /opt/forgejo-backup/data/ ├── _state/ ← ETags, timestamps, run manifest ├── _db/ ← SQLite / MySQL / PostgreSQL dump ├── _dump/ ← forgejo admin dump ZIP (full only) ├── _config/ ← redacted app.ini + custom/ ├── _users/ ← profiles, SSH keys, GPG keys ├── _orgs/ ← org JSON, members, teams ├── _registry/ ← OCI blobs + manifests (full only) ├── _packages/ ← package registry (full only) └── myorg/myrepo/ ├── repository.git/ ← bare mirror ├── wiki.git/ ├── lfs/ ← content-addressable blobs ├── metadata/ ← issues, PRs, labels, etc. └── releases/ ← release metadata + assets # Hook this directory into a standard Bacula FileSet FileSet { Name = "ForgejoPrimary" Include { Options { signature = MD5 } File = /opt/forgejo-backup/data } }
Why use it
Everything Forgejo holds — recovered in one restore job
A bare git clone --mirror gets the code. bacula-forgejo gets the rest: issue history, contributor keys, package registry, container images, and the database that ties it all together.
No Bacula plugins
The staging-directory approach means the FileDaemon treats Forgejo data exactly like local files. No plugins to install, no compatibility matrix between Bacula versions and plugin versions, no additional moving parts to maintain.
True incremental backups
Git repositories mirror with git remote update --prune; LFS pulls only new content-addressable blobs; issue and release metadata uses since= parameters. Daily jobs touch only what changed — weekly Full jobs capture everything.
Secure by default
The API token never appears in process arguments. It is loaded from an environment variable or a 0600 file. The package runs under a dedicated forgejo-backup system user with no login shell and no write access outside the staging directory.
Three database backends
SQLite, MySQL, and PostgreSQL are supported out of the box. PostgreSQL backups use pg_dump --format=directory with configurable parallelism so dump time scales with available CPU cores.
Two restore modes
API mode restores individual repositories, organisations, or users — pass --repos myorg/my-repo and only that repository is pushed back. Dump mode uses forgejo admin dump for the fastest possible full-instance disaster recovery when speed matters more than granularity.
Works with your existing schedule
The hook reads the Bacula job level via --level %l and adjusts its module selection automatically. Configure once; the backup level drives the rest. Your existing Full/Differential/Incremental schedule applies without modification.
Coverage
What gets backed up
All data is pulled before the FileDaemon sweep. The staging directory holds a consistent snapshot of every object listed below at the time the job runs.
Every backup (Full / Differential / Incremental)
- Git repositories — bare mirror via
git remote update --prune - Git LFS objects — content-addressable; only new blobs transferred
- Wikis — bare mirror
- Issues, PRs, and comments — incremental via
since=timestamp - Labels, milestones, and branch protections
- Release metadata and binary assets — size-matched skip for unchanged assets
- Forgejo Actions workflows and run logs — configurable log cap
- User profiles, SSH keys, and GPG keys
- Organisation structure — members, teams, webhooks, secrets (names only)
- Database dump — SQLite, MySQL, or PostgreSQL
- Redacted
app.iniandcustom/directory
Full backups only
- Container / OCI registry — filesystem copy or OCI Distribution API
- Package registry — all Forgejo package types
forgejo admin dumpZIP — optional; written to a separate Bacula pool
Explicitly does not
- Modify any data on the live Forgejo instance during backup — read-only throughout
- Replace the Bacula schedule, storage, or retention configuration — all existing policies apply
- Require any Bacula plugin — all data flows through standard FileSets and the FileDaemon
Use cases
Built for teams running Forgejo and Bacula in production
Whether you operate a single Forgejo instance or manage git infrastructure for multiple teams, bacula-forgejo fits into the backup architecture you already have.
Full Forgejo disaster recovery
Back up every repository, issue, release, container image, and the database in a single coordinated Bacula job. Restore to a fresh Forgejo instance in the same coordinated fashion — no manual API calls, no ad-hoc scripts.
Daily incrementals, weekly fulls
Daily Differential jobs back up changed metadata and new LFS blobs in minutes. Weekly Full jobs capture the registry and package content. The hook reads the Bacula job level and selects modules automatically.
Issue and PR audit trail
Every comment, label change, and branch protection is preserved in the staging directory with the timestamp it was pulled. Restoring a repository brings back its full issue history, not just the code.
Back up your Codeberg account
Enable remote_mode and point forgejo_url at codeberg.org. The tool caps concurrency, skips filesystem operations, and uses only the Forgejo-compatible API — no SSH access to Codeberg’s servers required. Your repositories, wikis, issues, and release assets land in Bacula just like local data.
Per-customer Forgejo isolation
Run separate Bacula jobs targeting separate Forgejo instances with separate API tokens and staging directories. Each customer’s data lives in its own FileSet and pool — no cross-contamination at the storage level.
Container and package registry backup
OCI image layers and Forgejo package content (npm, pip, Maven, Debian, RPM, and more) are captured on Full jobs alongside the code that produced them. Recover a historical release with its exact binary artefacts intact.
Features
Everything in the package — no tiers, no licences
BSD 2-Clause licence. The complete feature set ships in one package — .deb for Debian/Ubuntu, .rpm for RHEL/Rocky/Fedora/SUSE — because the staging-directory approach has no per-feature runtime costs.
Pre/post job hook integration
ClientRunBeforeJob and ClientRunAfterJob directives in the Bacula Director config. No daemon, no persistent process, no port to open.
Incremental-aware metadata sync
Issues, PRs, comments, releases, and Actions logs use since= timestamp parameters to pull only what changed since the last run. ETag caching skips unchanged API responses.
Content-addressable LFS backup
Git LFS objects are identified by their SHA-256 content hash. Incremental jobs transfer only blobs not already present in the staging directory — no re-downloading of unchanged objects.
Parallel PostgreSQL dump
pg_dump --format=directory --jobs=N with a configurable job count. Large databases complete in a fraction of the time of a single-threaded dump.
OCI registry backup
Container images backed up via OCI Distribution API or direct filesystem copy. Each layer stored once (content-addressable) regardless of how many images reference it.
Two restore modes
API mode: forgejo-restore --repos myorg/my-repo restores a single repository without touching anything else. Flags for org, user, database, and registry restore run independently. Dump mode: uses forgejo admin dump for the fastest possible full-instance disaster recovery.
Remote-forge support
remote_mode: true in config.yaml disables filesystem-level operations and caps API concurrency. Safe to run against hosted Forgejo instances (Codeberg and similar) without SSH access to the server — only the Forgejo API is required.
Module-level control
Enable or disable any module (registry, packages, dump, LFS, wikis) in config.yaml. Override the Full-only default on any module without touching the Bacula director config.
Dedicated service account
The package creates a forgejo-backup system user with no login shell and ownership of only the staging and log directories. The Bacula FileDaemon runs the hook via sudo -u forgejo-backup.
Run manifest and Prometheus metrics
Each backup run writes a JSON manifest to _state/ with per-module timing and item counts. A live Prometheus metrics file in the same directory is ready for node_exporter’s textfile collector.
Redacted configuration backup
app.ini is copied with secrets replaced by REDACTED placeholders before it reaches the staging directory. The custom/ directory (themes, templates, static files) is copied verbatim.
GPG-signed packages
Both the .deb (Debian/Ubuntu) and .rpm (RHEL/Rocky/Fedora/SUSE) release packages are GPG-signed and carry SHA-256 checksums. Install directly — no third-party repository required.
Written in Rust
A single statically-linked binary per command. No runtime dependencies beyond git, git-lfs, and the appropriate database dump tool. Starts in milliseconds; adds negligible overhead to the Bacula job.
Quick start
Running in four steps
Requires Forgejo 7.x or later, Bacula Community or Enterprise, and Debian 12+ (Bookworm) or RHEL 9+ / Rocky / Fedora / SUSE. git and git-lfs must be installed on the backup client.
Install the package
Install the signed package for your distribution. The package creates the forgejo-backup system user, the staging directory at /opt/forgejo-backup/data/, and the log directory at /var/log/forgejo-backup/.
# Debian / Ubuntu
apt install ./bacula-forgejo_0.1.1_amd64.deb
# RHEL / Rocky / Fedora
dnf install ./bacula-forgejo-0.1.1-1.x86_64.rpm
# SUSE
zypper install ./bacula-forgejo-0.1.1-1.x86_64.rpm
Create the configuration file
Copy the example config and set permissions before editing. The file must be owned by forgejo-backup and not world-readable — it may reference secret file paths.
cp /usr/share/doc/bacula-forgejo/config.yaml.example \
/etc/forgejo-backup/config.yaml
chmod 0600 /etc/forgejo-backup/config.yaml
chown forgejo-backup:forgejo-backup \
/etc/forgejo-backup/config.yaml
$EDITOR /etc/forgejo-backup/config.yaml
Wire the hooks into Bacula
Add the ClientRunBeforeJob and ClientRunAfterJob directives to your Bacula Job resource. The %l macro passes the current backup level to the hook.
Job {
Name = "ForgejoPrimary"
...
Client Run Before Job =
"sudo -u forgejo-backup \
env FORGEJO_BACKUP_TOKEN=<token> \
forgejo-backup \
--config /etc/forgejo-backup/config.yaml \
--level %l"
Client Run After Job =
"sudo -u forgejo-backup \
forgejo-restore \
--config /etc/forgejo-backup/config.yaml \
--verify-only"
}
Run a test backup
Trigger a manual Full job and confirm the staging directory is populated before the FileDaemon sweep begins.
# Trigger a manual Full run
sudo -u forgejo-backup \
env FORGEJO_BACKUP_TOKEN=<token> \
forgejo-backup \
--config /etc/forgejo-backup/config.yaml \
--level F
# Confirm staging directory contents
ls -lh /opt/forgejo-backup/data/
# Then run the Bacula job normally via bconsole
# run job=ForgejoPrimary level=Full yes
Your Forgejo data belongs in your backup infrastructure.
BSD 2-Clause, open source, and slots into the Bacula deployment you already operate. Two binaries. No plugins.