Migrate from the bundled Redis, PostgreSQL, and MinIO charts

  • Tier: Free, Premium, Ultimate
  • Offering: GitLab Self-Managed

When configuring a production system, you should migrate from the bundled Redis, MinIO, and PostgreSQL to externally managed alternatives.

The bundled Redis, MinIO, and PostgreSQL were removed in GitLab 19.0 (GitLab Helm chart 10.0). You must migrate to external solutions before you upgrade.

This guide assumes you’re migrating to Cloud Native alternatives such as Valkey, Garage, and CloudNativePG respectively.

This migration process requires you to perform the following steps:

  • Provision external services: Deploy and configure your chosen external services.
  • Back up your data: Create a backup of all data from the bundled PostgreSQL and MinIO services.
  • Reconfigure GitLab: Update GitLab configuration to use the external services instead of bundled ones.
  • Restore to new services: Restore your backup data to the newly provisioned external services.
  • Clean up old services: Manually delete the old bundled services and their persistent volumes when you’re confident the migration is complete.

Before you begin

Before you begin migrating from the bundled Redis, MinIO, or PostgreSQL:

  • Evaluate services that align with the installation requirements. Consider cloud provider services or other alternatives that meet your infrastructure needs and organizational requirements. For general reference architecture considerations and recommended providers, see the reference architecture documentation.
  • As a result of this migration, upgrading the GitLab chart will no longer upgrade your Redis or PostgreSQL deployments. Major GitLab upgrades may require newer versions of Valkey/Redis or PostgreSQL. Before following this guide, or before doing a major GitLab upgrade, check the requirements for your GitLab version.
  • Check the current size and data usage of your MinIO, Redis, and PostgreSQL persistent volume claims. The guide configures 5 GiB for PostgreSQL, 2 GiB for Valkey, and 5 GiB (replicated 3 times) for Garage which might need adjustment.
  • Note that GitLab cannot assist with the configuration or troubleshooting of third-party applications mentioned in this document. We can ensure that GitLab itself is sending properly formatted data to a third party in the bare-minimum configuration.
  • Plan in downtime for this migration. During the import of the data into the new external services, GitLab won’t be accessible.

Backup GitLab

First back up all of the current data and note the backup ID.

Please note that:

  • If you are migrating of MinIO, you will need to download the backup archive to a local machine.
  • If you are only migrating MinIO, you will need to backup the object storage buckets only.
  • If you are only migrating Redis, you can skip the backup and restore steps.
  • If you are only migrating PostgreSQL, you can skip backing up all components but the db.
  • If you enabled the Registry Metadata Database the metadata data will not be covered by the default backup/restore process.

Provision external services

To replace the bundled Redis, PostgreSQL, and MinIO charts, provision externally managed replacements. For an overview on the available options check the recommended infrastructure and services and make sure they meet the current minimum requirements.

Provision external Valkey or Redis

Provision your external Valkey or Redis service. For example, by using the official Valkey Helm chart.

The following install command uses these settings to protect the Valkey data, which includes user sessions:

  • valkeyConfig="appendonly yes" turns on the append-only file (AOF). Valkey logs every change and, by default, writes the log to disk every second. Without AOF, Valkey saves only periodic snapshots. If the node that runs Valkey fails without a graceful shutdown, all changes after the last snapshot are lost, and users must sign in again.
  • deploymentStrategy=Recreate stops the old Valkey Pod before the new Pod starts during an upgrade. Two Valkey instances then don’t use the same persistent volume at the same time.

If you already run Valkey without AOF, do not add valkeyConfig="appendonly yes" to an existing release. When Valkey restarts with AOF turned on and no AOF exists, Valkey starts with an empty dataset and ignores the existing snapshot. Instead, turn on AOF for an existing Valkey instance.

  1. Install Valkey:

    This sets up an independent Valkey instance that retains data across restarts. The authentication credentials are stored in a Secret named <RELEASE>-auth.

    shell
    helm repo add valkey https://valkey.io/valkey-helm/
    helm install valkey valkey/valkey -n <NAMESPACE> --version 0.12.1 \
      --set dataStorage.enabled=true \
      --set dataStorage.requestedSize=2Gi \
      --set deploymentStrategy=Recreate \
      --set valkeyConfig="appendonly yes" \
      --set metrics.enabled=true \
      --set auth.enabled=true \
      --set auth.aclUsers.default.permissions="~* &* +@all" \
      --set auth.aclUsers.default.password="<RANDOM PASSWORD>"
  2. Confirm Valkey is up and running:

    script
    $ kubectl get deployment -n <NAMESPACE> -l app.kubernetes.io/name=valkey
    NAME     READY   UP-TO-DATE   AVAILABLE   AGE
    valkey   1/1     1            1           30m

Turn on AOF for an existing Valkey instance

