VPS Snaps

How to back up and restore self-managed GitLab

On a Linux package (Omnibus) install, sudo gitlab-backup create writes one tar archive of the database, repositories, uploads, CI artifacts, LFS objects and packages to /var/opt/gitlab/backups. It leaves out /etc/gitlab/gitlab-secrets.json and gitlab.rb, and without the secrets file a restored GitLab cannot decrypt CI/CD variables or two-factor secrets, so copy them separately. To restore, install exactly the same GitLab version and edition, put the secrets back, stop Puma and Sidekiq, and run sudo gitlab-backup restore BACKUP=<backup-id>.

10 min readUpdated Checked against official documentation

What the backup contains, and what it leaves out

DataIn the archive?Notes
PostgreSQL databaseYesDumped with pg_dump and gzipped: issues, merge requests, users, permissions, encrypted CI/CD variables
Git repositories, wikis, snippetsYesOne Git bundle per repository. Forks are stored in full, not deduplicated.
Uploads, CI job logs and artifacts, LFS, packages, registry images, Pages, Terraform state, secure filesOnly on local diskAnything in object storage is skipped; back up the bucket with your provider's tools.
/etc/gitlab/gitlab-secrets.jsonNoThe database encryption keys. A restore needs it.
/etc/gitlab/gitlab.rbNoYour configuration
/etc/gitlab/ssl, /etc/gitlab/trusted-certs, SSH host keysNoRestore them to avoid certificate and host key warnings.
Redis, ElasticsearchNoRedis holds Sidekiq's job queue; Elasticsearch can be reindexed from PostgreSQL.
Global server hooks, file hooksNoCopy them yourself if you use them.

GitLab leaves the secrets out on purpose: the database holds values encrypted with keys from gitlab-secrets.json, and keeping the key beside the data defeats the encryption. Lose the file and GitLab cannot decrypt CI/CD variables, runner authentication, deploy tokens, integrations, webhooks or project mirroring settings, jobs get stuck, pages return 500 errors, and users with two-factor authentication cannot sign in. The file can change during upgrades, so save it with every backup.

Create a backup

Terminal
sudo gitlab-backup create

That command exists since GitLab 12.2; on 12.1 and earlier it was gitlab-rake gitlab:backup:create. The archive is named <backup-id>_gitlab_backup.tar, where the ID is a Unix timestamp, the date, and the version and edition, for example 1791079200_2026_10_04_19.4.1-ce. You need that ID, and that version, to restore. Archives belong to git with mode 0600, so whatever copies them off the server must run as root or git. Options go after the command:

  • STRATEGY=copy copies the data to a temporary folder before archiving. Use it if the backup fails with file changed as we read it; it needs up to the same amount of disk space again.
  • SKIP= leaves parts out, comma-separated: db, repositories, uploads, builds (job logs), artifacts, pages, lfs, terraform_state, registry, packages, ci_secure_files, external_diffs and a few newer ones. SKIP=tar leaves the parts unpacked in the backup folder instead of building the tar.
  • BACKUP=dump replaces the generated ID with a fixed name, dump_gitlab_backup.tar, which suits rsync. Pruning, below, then no longer works.

Before an upgrade, GitLab suggests a database-only backup you can roll back to:

Terminal
sudo gitlab-backup create SKIP=artifacts,repositories,registry,uploads,builds,pages,lfs,packages,terraform_state

Schedule it, with the secrets and retention

GitLab's own example is a root cron line, 0 2 * * * /opt/gitlab/bin/gitlab-backup create CRON=1; CRON=1 hides the progress output unless something fails. For /etc/gitlab it offers sudo gitlab-ctl backup-etc, which writes a root-only tar to /etc/gitlab/config_backup/, and it recommends keeping that apart from the application backups. If both will end up in the same storage, encrypt the configuration to a GPG key whose private half lives elsewhere (how). This script does that, then runs the backup:

/usr/local/bin/gitlab-backup.sh
#!/usr/bin/env bash
set -euo pipefail
umask 077

KEY="<gpg-key-fingerprint>"
CONFIG_DIR="/var/backups/gitlab-config"
KEEP_DAYS=7
OUT="$CONFIG_DIR/gitlab-config-$(date +%Y-%m-%d_%H%M).tar.gz.gpg"

trap 'rm -f "$OUT.partial"' EXIT
mkdir -p "$CONFIG_DIR"

tar -czf - -C / --exclude=etc/gitlab/config_backup etc/gitlab etc/ssh \
  | gpg --batch --encrypt --recipient "$KEY" --output "$OUT.partial"
mv "$OUT.partial" "$OUT"
find "$CONFIG_DIR" -name 'gitlab-config-*.tar.gz.gpg' -type f -mtime +"$KEEP_DAYS" -delete

