Post

ENG | Migrating Forgejo to PostgreSQL

This guide walks through migrating a Forgejo instance from MariaDB to PostgreSQL using Podman quadlets. Whether you're consolidating databases or prefer PostgreSQL's feature set, this step-by-step tutorial covers backup, restoration, and verification

ENG | Migrating Forgejo to PostgreSQL

Motivation

In June, I tried to migrate Forgejo from MariaDB to PostgreSQL to use one database for everything and gave up.

I don’t have the mental capacity to learn how to manage two different even one SQL server and its dialect properly. Fifteen or twenty years ago it was my job, but I use databases rarely today, and every time I come back to SQL I’ve forgotten half of it again.

Now I have a new server (ferret), so there’s a good opportunity to try again - the old server (marten) keeps running as usual in the meantime, so worst case I just fall back to it.

Prerequisites: I assume you’re already somewhat comfortable with podman quadlets and basic SQL — this isn’t an introduction to either. Tutorial also shows podman quadlets for database, but feel free to use docker-compose.yml. With docker compose, services are not started or stopped via systemctl obviously.

Also keep in mind that I have ~/dotfiles in git and there are symlinks to configs.

But I hope, you - reader - can adapt this workflow to your own setup.

Step 1. Create backup of old instance

1
2
3
4
5
6
7
8
9
10
11
12
13
[pavel@marten -=- ~]$ podman exec -it gitea bash
9fe18923c262:/var/lib/gitea$ forgejo dump --database postgres --config /var/lib/gitea/custom/conf/app.ini
2026/06/18 15:24:20 ...g/config_provider.go:349:deprecatedSettingWarning() [E] Deprecated config option `[log]` `logger.router.MODE` present. Use `[log]` `LOGGER_ROUTER_MODE` instead.
...
2026/06/18 15:24:20 cmd/dump.go:421:runDump() [I] Finished dumping in file forgejo-dump-1781789060.zip
9fe18923c262:/var/lib/gitea$ ls -la
...
-rw-------    1 git      git      131285092 Jun 18 15:24 forgejo-dump-1781789060.zip
9fe18923c262:/var/lib/gitea$ exit
exit

[pavel@marten -=- ~]$ podman cp gitea:/var/lib/gitea/forgejo-dump-1781789060.zip ~/tmp
[pavel@marten -=- ~]$ cd tmp

Step 2. Configure and restore database

forgejo-database is a private network just for Forgejo ↔ PostgreSQL traffic; shared_network is the one my reverse proxy and other containers sit on, so Forgejo needs both.

Podman quadlet files

1
2
[Network]
NetworkName=shared_network
1
2
[Network]
NetworkName=forgejo-database
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Unit]
Description=Forgejo PostgreSQL 18

[Container]
ContainerName=forgejo-postgresql
Image=docker.io/library/postgres:18-alpine
Network=forgejo-database
Volume=forgejo-postgresql-data:/var/lib/postgresql
AutoUpdate=registry

[Service]
Restart=always

[Install]
WantedBy=default.target

Restoring database

We need to create volume for SQL database(s)

1
2
3
4
5
6
7
8
9
10
11
12
# Create volume for database
podman volume create forgejo-postgresql-data
# Create systemd files
cd .config/containers/systemd
ln -s ~/dotfiles/.config/containers/systemd/forgejo-postgresql.container
ln -s ~/dotfiles/.config/containers/systemd/forgejo-database.network
# Reload configs and check what went wrong (likely forgotten network)
systemctl --user daemon-reload
systemctl --user start forgejo-database-network.service
systemctl --user start forgejo-postgresql.service
systemctl --user status forgejo-postgresql.service
journalctl --user -xeu forgejo-postgresql

Once container is running proceed with database. Unzip dump from step 1

1
2
3
4
5
6
7
# Create directory for backups
mkdir -p ~/backup/forgejo
# Copy and unzip backup
cd ~/backup/forgejo
scp [email protected]:~/tmp/forgejo-dump-1788207667.zip .
unzip forgejo-dump-1788207667.zip
ls -la

Copy database into container and then continue inside

1
2
3
4
# Copy database dump (content of zip file above) to the container
podman cp forgejo-db.sql forgejo-postgresql:/tmp/
# Launch bash inside container
podman exec -it forgejo-postgresql bash

We are inside the container now

1
2
3
4
5
# Change owner of file so postgres user can access it
chown postgres /tmp/forgejo-db.sql
# Run psql as postgres user
su postgres
psql

Create database and import data. Without replica role it fails on referencing not yet existing tables. Note we have to change database to forgejo and after changing replication role, we need to change user to forgejo as well, so this user owns imported tables. Oh, by the way, note ‘CHANGE_THIS_PASSWORD’ before pasting code snippet into terminal 😀.

