Configure GitLab backup automation with GPG encryption

Intermediate 45 min May 27, 2026 77 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Set up automated GitLab backups with GPG encryption, remote storage, and systemd timers. This tutorial covers backup script creation, encryption setup, and monitoring for production GitLab instances.

Prerequisites

  • GitLab CE or EE installed and running
  • Root or sudo access
  • Remote backup server with SSH access
  • At least 10GB free disk space for temporary backup files

What this solves

GitLab contains critical data including repositories, issues, merge requests, and user accounts that need reliable backup protection. This tutorial sets up automated GitLab backups with GPG encryption, remote storage, and systemd scheduling to ensure your GitLab data is secure and recoverable.

Step-by-step configuration

Install required packages

Install GPG for encryption, rsync for remote transfers, and backup utilities.

sudo apt update
sudo apt install -y gnupg2 rsync gzip pigz
sudo dnf install -y gnupg2 rsync gzip pigz

Create backup user and directories

Create a dedicated user for backup operations with restricted permissions for security.

sudo useradd -r -s /bin/bash -d /opt/gitlab-backup gitlab-backup
sudo mkdir -p /opt/gitlab-backup/{scripts,logs,tmp}
sudo mkdir -p /var/backups/gitlab
sudo chown -R gitlab-backup:gitlab-backup /opt/gitlab-backup
sudo chown gitlab-backup:gitlab-backup /var/backups/gitlab
sudo chmod 750 /opt/gitlab-backup /var/backups/gitlab

Generate GPG encryption key

Create a dedicated GPG key for backup encryption. Use a strong passphrase and store it securely.

sudo -u gitlab-backup gpg --full-generate-key

When prompted, select:

  • Key type: RSA and RSA (default)
  • Key size: 4096
  • Validity: 0 (key does not expire)
  • Real name: GitLab Backup
  • Email: backup@example.com

List the key to get the key ID:

sudo -u gitlab-backup gpg --list-secret-keys --keyid-format LONG

Configure GitLab backup settings

Modify GitLab configuration to optimize backup creation and set the backup path.

gitlab_rails['backup_path'] = "/var/backups/gitlab"
gitlab_rails['backup_archive_permissions'] = 0640
gitlab_rails['backup_pg_schema'] = 'public'
gitlab_rails['backup_keep_time'] = 604800
gitlab_rails['backup_upload_connection'] = {
  'provider' => 'Local'
}

Reconfigure GitLab to apply the changes:

sudo gitlab-ctl reconfigure

Create backup encryption script

Create the main backup script that handles GitLab backup creation, encryption, and cleanup.

#!/bin/bash

GitLab Backup Script with GPG Encryption

set -euo pipefail

Configuration

BACKUP_DIR="/var/backups/gitlab" LOG_DIR="/opt/gitlab-backup/logs" TMP_DIR="/opt/gitlab-backup/tmp" REMOTE_HOST="backup-server.example.com" REMOTE_PATH="/backup/gitlab" REMOTE_USER="backup" GPG_RECIPIENT="backup@example.com" RETENTION_DAYS=30 LOG_FILE="$LOG_DIR/backup-$(date +%Y%m%d).log"

Logging function

log() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE" }

Error handling

error_exit() { log "ERROR: $1" exit 1 }

Start backup process

log "Starting GitLab backup process"

Create GitLab backup

log "Creating GitLab backup" gitlab-backup create BACKUP=gitlab_backup_$(date +%s_%Y_%m_%d_%H_%M_%S) || error_exit "GitLab backup creation failed"

Find the latest backup file