/opt/gitlab/bin/gitlab-backup create CRON=1
Terminal
sudo chmod 700 /usr/local/bin/gitlab-backup.sh
/etc/cron.d/gitlab-backup
0 2 * * * root /usr/local/bin/gitlab-backup.sh >> /var/log/gitlab-backup.log 2>&1
  • The configuration is saved first, so the secrets are kept even when the application backup fails.
  • --exclude keeps earlier backup-etc archives out; etc/ssh brings the server's SSH host keys, which GitLab also recommends keeping.
  • With pipefail, a failure in tar or gpg stops the script, and the trap removes the unfinished file.

GitLab prunes its own archives by age, in seconds. Set that in /etc/gitlab/gitlab.rb, where backup_path (shown at its default) moves the archives elsewhere, then run sudo gitlab-ctl reconfigure:

/etc/gitlab/gitlab.rb
gitlab_rails['backup_keep_time'] = 604800
gitlab_rails['backup_path'] = '/var/opt/gitlab/backups'

604800 seconds is 7 days. Older archives are deleted the next time a backup runs; the default, 0, keeps everything. Pruning skips custom BACKUP= names and never touches object storage, so give a bucket a lifecycle rule.

Upload backups to object storage

gitlab-backup can upload each archive itself once it is built. For Amazon S3, add this to /etc/gitlab/gitlab.rb and reconfigure:

/etc/gitlab/gitlab.rb
gitlab_rails['backup_upload_connection'] = {
  'provider' => 'AWS',
  'region' => 'eu-west-1',
  'aws_access_key_id' => '<access-key-id>',
  'aws_secret_access_key' => '<secret-access-key>'
}
gitlab_rails['backup_upload_remote_directory'] = '<bucket-name>'
  • On EC2, 'use_iam_profile' => true replaces the two keys; the instance role needs s3:PutObject, s3:GetObject and s3:DeleteObject on the bucket's objects.
  • For S3-compatible storage add 'endpoint' => 'https://<endpoint>'; GitLab's example is DigitalOcean Spaces. If uploads fail with 411 Length Required, set aws_signature_version to 2. If Spaces answers 400 Bad Request, remove gitlab_rails['backup_encryption'], which it does not support.
  • gitlab_rails['backup_upload_storage_options'] = { 'server_side_encryption' => 'AES256' } turns on S3-managed encryption.
  • Google Cloud Storage and Azure Blob Storage use their own provider settings; a mounted share uses Local with local_root, which must not be the same folder as backup_path.
  • DIRECTORY=daily puts one run's upload in a subfolder; SKIP=remote skips the upload for one run.

The secrets rule still applies: the upload carries the archive only, never /etc/gitlab.

Large instances

