Migrating Gitea from SQLite to PostgreSQL
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.