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>.
What the backup contains, and what it leaves out
| Data | In the archive? | Notes |
|---|---|---|
| PostgreSQL database | Yes | Dumped with pg_dump and gzipped: issues, merge requests, users, permissions, encrypted CI/CD variables |
| Git repositories, wikis, snippets | Yes | One 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 files | Only on local disk | Anything in object storage is skipped; back up the bucket with your provider's tools. |
/etc/gitlab/gitlab-secrets.json | No | The database encryption keys. A restore needs it. |
/etc/gitlab/gitlab.rb | No | Your configuration |
/etc/gitlab/ssl, /etc/gitlab/trusted-certs, SSH host keys | No | Restore them to avoid certificate and host key warnings. |
| Redis, Elasticsearch | No | Redis holds Sidekiq's job queue; Elasticsearch can be reindexed from PostgreSQL. |
| Global server hooks, file hooks | No | Copy 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
sudo gitlab-backup createThat 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=copycopies the data to a temporary folder before archiving. Use it if the backup fails withfile 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_diffsand a few newer ones.SKIP=tarleaves the parts unpacked in the backup folder instead of building the tar.BACKUP=dumpreplaces 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:
sudo gitlab-backup create SKIP=artifacts,repositories,registry,uploads,builds,pages,lfs,packages,terraform_stateSchedule 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/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=1sudo chmod 700 /usr/local/bin/gitlab-backup.sh0 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.
--excludekeeps earlierbackup-etcarchives out;etc/sshbrings the server's SSH host keys, which GitLab also recommends keeping.- With
pipefail, a failure in tar or gpg stops the script, and thetrapremoves 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:
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:
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' => truereplaces the two keys; the instance role needss3:PutObject,s3:GetObjectands3:DeleteObjecton the bucket's objects. - For S3-compatible storage add
'endpoint' => 'https://<endpoint>'; GitLab's example is DigitalOcean Spaces. If uploads fail with411 Length Required, setaws_signature_versionto 2. If Spaces answers400 Bad Request, removegitlab_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
providersettings; a mounted share usesLocalwithlocal_root, which must not be the same folder asbackup_path. DIRECTORY=dailyputs one run's upload in a subfolder;SKIP=remoteskips 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_CONCURRENCYandGITLAB_BACKUP_MAX_STORAGE_CONCURRENCYback 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_BACKUParrived 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=truemakes 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:
docker exec -t gitlab gitlab-backup createWith 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:
sudo apt install gitlab-ce=<version>-ce.0Once 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:
sudo mv /etc/gitlab /etc/gitlab.freshgpg --decrypt gitlab-config-2026-10-04_0200.tar.gz.gpg | sudo tar -xzf - -C / etc/gitlabsudo gitlab-ctl reconfigureCopy the archive into the backup folder, owned by git:
sudo cp 1791079200_2026_10_04_19.4.1-ce_gitlab_backup.tar /var/opt/gitlab/backups/sudo chown git:git /var/opt/gitlab/backups/1791079200_2026_10_04_19.4.1-ce_gitlab_backup.tarStop the two services connected to the database, leave the rest running, and check:
sudo gitlab-ctl stop pumasudo gitlab-ctl stop sidekiqsudo gitlab-ctl statusRestore 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:
sudo gitlab-backup restore BACKUP=1791079200_2026_10_04_19.4.1-cesudo gitlab-ctl reconfiguresudo gitlab-ctl startRestore 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
sudo gitlab-rake gitlab:check SANITIZE=truesudo gitlab-rake gitlab:doctor:secretsgitlab:checkchecks the configuration and services;SANITIZE=trueleaves project names out of the output.gitlab:doctor:secretstries 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:checkandgitlab:uploads:checkverify 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
| Error | Fix |
|---|---|
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 it | Data changed while tar read it. Run the backup with STRATEGY=copy. |
gzip: stdout: Input/output error, then Backup failed | Check 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::UndefinedTable | The 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 upload | The S3-compatible provider needs aws_signature_version 2. |
| 500 errors, stuck jobs or 2FA users locked out after a restore | gitlab-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 restore | The 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:
- GitLab Docs: Back up and restore overview
- GitLab Docs: Back up GitLab
- GitLab Docs: Restore GitLab
- GitLab Docs: Troubleshooting GitLab backups
- GitLab Docs: Backup archive process (backup ID, default path)
- GitLab Docs: Linux package backups (backup-etc, backup_path)
- GitLab Docs: Back up GitLab running in a Docker container
- GitLab Docs: Install GitLab in a Docker container (volumes)
- GitLab Docs: Upgrade Linux package instances (installing a specific version)
- GitLab Docs: Integrity check Rake tasks (gitlab:doctor:secrets)
- GitLab Docs: Maintenance Rake tasks (gitlab:check)
- GitLab 13.12 docs: Back up and restore (command for 12.1 and earlier)
- GitLab 16.6 docs: Back up GitLab (version history of incremental and server-side backups)