LATEST_BACKUP=$(ls -t "$BACKUP_DIR"/*.tar 2>/dev/null | head -1) if [[ -z "$LATEST_BACKUP" ]]; then error_exit "No backup file found" fi log "Latest backup: $LATEST_BACKUP"

Compress and encrypt backup

BACKUP_NAME=$(basename "$LATEST_BACKUP" .tar) ENCRYPTED_FILE="$TMP_DIR/${BACKUP_NAME}.tar.gz.gpg" log "Compressing and encrypting backup" pigz -c "$LATEST_BACKUP" | gpg --trust-model always --encrypt -r "$GPG_RECIPIENT" --cipher-algo AES256 --compress-algo 1 --output "$ENCRYPTED_FILE" || error_exit "Encryption failed"

Verify encrypted file

if [[ ! -f "$ENCRYPTED_FILE" ]] || [[ ! -s "$ENCRYPTED_FILE" ]]; then error_exit "Encrypted backup file is missing or empty" fi log "Encrypted backup size: $(du -h $ENCRYPTED_FILE | cut -f1)"

Upload to remote storage

log "Uploading backup to remote storage" rsync -avz --progress "$ENCRYPTED_FILE" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH}/" || error_exit "Remote upload failed"

Cleanup local files

log "Cleaning up local backup files" rm -f "$LATEST_BACKUP" rm -f "$ENCRYPTED_FILE"

Remove old backups from remote storage

log "Cleaning old backups on remote storage" ssh "${REMOTE_USER}@${REMOTE_HOST}" "find ${REMOTE_PATH} -name '*.tar.gz.gpg' -type f -mtime +${RETENTION_DAYS} -delete" || log "WARNING: Could not clean old remote backups"

Remove old local log files

find "$LOG_DIR" -name "backup-*.log" -type f -mtime +7 -delete || log "WARNING: Could not clean old log files" log "GitLab backup completed successfully"

Send completion notification

echo "GitLab backup completed at $(date)" | logger -t gitlab-backup

Make the script executable:

sudo chmod 750 /opt/gitlab-backup/scripts/gitlab-backup.sh
sudo chown gitlab-backup:gitlab-backup /opt/gitlab-backup/scripts/gitlab-backup.sh

Create backup restore script

Create a script to decrypt and restore GitLab backups when needed.

#!/bin/bash

GitLab Restore Script

set -euo pipefail

Configuration

BACKUP_DIR="/var/backups/gitlab" TMP_DIR="/opt/gitlab-backup/tmp" LOG_DIR="/opt/gitlab-backup/logs" LOG_FILE="$LOG_DIR/restore-$(date +%Y%m%d-%H%M%S).log"

Logging function

log() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE" }

Error handling

error_exit() { log "ERROR: $1" exit 1 }

Check if backup file provided

if [[ $# -ne 1 ]]; then echo "Usage: $0 " exit 1 fi ENCRYPTED_FILE="$1" if [[ ! -f "$ENCRYPTED_FILE" ]]; then error_exit "Backup file not found: $ENCRYPTED_FILE" fi log "Starting GitLab restore process" log "Backup file: $ENCRYPTED_FILE"

Extract backup name

BACKUP_NAME=$(basename "$ENCRYPTED_FILE" .tar.gz.gpg) DECRYPTED_FILE="$TMP_DIR/${BACKUP_NAME}.tar"

Decrypt backup

log "Decrypting backup file" gpg --decrypt "$ENCRYPTED_FILE" | pigz -d > "$DECRYPTED_FILE" || error_exit "Decryption failed"

Move to GitLab backup directory

mv "$DECRYPTED_FILE" "$BACKUP_DIR/" DECRYPTED_FILE="$BACKUP_DIR/${BACKUP_NAME}.tar"

Stop GitLab services

log "Stopping GitLab services" gitlab-ctl stop unicorn gitlab-ctl stop puma gitlab-ctl stop sidekiq

Restore GitLab backup

log "Restoring GitLab from backup" gitlab-backup restore BACKUP="$BACKUP_NAME" || error_exit "GitLab restore failed"

Start GitLab services

log "Starting GitLab services" gitlab-ctl start

Cleanup

rm -f "$DECRYPTED_FILE" log "GitLab restore completed successfully" log "Please run: gitlab-rake gitlab:check SANITIZE=true"

Make the restore script executable:

sudo chmod 750 /opt/gitlab-backup/scripts/gitlab-restore.sh
sudo chown gitlab-backup:gitlab-backup /opt/gitlab-backup/scripts/gitlab-restore.sh

Set up SSH key authentication for remote storage

Configure passwordless SSH for secure remote backup transfers.

sudo -u gitlab-backup ssh-keygen -t ed25519 -f /opt/gitlab-backup/.ssh/id_ed25519 -N ""
sudo -u gitlab-backup ssh-copy-id backup@backup-server.example.com

Test the SSH connection:

sudo -u gitlab-backup ssh backup@backup-server.example.com "mkdir -p /backup/gitlab"

Create systemd service and timer

Configure systemd to run backups automatically on a schedule.

[Unit]
Description=GitLab Backup Service
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
User=gitlab-backup
Group=gitlab-backup
ExecStart=/opt/gitlab-backup/scripts/gitlab-backup.sh
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ReadWritePaths=/var/backups/gitlab /opt/gitlab-backup
NoNewPrivileges=true
[Unit]
Description=Run GitLab backup daily
Requires=gitlab-backup.service

[Timer]
OnCalendar=daily
RandomizedDelaySec=3600
Persistent=true

[Install]
WantedBy=timers.target

Enable and start the timer:

sudo systemctl daemon-reload
sudo systemctl enable gitlab-backup.timer
sudo systemctl start gitlab-backup.timer

Create backup monitoring script

Set up monitoring to alert when backups fail or are missing.

#!/bin/bash

GitLab Backup Monitor

set -euo pipefail REMOTE_HOST="backup-server.example.com" REMOTE_PATH="/backup/gitlab" REMOTE_USER="backup" ALERT_EMAIL="admin@example.com" MAX_AGE_HOURS=25

Check if latest backup exists and is recent

LATEST_BACKUP=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" "find ${REMOTE_PATH} -name '*.tar.gz.gpg' -type f -printf '%T@ %p\n' | sort -n | tail -1 | cut -d' ' -f2" || echo "") if [[ -z "$LATEST_BACKUP" ]]; then echo "CRITICAL: No GitLab backups found on remote storage" | mail -s "GitLab Backup Alert" "$ALERT_EMAIL" exit 1 fi

Check backup age

BACKUP_TIME=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" "stat -c %Y '$LATEST_BACKUP'") CURRENT_TIME=$(date +%s) AGE_HOURS=$(( (CURRENT_TIME - BACKUP_TIME) / 3600 )) if [[ $AGE_HOURS -gt $MAX_AGE_HOURS ]]; then echo "WARNING: Latest GitLab backup is $AGE_HOURS hours old (file: $LATEST_BACKUP)" | mail -s "GitLab Backup Age Warning" "$ALERT_EMAIL" exit 1 fi echo "GitLab backup check passed: Latest backup is $AGE_HOURS hours old" exit 0

Make the monitor script executable and create a timer:

sudo chmod 750 /opt/gitlab-backup/scripts/backup-monitor.sh
sudo chown gitlab-backup:gitlab-backup /opt/gitlab-backup/scripts/backup-monitor.sh

Configure backup verification

Create a script to periodically verify backup integrity.

#!/bin/bash

GitLab Backup Verification

set -euo pipefail REMOTE_HOST="backup-server.example.com" REMOTE_PATH="/backup/gitlab" REMOTE_USER="backup" TMP_DIR="/opt/gitlab-backup/tmp" LOG_FILE="/opt/gitlab-backup/logs/verify-$(date +%Y%m%d).log" log() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE" }

Download latest backup for verification

log "Starting backup verification" LATEST_BACKUP=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" "find ${REMOTE_PATH} -name '*.tar.gz.gpg' -type f -printf '%T@ %p\n' | sort -n | tail -1 | cut -d' ' -f2") if [[ -z "$LATEST_BACKUP" ]]; then log "ERROR: No backup found for verification" exit 1 fi BACKUP_NAME=$(basename "$LATEST_BACKUP") LOCAL_FILE="$TMP_DIR/$BACKUP_NAME" log "Downloading backup: $BACKUP_NAME" rsync -avz "${REMOTE_USER}@${REMOTE_HOST}:$LATEST_BACKUP" "$LOCAL_FILE"

Verify GPG signature and decrypt headers only

log "Verifying GPG encryption" if gpg --list-packets "$LOCAL_FILE" > /dev/null 2>&1; then log "SUCCESS: Backup GPG encryption verified" else log "ERROR: Backup GPG verification failed" rm -f "$LOCAL_FILE" exit 1 fi

Test partial decryption

log "Testing backup decryption" if gpg --decrypt "$LOCAL_FILE" 2>/dev/null | pigz -t; then log "SUCCESS: Backup can be decrypted and decompressed" else log "ERROR: Backup decryption or decompression failed" rm -f "$LOCAL_FILE" exit 1 fi

Cleanup

rm -f "$LOCAL_FILE" log "Backup verification completed successfully"

Report to monitoring system if using one

echo "GitLab backup verification passed" | logger -t gitlab-backup-verify

Make the script executable:

sudo chmod 750 /opt/gitlab-backup/scripts/backup-verify.sh
sudo chown gitlab-backup:gitlab-backup /opt/gitlab-backup/scripts/backup-verify.sh

Add GitLab backup user to required groups

Grant necessary permissions for the backup user to access GitLab data.

sudo usermod -a -G git gitlab-backup
sudo usermod -a -G gitlab-redis gitlab-backup
Never use chmod 777. The group membership provides the specific access needed without exposing files to all users on the system.

Verify your setup

Test the backup system to ensure everything works correctly.

# Test backup creation
sudo systemctl start gitlab-backup.service
sudo systemctl status gitlab-backup.service

Check backup logs

sudo tail -f /opt/gitlab-backup/logs/backup-$(date +%Y%m%d).log

Verify timer schedule

sudo systemctl list-timers gitlab-backup.timer

Test GPG functionality

sudo -u gitlab-backup gpg --list-keys

Test remote connectivity

sudo -u gitlab-backup ssh backup@backup-server.example.com "ls -la /backup/gitlab/"

Common issues

Symptom Cause Fix
Permission denied accessing GitLab files Backup user not in git group sudo usermod -a -G git gitlab-backup
GPG encryption fails GPG key not found or expired sudo -u gitlab-backup gpg --list-keys and regenerate if needed
Remote upload fails SSH authentication or network issue Test SSH: sudo -u gitlab-backup ssh backup@backup-server.example.com
Backup service fails to start Missing directories or permissions Check sudo journalctl -u gitlab-backup.service and verify paths
Out of disk space during backup Insufficient space in backup directory Increase disk space or adjust retention policy in script
GitLab backup command not found GitLab not properly installed Verify with which gitlab-backup and check GitLab installation

Next steps

With your GitLab backup automation configured, consider these additional enhancements:

Running this in production?

Want this handled for you? Setting up GitLab backups once is straightforward. Keeping them monitored, verified, tested for restoration, and maintaining encryption keys across environments is the harder operational work. See how we run infrastructure like this for European SaaS and development teams.

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle managed devops services for businesses that depend on uptime. From initial setup to ongoing operations.