gitlab-backup2s3: Encrypted GitLab Backups to S3, with Kubernetes Support
Source code: github.com/sgaunet/gitlab-backup2s3 Helm chart: github.com/sgaunet/helm-gitlab-backup2s3
In the previous article, I covered gitlab-backup, a CLI tool for exporting GitLab projects and groups as portable archives. It handles the export, the restore, and supports both local and S3 storage natively.
But for production backup workflows — especially in a Kubernetes environment — you often want more: scheduled execution, optional encryption at rest, and a container image that bundles everything together. That’s exactly what gitlab-backup2s3 brings to the table.
What Is gitlab-backup2s3?
gitlab-backup2s3 is a Docker image that wraps gitlab-backup and adds two things:
- Optional AES-GCM encryption of archives before upload, powered by gocrypt
- First-class Kubernetes deployment via raw manifests or a Helm chart
The typical workflow it implements is:
GitLab Export API → tar.gz archive → (optional: gocrypt encrypt) → S3
If you just need gitlab-backup without encryption or container scheduling, the base tool is sufficient. gitlab-backup2s3 targets the use case where backups are automated, run in a cluster, and need to be stored securely in object storage.
A Note on Encryption Compatibility
⚠️ gocrypt v2 introduced AES GCM (Galois/Counter Mode) encryption, which is not backwards compatible with v1. Archives encrypted with v1 cannot be decrypted with v2 and vice versa. gitlab-backup2s3 v2 uses gocrypt v2. If you have archives encrypted with v1, decrypt them first with the v1 tooling before upgrading.
Configuration
gitlab-backup2s3 is configured entirely through environment variables. There is no configuration file — this is intentional for container-native deployments where secrets and settings come from the orchestrator.
| Variable | Description | Default |
|---|---|---|
GITLAB_TOKEN | GitLab personal access token | (required) |
GITALB_URI | GitLab base URL | https://gitlab.com |
GITLABPROJECTID | ID of a single project to export | — |
GITLABGROUPID | ID of a group to export (all sub-projects) | — |
GOCRYPT_KEY | Encryption key for AES-GCM encryption | (omit to skip encryption) |
POSTBACKUP | Post-backup hook; set to gocrypt enc --i %INPUTFILE% to encrypt | "" |
S3ENDPOINT | S3 endpoint URL (e.g. https://s3.eu-west-3.amazonaws.com) | — |
S3REGION | S3 region | — |
S3BUCKETNAME | Destination S3 bucket | — |
S3BUCKETPATH | Path prefix inside the bucket | — |
AWS_ACCESS_KEY_ID | AWS access key | (optional if using IAM role) |
AWS_SECRET_ACCESS_KEY | AWS secret key | (optional if using IAM role) |
LOCALPATH | Export archives locally instead of S3 | "" |
EXPORT_TIMEOUT_MIN | Per-project export timeout in minutes | 10 |
TMPDIR | Temporary working directory | /tmp |
DEBUGLEVEL | Log level: debug, info, warn, error | info |
On AWS credentials: if you’re running in EKS with an IAM role associated to the pod’s service account (IRSA), or on an EC2 instance with an instance profile, you don’t need to supply AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY at all — the SDK will pick up credentials automatically from the instance metadata service.
To enable encryption, set both GOCRYPT_KEY (the AES key) and POSTBACKUP to the gocrypt invocation:
GOCRYPT_KEY="your-secret-key"
POSTBACKUP="gocrypt enc --i %INPUTFILE%"
Deploying on Kubernetes
Raw Manifests
The repository includes ready-to-use Kubernetes manifests in the deploy/k8s folder. These define a CronJob resource that runs gitlab-backup2s3 on a schedule, with environment variables populated from a Secret.
This is the quickest path if you want to stay close to the metal without introducing Helm.
Helm Chart
For teams using Helm, a dedicated chart is available at github.com/sgaunet/helm-gitlab-backup2s3. It wraps the same CronJob manifest with full values.yaml configurability.
Key chart parameters:
| Key | Description | Default |
|---|---|---|
schedule | CronJob schedule expression | 00 01 1 * * (monthly) |
concurrencyPolicy | Whether to allow concurrent job runs | Forbid |
backoffLimit | Number of retries on failure | 2 |
failedJobsHistoryLimit | How many failed job pods to retain | 10 |
successfulJobsHistoryLimit | How many successful job pods to retain | 3 |
image.repository | Container image | ghcr.io/sgaunet/gitlab-backup2s3 |
serviceAccount.create | Create a dedicated service account | true |
resources.limits.memory | Memory limit | 128Mi |
configuration.* | All environment variables above | (see values.yaml) |
The additionalEnvFrom field lets you inject additional ConfigMap or Secret references into the pod, which is useful for supplying AWS credentials or other secrets managed externally (e.g. via Vault or External Secrets Operator).
A minimal values.yaml for a group backup to AWS S3 might look like:
schedule: "0 2 * * *" # every night at 2am
configuration:
GITLAB_TOKEN: "glpat-xxxxx"
GITLABGROUPID: "123"
S3ENDPOINT: "https://s3.eu-west-3.amazonaws.com"
S3REGION: "eu-west-3"
S3BUCKETNAME: "my-gitlab-backups"
S3BUCKETPATH: "daily"
EXPORT_TIMEOUT_MIN: "20"
GOCRYPT_KEY: "my-aes-key"
POSTBACKUP: "gocrypt enc --i %INPUTFILE%"
serviceAccount:
annotations:
eks.amazonaws.com/role-arn: "arn:aws:iam::123456789:role/gitlab-backup-role"
This will schedule a nightly backup of all projects in group 123, encrypt each archive with AES-GCM, and upload them to S3 — without any static AWS credentials in the pod, thanks to IRSA.
Summary
gitlab-backup handles the export and restore mechanics. gitlab-backup2s3 takes it further for production environments: it adds encryption, packages everything into a container image, and ships with Kubernetes deployment support out of the box.
The full toolchain for a secure, automated GitLab backup pipeline therefore looks like this:
gitlab-backup2s3 (CronJob)
└── gitlab-backup → export via GitLab API
└── gocrypt → AES-GCM encryption (optional)
└── S3 → durable remote storage
Both projects are available on GitHub and distributed under open-source licenses. Releases are published on GitHub and the container image is available at ghcr.io/sgaunet/gitlab-backup2s3.