The backup command runs pg_dump and bundles every repository, so it slows as GitLab grows. GitLab's guidance: past about 100 GB of database, pg_dump and the backup command are likely unusable. In GitLab's tests, 100 GB of repositories took a little over two hours to back up and upload to S3, and at around 400 GB the command is likely not viable for regular backups. What helps:

  • GITLAB_BACKUP_MAX_CONCURRENCY and GITLAB_BACKUP_MAX_STORAGE_CONCURRENCY back up repositories in parallel; the defaults are one per logical CPU and two per storage.
  • Incremental repository backups, on by default since 14.10: sudo gitlab-backup create INCREMENTAL=yes PREVIOUS_BACKUP=<backup-id> packs only changes since that backup (PREVIOUS_BACKUP arrived in 15.0). Only repositories are incremental; everything else is backed up in full, and the archive still restores on its own.
  • Server-side repository backups, since 16.3: with a backup destination configured in Gitaly, REPOSITORIES_SERVER_SIDE=true makes each Gitaly node stream its repositories straight to object storage. Never let a lifecycle rule delete those increments: each depends on all the ones before it.
  • Beyond that, GitLab suggests backing up PostgreSQL another way (a managed database's own backups, for example) with SKIP=db, and repositories with disk snapshots or LVM snapshots taken while GitLab is stopped or read-only.

Docker installs

In the official Docker image, run the same commands from the host through docker exec. Here the container is named gitlab:

Terminal
docker exec -t gitlab gitlab-backup create

With the volumes from GitLab's install guide ($GITLAB_HOME set to /srv/gitlab), archives appear on the host in /srv/gitlab/data/backups and the secrets in /srv/gitlab/config/gitlab-secrets.json; back up the whole /srv/gitlab/config folder. If every setting comes from GITLAB_OMNIBUS_CONFIG, there is no gitlab.rb to save, but the secrets file still matters. For cron, put docker exec -t gitlab in front of the same command on the host.

Restore on the Linux package

A restore needs a working GitLab of exactly the same version and edition as the backup, CE or EE; the backup ID tells you which. Install that version first and upgrade after the restore. On Debian or Ubuntu, with GitLab's package repository set up:

Terminal
sudo apt install gitlab-ce=<version>-ce.0

Once sudo gitlab-ctl reconfigure has run, put your configuration back. Move the fresh /etc/gitlab aside, unpack yours (on the machine with the private key) and reconfigure:

Terminal
sudo mv /etc/gitlab /etc/gitlab.fresh
Terminal
gpg --decrypt gitlab-config-2026-10-04_0200.tar.gz.gpg | sudo tar -xzf - -C / etc/gitlab
Terminal
sudo gitlab-ctl reconfigure

Copy the archive into the backup folder, owned by git:

Terminal
sudo cp 1791079200_2026_10_04_19.4.1-ce_gitlab_backup.tar /var/opt/gitlab/backups/
Terminal
sudo chown git:git /var/opt/gitlab/backups/1791079200_2026_10_04_19.4.1-ce_gitlab_backup.tar

Stop the two services connected to the database, leave the rest running, and check:

Terminal
sudo gitlab-ctl stop puma
Terminal
sudo gitlab-ctl stop sidekiq
Terminal
sudo gitlab-ctl status

Restore by ID, leaving off _gitlab_backup.tar. It overwrites the database, and asks before deleting tables and before rebuilding authorized_keys; GITLAB_ASSUME_YES=1 in front answers yes for scripts. Then reconfigure and start:

Terminal
sudo gitlab-backup restore BACKUP=1791079200_2026_10_04_19.4.1-ce
Terminal
sudo gitlab-ctl reconfigure
Terminal
sudo gitlab-ctl start

Restore into a fresh install. PostgreSQL data is replaced, but a repository that already exists at the same path makes the restore fail, and object storage buckets are not cleared.

In Docker, put the secrets in /srv/gitlab/config and the archive in /srv/gitlab/data/backups first, run the same steps with docker exec -it gitlab in front, and finish with docker restart gitlab instead of gitlab-ctl start.

Check the restore

Terminal
sudo gitlab-rake gitlab:check SANITIZE=true
Terminal
sudo gitlab-rake gitlab:doctor:secrets
  • gitlab:check checks the configuration and services; SANITIZE=true leaves project names out of the output.
  • gitlab:doctor:secrets tries to decrypt every encrypted value and prints a failure count per model and a total. Every count should be 0; anything else means the secrets file does not belong to this database. It can take a long time on a big database.
  • gitlab:artifacts:check, gitlab:lfs:check and gitlab:uploads:check verify the restored files.
  • GitLab recommends refreshing database statistics afterwards: SET STATEMENT_TIMEOUT=0 ; ANALYZE VERBOSE; in the database console.
  • WebAuthn devices such as security keys are tied to the domain. Restored under a different domain, users must register them again.

Run this on a throwaway server on a schedule, not only after a disaster: a successful restore is the only proof that an archive and its secrets belong together. Testing restores covers the routine. To get back a single project, restore into a temporary GitLab of the same version, export the project there, and import it on the real instance.

Common errors

ErrorFix
GitLab version mismatch: Your current GitLab version (16.5.0-ee) differs from the GitLab version in the backup!Install the version the message names, restore, then upgrade.
file changed as we read itData changed while tar read it. Run the backup with STRATEGY=copy.
gzip: stdout: Input/output error, then Backup failedCheck disk space: the default strategy can need half the instance's size free. On NFS, check the timeout mount option has not been lowered from 600.
ActiveRecord::StatementInvalid: PG::UndefinedTableThe backup went through PgBouncer. Point it at the primary with GITLAB_BACKUP_PGHOST and GITLAB_BACKUP_PGPORT.
ERROR: must be owner of extension pg_trgm (or btree_gist, plpgsql)Harmless during a restore; GitLab says the backup is restored in spite of these.
ERROR: permission denied to create extension "pg_stat_statements"The source database had pg_stat_statements in the public schema. Remove its lines from db/database.sql.gz in the archive, or move the extension to another schema before the next backup.
411 Length Required on uploadThe S3-compatible provider needs aws_signature_version 2.
500 errors, stuck jobs or 2FA users locked out after a restoregitlab-secrets.json is missing or from another server. Restore the right one, reconfigure, and confirm with gitlab:doctor:secrets.
Registry pushes fail with permission denied after a restoreThe restore ran as git. Run sudo chown -R registry:registry /var/opt/gitlab/gitlab-rails/shared/registry/docker.

Frequently asked questions

Does gitlab-backup include gitlab-secrets.json?
No. Neither gitlab-secrets.json nor gitlab.rb is in the archive. Copy /etc/gitlab separately, for example with sudo gitlab-ctl backup-etc, and keep it apart from the archives or encrypted.
Where does GitLab store its backups?
In /var/opt/gitlab/backups on Linux package installs, set by gitlab_rails['backup_path']. With GitLab's Docker volumes that is /srv/gitlab/data/backups on the host.
Can I restore a GitLab backup on a newer version?
No. The restore needs exactly the same version and edition, CE or EE. Install that version, restore, then upgrade.
Can I restore a single project from a GitLab backup?
Not directly. Restore the backup into a temporary GitLab of the same version, export the project there, and import it on your real instance.
How do I delete old GitLab backups automatically?
Set gitlab_rails['backup_keep_time'] in seconds and reconfigure; older archives are pruned when the next backup runs. It does not work with custom BACKUP names or in object storage.

How this was checked

Commands, limits and prices were checked against these official pages, on October 4, 2026: