Backup & Restore
The app runs as a Docker Swarm service named gitblixt, not a
Compose service
— Postgres, MariaDB and Caddy run as the gitblixt
Swarm stack. So you run bin/gitblixt
commands by exec-ing into the running task container, not with docker compose exec:
docker exec $(docker ps -q -f name=gitblixt.1) bin/gitblixt ...
What's in a Backup
-
A full
pg_dumpof the database (users, repos metadata, issues, MRs, settings…) - The apps' managed Postgres databases and their roles. Dumping them needs GitBlixt's database role to be a superuser; when it isn't, or a dump fails, the backup completes without them and Admin → Backups shows a warning on it. Turn them off there if they are backed up another way.
- All bare git repositories (
/data/repos) - Uploads — avatars and attachments (
/data/uploads) - The persistent volumes of every app environment except previews. They are copied while the apps keep running, so each copy is crash-consistent: what the app would find after a power cut. Databases kept in a volume (SQLite, an embedded store) normally recover from that, but a consistent copy needs the app stopped first. Maintainers can leave a volume out with its Include in backups checkbox on the environment's Configuration tab. A volume that can't be copied, or doesn't fit in the free disk next to the rest of the backup, is listed with its error and the backup completes with a warning, as it does when files change mid-copy.
And, when encryption is enabled, a second object carrying the secret material needed to decrypt the encrypted-at-rest database columns (see below).
Not included:
SSH host keys (/data/ssh) and your .env. After a restore onto a new host, users will get a one-time SSH
"host key changed" warning, and you re-enter host-level config (domain, Postgres
password).
SMTP set via Admin → Settings is stored in the database, so it does come back
with the restore; SMTP set only via .env
does not.
Creating a Backup
The recommended path is Admin → Backups — configure a schedule, trigger on-demand backups, and store locally or to S3. To run one from the shell:
docker exec $(docker ps -q -f name=gitblixt.1) bin/gitblixt eval "Gitblixt.Backups.trigger_backup(nil)"
Backups land in /data/backups
(i.e. /opt/gitblixt/data/backups
on the host, via the bind mount) or in your configured S3 bucket.
Storing Backups in a Bucket
The Where Backups Are Stored card at the top of Admin → Backups says where the next backup will go. Until a bucket is set up, backups stay on the server's own disk, and a lost disk takes them with it.
To send them to any S3-compatible bucket (AWS S3, Cloudflare R2, Backblaze B2, MinIO), open Admin → Settings → File Storage, choose the S3 provider and fill in the endpoint, the access and secret keys and Backup Bucket. Each backup after that is uploaded to the bucket and then deleted from the server. Bucket Name is for uploads (avatars, attachments, packages); leave it blank to keep uploads on the server and send only backups to the bucket. When Backup Bucket is blank, backups use Bucket Name. The card's badges show what each part uses, and Test writes and deletes a small file in each bucket that is set.
Retention
The Retention card on Admin → Backups sets how many completed backups to keep. After each backup the older ones past the number are deleted.
-
Keep in the bucket
(shown once a bucket is set up). Blank keeps every backup in the bucket. A backup whose
files the bucket refuses to delete stays listed and is tried again after the next
upload.
BACKUP_REMOTE_KEEPsets the starting value on a new install. -
Keep on this server. Without a bucket, every backup is kept until you save a
number; the newest is always kept. With a bucket, new backups don't stay on the server,
so the number only limits backups made before the bucket was set up (default
BACKUP_LOCAL_KEEP, 2).
Set retention in one place. If the bucket also has a lifecycle rule that deletes old files, GitBlixt doesn't notice: those backups stay listed, and their downloads stop working. Some providers, Backblaze B2 among them, keep every version of a file by default, so a deleted backup is only hidden and is still billed. Set the bucket to keep only the latest version, or add a rule that removes hidden files.
Encrypted Backups
Enable encryption under Admin → Backups → Encryption. GitBlixt generates a keypair and shows you the private key once — save it immediately (e.g. in 1Password). The server keeps only the public key, so it can encrypt new backups but can never decrypt its own backups. A stolen server therefore cannot reveal any past backup — but if you lose the private key, the backups are unrecoverable. There is no recovery path; that is the point.
Each encrypted backup is two objects:
-
gitblixt-backup-TIMESTAMP-ID.tar.gz.enc— database, repos, uploads, and volumes -
gitblixt-backup-TIMESTAMP-ID.key.enc— the source server'ssecret_key_base(andapp_secrets_keyif set), required to decrypt the encrypted-at-rest columns
You need both objects plus the private key to restore.
Critical: the private key must be PEM/PKCS#1
The key must look like -----BEGIN RSA PRIVATE KEY-----. If you saved an
OpenSSH-format key instead (-----BEGIN OPENSSH PRIVATE KEY-----), the
restore fails with a :public_key.pem_entry_decode error. Convert it:
chmod 600 backup-private.pem
ssh-keygen -p -m PEM -N "" -f backup-private.pem
head -1 backup-private.pem # should now read: -----BEGIN RSA PRIVATE KEY-----
Restoring a Backup
Restore runs through a running GitBlixt instance — so the app must already be installed and running (see Getting Started). For a full new-server move (DNS, secrets, PaaS apps), follow Migrating to a New Server; the mechanics below are the core of it.
Always restore into an EMPTY database. If the app has already booted
against the target DB, it has run migrations, and pg_restore --clean only
drops objects present in the (possibly older) dump — leaving newer tables orphaned and
reverting schema_migrations, producing a broken hybrid schema. Drop and
recreate the database first, restore, then run migrations.
1.
Stage all three files where the container can read them — under /opt/gitblixt/data
(the bind mount, visible as /data
inside the container):
sudo mkdir -p /opt/gitblixt/data/backups
sudo cp gitblixt-backup-TIMESTAMP-ID.tar.gz.enc /opt/gitblixt/data/backups/
sudo cp gitblixt-backup-TIMESTAMP-ID.key.enc /opt/gitblixt/data/backups/
sudo cp backup-private.pem /opt/gitblixt/data/backup-private.pem
2. Reset the database to empty (the app must be stopped so it releases connections):
cd /opt/gitblixt
docker service scale gitblixt=0
PG=$(docker ps -q -f name=postgres)
docker exec "$PG" psql -U gitblixt -d postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='gitblixt' AND pid <> pg_backend_pid();"
docker exec "$PG" psql -U gitblixt -d postgres -c "DROP DATABASE gitblixt;"
docker exec "$PG" psql -U gitblixt -d postgres -c "CREATE DATABASE gitblixt OWNER gitblixt;"
3. Run the restore in a one-off container against the empty DB:
set -a; . /opt/gitblixt/.env; set +a
docker run --rm --network gitblixt -v /opt/gitblixt/data:/data \
-e DATABASE_URL="ecto://gitblixt:$POSTGRES_PASSWORD@postgres/gitblixt" \
willc0de4food/gitblixt:latest \
bin/gitblixt eval 'Gitblixt.Release.restore("/data/backups/gitblixt-backup-TIMESTAMP-ID.tar.gz.enc", "/data/backups/gitblixt-backup-TIMESTAMP-ID.key.enc", "/data/backup-private.pem")'
This decrypts both objects, restores the database, writes the source server's
secret_key_base
into /data/config, and unpacks repos and uploads.
To also recreate the app volumes, add -v /var/run/docker.sock:/var/run/docker.sock
to that docker run. Each volume is created and filled from the backup; one
that already holds data is skipped with a warning in the output, never overwritten.
4. Bring the app back; its entrypoint migrates the freshly-restored DB cleanly:
docker service scale gitblixt=1
docker service ps gitblixt # wait for Running
5. Shred the private key:
shred -u /opt/gitblixt/data/backup-private.pem
Encryption-key continuity (or your secrets won't decrypt)
Encrypted-at-rest columns — AI provider keys, app env vars and secret files, OAuth tokens,
webhook secret tokens, CI variables, mirror credentials — are encrypted with a key derived from the
source server's secret_key_base
(or an explicit APP_SECRETS_KEY).
The restore brings the source secret_key_base
across, but an explicit APP_SECRETS_KEY set in the environment overrides it.
-
If your source server used no
APP_SECRETS_KEY(the default — the key is derived fromsecret_key_base), make sure the new server also has none, so it uses the restoredsecret_key_base:
(also delete the line fromdocker service update --env-rm APP_SECRETS_KEY gitblixt/opt/gitblixt/.envso a restart doesn't re-add it). -
If your source server set an explicit
APP_SECRETS_KEY, set the same value on the new server before relying on encrypted features.
Verify by opening something backed by an encrypted column after restore — e.g. an AI provider key in Settings, or Send Test on the SMTP settings page. A clean result means the key lines up.