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
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 2podman 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