1
2
3
4
5
6
7
8
9
create role forgejo with login password 'CHANGE_THIS_PASSWORD';
create database forgejo with owner forgejo template template0 encoding UTF8 LC_COLLATE 'en_US.UTF-8' LC_CTYPE 'en_US.UTF-8';
\c forgejo
SET session_replication_role = 'replica';
SET ROLE forgejo;
\i /tmp/forgejo-db.sql
SET ROLE postgres;
SET session_replication_role = 'origin';
\q

Note: you can change password with ALTER ROLE forgejo WITH PASSWORD 'new_password';. Ofc, when I was tired, I pasted CHANGE_THIS_PASSWORD when I was importing data for about the fifth time (don’t import tables to default/system database or as postgres user)

Remove SQL dump now:

1
rm /tmp/forgejo-db.sql

And exit container (Ctrl+D twice). Optionally backup database volume.

1
2
3
systemctl --user stop forgejo-postgresql.service 
podman volume export forgejo-postgresql-data| zstd -11 > ~/backup/forgejo-db-2026-09-02.tar.zst
systemctl --user start forgejo-postgresql.service  

3. Forgejo application

Podman quadlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[Unit]
Description=Forgejo
Requires=forgejo-postgresql.service
After=forgejo-postgresql.service

[Container]
Image=codeberg.org/forgejo/forgejo:15-rootless
ContainerName=forgejo
Network=forgejo-database
Network=shared_network
Volume=forgejo-data:/var/lib/gitea
Volume=/etc/localtime:/etc/localtime:ro
PublishPort=8082:3000
PublishPort=2222:2222
Environment=FORGEJO__database__DB_TYPE=postgres
Environment=FORGEJO__database__HOST=forgejo-postgresql:5432
Environment=FORGEJO__database__NAME=forgejo
Environment=FORGEJO__database__USER=forgejo
Environment=FORGEJO__server__ROOT_URL=https://git.pavelp.cz/
Secret=forgejo-postgres-password,type=env,target=FORGEJO__database__PASSWD
AutoUpdate=registry

[Service]
Restart=always

[Install]
WantedBy=default.target

NOTE: ROOT_URL can initially differ (e.g. git2.pavelp.cz) while old server is active and primary.

Create secret

Create podman secret, note CHANGE_THIS_PASSWORD - must match password in database.

1
printf '%s' 'CHANGE_THIS_PASSWORD' | podman secret create forgejo-postgres-password

Podman secrets can be revealed by these commands, which is useful for verification or data migration:

1
2
podman secret list
podman secret inspect --showsecret forgejo-postgres-password

Migrate data

This step migrates forgejo data such as git repositories.

1
2
3
4
5
6
7
8
9
10
11
# Create and link config above
cd .config/containers/systemd
ln -s ~/dotfiles/.config/containers/systemd/forgejo.container.ferret forgejo.container
systemctl --user daemon-reload
# Restore data (useful should be data/, repos/, app.ini)
podman volume create forgejo-data
cd backup
scp [email protected]:/home/pavel/backup/gitea-data-2026-09-02.tar.zst .
zstd -d < gitea-data-2026-09-02.tar.zst | podman volume import forgejo-data -
# Start service
systemctl --user start forgejo

Ofc, as usual this helps with diagnostics

1
2
journalctl --user -xeu forgejo-postgresql.service
journalctl --user -xeu forgejo.service

4. Recreate backup script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#!/usr/bin/sh
#
# Pavel Perina
#
# Changes:
# * 2023-07-04 Initial version (based on backup-nextcloud.sh)
# * 2023-07-17 Increase compression
# * 2026-06-18 Abandoned docker-compose for podman quadlets, changed texts Gitea->Forgejo
# * 2026-09-03 Database changed from MariaDB to PostgreSQL, tabs to spaces

#######################
# Setup variables
DATE=$(date +%Y-%m-%d)
TARGET=$HOME/backup

#################
# Backup Forgejo

backup_forgejo() {
  echo "🛑 Shutting down Forgejo ..."
  systemctl --user stop forgejo

  # Needs running database
  echo "💾 Backing up Forgejo SQL dump ..."
  podman exec forgejo-postgresql /usr/local/bin/pg_dump --username forgejo --format=plain forgejo \
         | zstd -11 > $TARGET/forgejo-psql-dump-$DATE.sql.zst

  echo "🛑 Shutting down PostgreSQL ..."
  systemctl --user stop forgejo-postgresql

  echo "💾 Backing up Forgejo data files..."
  podman volume export forgejo-data | zstd -11 > $TARGET/forgejo-data-$DATE.tar.zst

  echo "💾 Backing up Forgejo's database (be patient) ..."
  podman volume export forgejo-postgresql-data  | zstd -11 > $TARGET/forgejo-postgresql-data-$DATE.tar.zst

  echo "🟢 Bringing Forgejo up ..."
  systemctl --user start forgejo

  echo "Forgejo backup finished"
}

backup_forgejo

5. Go for a walk

This post is licensed under CC BY 4.0 by the author.