Migrating Gitea from SQLite to PostgreSQL

Feb 24, 2026

I recently migrated my Gitea instance from SQLite to PostgreSQL managed by the CloudNativePG (CNPG) operator. Here’s how I did it.

Prerequisites

  • CloudNativePG operator installed in your cluster
  • ArgoCD managing your Gitea deployment
  • A backup strategy in place

Backup First

Gitea’s dump command can export directly in PostgreSQL format, which avoids manual SQL conversion:

GITEA_POD=$(kubectl get pod -n gitea -l app.kubernetes.io/name=gitea -o jsonpath='{.items[0].metadata.name}')

kubectl exec -n gitea $GITEA_POD -- gitea dump \
  --config /data/gitea/conf/app.ini \
  --database postgres \
  --file /tmp/gitea-postgres-export.zip

kubectl cp gitea/$GITEA_POD:/tmp/gitea-postgres-export.zip ./gitea-postgres-export.zip

unzip -j gitea-postgres-export.zip "gitea-db.sql" -d .

Then scale Gitea down to prevent data changes during migration:

kubectl scale deployment -n gitea gitea --replicas=0
kubectl wait --for=delete pod -n gitea -l app.kubernetes.io/name=gitea --timeout=120s

Create the PostgreSQL Cluster

Create a CNPG Cluster resource. A minimal setup looks like this:

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: gitea-postgresql
  namespace: gitea
spec:
  instances: 1
  bootstrap:
    initdb:
      database: gitea
      owner: gitea
  imageCatalogRef:
    apiGroup: postgresql.cnpg.io
    kind: ClusterImageCatalog
    name: postgresql
    major: 17
  storage:
    size: 5Gi
  walStorage:
    size: 2Gi

Wait for it to be ready:

kubectl wait --for=condition=Ready cluster/gitea-postgresql -n gitea --timeout=300s

Migrate the Data

Then port-forward to the new cluster and import:

PGUSER=$(kubectl get secret -n gitea gitea-postgresql-app -o jsonpath='{.data.username}' | base64 -d)
PGPASS=$(kubectl get secret -n gitea gitea-postgresql-app -o jsonpath='{.data.password}' | base64 -d)
PGDB=$(kubectl get secret -n gitea gitea-postgresql-app -o jsonpath='{.data.dbname}' | base64 -d)

kubectl port-forward -n gitea svc/gitea-postgresql-rw 5432:5432

PGPASSWORD=$PGPASS psql -h localhost -U $PGUSER -d $PGDB -f gitea-db.sql

Update the Gitea Helm Values

Point Gitea at the new PostgreSQL instance using environment variables sourced from the CNPG-generated secret. If you use network policies, also add an egress-postgres label to your deployment.

gitea:
  additionalConfigFromEnvs:
    - name: GITEA__DATABASE__DB_TYPE
      value: postgres
    - name: GITEA__DATABASE__HOST
      value: gitea-postgresql-rw.gitea.svc.cluster.local:5432
    - name: GITEA__DATABASE__NAME
      valueFrom:
        secretKeyRef:
          name: gitea-postgresql-app
          key: dbname
    - name: GITEA__DATABASE__USER
      valueFrom:
        secretKeyRef:
          name: gitea-postgresql-app
          key: username
    - name: GITEA__DATABASE__PASSWD
      valueFrom:
        secretKeyRef:
          name: gitea-postgresql-app
          key: password
    - name: GITEA__DATABASE__SSL_MODE
      value: disable

Commit and push so ArgoCD picks up the changes. The pod will start but stay unhealthy until the data is imported.

Sync and Validate

Trigger a final ArgoCD sync to bring Gitea up with the new configuration:

argocd app sync gitea --force
kubectl get pods -n gitea -w

Once the pod is running, verify the data made it across.

A few things worth knowing about CNPG: it creates three services for the cluster — gitea-postgresql-rw (primary), gitea-postgresql-ro (read replicas), and gitea-postgresql-r (any instance). The app secret (gitea-postgresql-app) contains username, password, dbname, host, and port, which map cleanly to Gitea’s config environment variables.