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_dump of 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_KEEP sets 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's secret_key_base (and app_secrets_key if 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 from secret_key_base), make sure the new server also has none, so it uses the restored secret_key_base:
    docker service update --env-rm APP_SECRETS_KEY gitblixt
    (also delete the line from /opt/gitblixt/.env so 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.