If you installed Valkey without append-only file (AOF) (valkeyConfig="appendonly yes"), turn on AOF on the running instance before you change the Helm release. Valkey then writes the current dataset to the AOF and no data is lost.

  1. Optional. Back up the latest snapshot:

    shell
    kubectl exec -n <NAMESPACE> deploy/valkey -c valkey -- cat /data/dump.rdb > valkey-dump.rdb
  2. Switch the Deployment strategy to Recreate, so that a restart during this procedure doesn’t run two Valkey Pods on the same volume. Helm cannot change the strategy from RollingUpdate to Recreate because the existing Deployment keeps its rollingUpdate settings. This command does not restart the Pod:

    shell
    kubectl patch deployment valkey -n <NAMESPACE> \
      -p '{"spec":{"strategy":{"type":"Recreate","rollingUpdate":null}}}'
  3. Get the password of the default user:

    shell
    VALKEY_PASSWORD=$(kubectl get secret -n <NAMESPACE> valkey-auth \
      -o jsonpath='{.data.default-password}' | base64 --decode)

    If you use auth.usersExistingSecret, read the password from that Secret instead.

  4. Turn on AOF on the running instance:

    shell
    kubectl exec -n <NAMESPACE> deploy/valkey -c valkey -- \
      valkey-cli --no-auth-warning -a "$VALKEY_PASSWORD" CONFIG SET appendonly yes
  5. Wait until Valkey finishes writing the AOF:

    shell
    kubectl exec -n <NAMESPACE> deploy/valkey -c valkey -- \
      valkey-cli --no-auth-warning -a "$VALKEY_PASSWORD" INFO persistence \
      | grep -E 'aof_enabled|aof_rewrite_in_progress|aof_rewrite_scheduled|aof_last_bgrewrite_status'

    Continue only when the output matches:

    aof_enabled:1
    aof_rewrite_in_progress:0
    aof_rewrite_scheduled:0
    aof_last_bgrewrite_status:ok
  6. Persist the settings in the Helm release:

    shell
    helm upgrade valkey valkey/valkey -n <NAMESPACE> --version 0.12.1 --reset-then-reuse-values \
      --set deploymentStrategy=Recreate \
      --set valkeyConfig="appendonly yes"

    This upgrade restarts Valkey. With the Recreate strategy, Valkey is unavailable until the new Pod is ready. The new Pod loads its data from the AOF.

    Until this upgrade succeeds, AOF is turned on only in the running process. If Valkey restarts before the upgrade, AOF is turned off again and the AOF on the volume stops receiving changes. Repeat the steps to turn on AOF and wait for the AOF before you upgrade. Otherwise, Valkey loads the outdated AOF and loses the changes made after the restart.

Provision external PostgreSQL

Provision your external PostgreSQL service. For example, by using CloudNativePG:

  1. Install the CloudNativePG Operator:

    shell
    kubectl apply --server-side -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.28/releases/cnpg-1.28.0.yaml
  2. Provision a PostgreSQL cluster for GitLab (the Registry Metadata Database is not covered):

    Check the Cluster API to customize your cluster.

    yaml
    apiVersion: postgresql.cnpg.io/v1
    kind: Cluster
    metadata:
      name: gitlab-rails-db
      namespace: <NAMESPACE>
    spec:
      instances: 1
      imageName: ghcr.io/cloudnative-pg/postgresql:17
      storage:
        size: 5Gi
      bootstrap:
        initdb:
          database: gitlabhq_production
          owner: gitlab
          postInitSQL:
            - CREATE EXTENSION IF NOT EXISTS pg_trgm;
            - CREATE EXTENSION IF NOT EXISTS btree_gist;
            - CREATE EXTENSION IF NOT EXISTS plpgsql;
            - CREATE EXTENSION IF NOT EXISTS amcheck;
            - CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
  3. Confirm the PostgreSQL cluster is healthy:

    script
    $ kubectl get clusters -n <NAMESPACE>
    NAME                 AGE   INSTANCES   READY   STATUS                     PRIMARY
    gitlab-rails-db      20m   1           1       Cluster in healthy state   gitlab-rails-db-1

Provision Garage for external object storage

To migrate away from the bundled MinIO, you must provision your own external object storage solution.

One option is Garage. Before installing Garage, review the Garage documentation for:

Prerequisites:

  • Version 2.2.0 of the Garage App.
  1. Install the Garage Helm chart:

    shell
    helm plugin install https://github.com/aslafy-z/helm-git
    helm repo add garage "git+https://git.deuxfleurs.fr/Deuxfleurs/garage.git@script/helm?ref=v2.2.0"
    helm install garage garage/garage -n <NAMESPACE> \
      --set persistence.data.size=5Gi \
      --set persistence.meta.size=250Mi
  2. Confirm Garage is up and running:

    shell
    $ kubectl get statefulsets.apps -n <NAMESPACE> -l app.kubernetes.io/name=garage
    NAME     READY   AGE
    garage   3/3     36s
  3. Initialize the cluster layout.

    This example provisions a Garage layout with three zones, one node per zone, and uses the default replication factor of three. Review the Garage production recommendations and adjust these settings to suit your requirements.

    Since GitLab stores both primary object data and backups in the same storage backend (Garage in this case), any failures at the object storage or persistence layer could affect both datasets. Therefore, in addition to backing up GitLab regularly, you should also familiarize yourself with recovering from Garage failures.

    shell
    # Check node IDs
    kubectl exec <GARAGE_POD>  -- /garage status
    
    # Assign nodes to gitlab zone
    kubectl exec <GARAGE_POD>  -- /garage layout assign -z gitlab1 -c 5G <Node ID 1>
    kubectl exec <GARAGE_POD>  -- /garage layout assign -z gitlab2 -c 5G <Node ID 2>
    kubectl exec <GARAGE_POD>  -- /garage layout assign -z gitlab3 -c 5G <Node ID 3>
    
    # Apply the layout
    kubectl exec <GARAGE_POD>  -- /garage layout apply --version 1
  4. Set the namespace and Garage pod variables, replacing the NAMESPACE and GARAGE_POD placeholders with the correct values from your deployment. Run the remaining steps in this section in the same shell session, because they reuse these variables:

    shell
    NAMESPACE=<NAMESPACE>
    GARAGE_POD=<GARAGE_POD>
  5. Create the GitLab buckets:

    The following command uses the default bucket names from the GitLab chart. If you’ve customized your bucket names previously, adjust them accordingly here and in the steps below.

    shell
    buckets=("git-lfs" "gitlab-agent-plan-content" "gitlab-artifacts" "gitlab-backups" \
             "gitlab-ci-catalog-bundles" "gitlab-ci-secure-files" "gitlab-dependency-proxy" \
             "gitlab-mr-diffs" "gitlab-packages" "gitlab-pages" "gitlab-terraform-state" \
             "gitlab-uploads" "registry" "runner-cache" "tmp" )
    for bucket in "${buckets[@]}"; do
      kubectl exec -n ${NAMESPACE} ${GARAGE_POD}  -- /garage bucket create "${bucket}";
    done
  6. Create an API key and grant it access to the created buckets:

    shell
    KEY_OUTPUT=$(kubectl exec -n ${NAMESPACE} ${GARAGE_POD} -- \
        /garage key create gitlab-app-key)
    
    GARAGE_ACCESS_KEY=$(echo "${KEY_OUTPUT}" | grep 'Key ID:' | awk '{print $3}')
    GARAGE_SECRET_KEY=$(echo "${KEY_OUTPUT}" | grep 'Secret key:' | awk '{print $3}')
    
    # Grant permissions to the GitLab key.
    for bucket in "${buckets[@]}"; do
      kubectl exec -n ${NAMESPACE} ${GARAGE_POD}  -- /garage bucket allow --read --write --key gitlab-app-key "${bucket}";
    done
  7. Create a Secret configuring the object storage access:

    shell
    cat <<EOF | kubectl create secret -n ${NAMESPACE} generic gitlab-object-storage --from-file=config=/dev/stdin
    provider: AWS
    region: garage
    aws_access_key_id: ${GARAGE_ACCESS_KEY}
    aws_secret_access_key: ${GARAGE_SECRET_KEY}
    endpoint: "http://garage.${NAMESPACE}.svc.cluster.local:3900"
    path_style: true
    EOF
  8. Create a Secret configuring access for backup/restore:

    shell
    cat <<EOF | kubectl create secret -n ${NAMESPACE} generic gitlab-object-storage-s3cmd --from-file=config=/dev/stdin
    [default]
    access_key = ${GARAGE_ACCESS_KEY}
    secret_key = ${GARAGE_SECRET_KEY}
    host_base = garage.${NAMESPACE}.svc.cluster.local:3900
    host_bucket = garage.${NAMESPACE}.svc.cluster.local:3900
    use_https = False
    EOF
  9. Create a Secret configuring access for registry:

    shell
    cat <<EOF | kubectl create secret -n ${NAMESPACE} generic gitlab-registry-storage --from-file=config=/dev/stdin
    s3:
      accesskey: ${GARAGE_ACCESS_KEY}
      secretkey: ${GARAGE_SECRET_KEY}
      bucket: registry
      region: garage
      regionendpoint: http://garage.${NAMESPACE}.svc.cluster.local:3900
      secure: false
      v4auth: true
      pathstyle: true
    EOF

Migrate to external services

With all replacements provisioned, you can now disable the bundled MinIO, Redis, and PostgreSQL.

  1. Ensure the MinIO persistent volume will be retained for now.

    yaml
    minio:
      persistence:
        # keep: true # Only available in GitLab chart 9.8+
        annotations:
          helm.sh/resource-policy: "keep"
    shell
    helm upgrade <RELEASE> gitlab/gitlab -f your-values.yaml
    kubectl annotate pvc <RELEASE>-minio --list

    The Redis and PostgreSQL persistent volumes are managed by their StatefulSet instead of Helm. The default retention policy is Retain. Unless you modified this policy, these two volumes will not be deleted when you remove their StatefulSet.

  2. Update your values to point to the newly provisioned services:

    yaml
    global:
      # Configure DB managed by CloudNativePG.
      psql:
        host: gitlab-rails-db-rw.<NAMESPACE>.svc.cluster.local
        password:
         secret: gitlab-rails-db-app
         key: password
      # Configure Valkey service.
      redis:
        host: valkey.<NAMESPACE>.svc.cluster.local
        auth:
         secret: valkey-auth # <VALKEY RELEASE>-auth
         key: default-password
      # Configure Garage as object storage.
      appConfig:
        object_store:
          enabled: true
          # If you aren't exposing Garage through the Ingress Gateway API, set object storage download proxying to true
          proxy_download: true
          connection:
            secret: gitlab-object-storage
            key: config
        # Set to buckets created in Garage. Can be omitted if you used the default bucket names.
        artifacts:
          bucket: gitlab-artifacts
        lfs:
          bucket: git-lfs
        uploads:
          bucket: gitlab-uploads
        packages:
          bucket: gitlab-packages
        externalDiffs:
          enabled: true
          bucket: gitlab-mr-diffs
        terraformState:
          enabled: true
          bucket: gitlab-terraform-state
        ciSecureFiles:
          enabled: true
          bucket: gitlab-ci-secure-files
        agentPlanContent:
          enabled: true
          bucket: gitlab-agent-plan-content
        ciCatalogBundles:
          enabled: true
          bucket: gitlab-ci-catalog-bundles
        dependencyProxy:
          enabled: true
          bucket: gitlab-dependency-proxy
      # Disable bundled MinIO.
      minio:
        enabled: false
    # Configure backup/restore to use Garage backend.
    gitlab:
      toolbox:
        backups:
          objectStorage:
            config:
              secret: gitlab-object-storage-s3cmd
              key: config
    # Disable Registry redirect if not exposing Garage via Ingress/Gateway API
    registry:
      storage:
        secret: gitlab-registry-storage
        key: config
        redirect:
          disable: true
    # Disable bundled PostgreSQL and Redis.
    postgresql:
      install: false
    redis:
      install: false

    Check the related Redis, PostgreSQL, and object storage documentation for more information.

  3. Optional. To migrate GitLab Pages, add the following configuration under global. Pages is disabled by default. Add this only if your installation uses GitLab Pages.

    yaml
    global:
      pages:
        enabled: true
        objectStore:
          enabled: true
          bucket: gitlab-pages
          connection:
            secret: gitlab-object-storage
            key: config
  4. If you are migrating PostgreSQL, upgrade your GitLab instance with migrations disabled:

    shell
    helm upgrade <RELEASE> gitlab/gitlab -f your-values.yaml --set gitlab.migrations.enabled=false
  5. If you are migrating MinIO, copy your backup to the toolbox and upload it to your new object storage:

    shell
    # Find Toolbox Pod
    kubectl get pods -n <NAMESPACE> -l app=toolbox
    # Copy backup archive to Pod
    kubectl cp -n <NAMESPACE> -c toolbox LOCAL_BACKUP_ARCHIVE.tar <TOOLBOX_POD>:/tmp
    # Upload archive to backup bucket from inside the Pod
    kubectl exec -n <NAMESPACE> <TOOLBOX_POD> -c toolbox -- s3cmd put /tmp/LOCAL_BACKUP_ARCHIVE.tar s3://gitlab-backups/
  6. If you are migrating PostgreSQL or MinIO, scale down the workloads and restore the backup.

  7. After the upgrade is complete, upgrade your GitLab instance to run any pending migrations.

    shell
    helm upgrade <RELEASE> gitlab/gitlab -f your-values.yaml
  8. Confirm GitLab is operational.

  9. Confirm backups work as intended by doing a fresh backup.

  10. Delete Secrets and PersistentVolumeClaims related to the bundled PostgreSQL, MinIO, and Redis.

    shell
    kubectl delete pvc <RELEASE>-minio redis-data-<RELEASE>-redis-master-0 data-<RELEASE>-postgresql-0
    kubectl delete secret <RELEASE>-postgresql-password <RELEASE>-redis-secret <RELEASE>-minio-secret <RELEASE>-minio-tls