VPS Snaps

How to back up and restore Gitea or Forgejo

Stop Gitea, then run gitea dump as the user Gitea runs as. You get one archive with the repositories, a SQL dump of the database, app.ini and the data directory, LFS objects and attachments included. There is no restore command: you unpack the archive, put each part back, import the database, fix ownership and regenerate the Git hooks. Forgejo works the same way with forgejo dump, with a few differences listed below.

9 min readUpdated Checked against official documentation

What the dump contains

The commands here assume the layout from Gitea's binary install guide: the binary at /usr/local/bin/gitea, config at /etc/gitea/app.ini, work path /var/lib/gitea, a git system user and a systemd service called gitea. The archive has no top-level folder. It holds:

In the archiveWhat it is
app.iniThe config file passed with -c, including SECRET_KEY and the database password
gitea-db.sqlA SQL dump written by Gitea itself. Forgejo calls it forgejo-db.sql.
repos/Every repository under [repository] ROOT
data/APP_DATA_PATH: avatars, indexes, queues, the SQLite file if you use SQLite, and LFS objects, attachments and packages (read through Gitea's storage layer, so even from S3-compatible storage)
custom/Customizations, when the custom folder sits outside the data folder
log/Logs. Not needed for a restore.

Not in the archive: the OpenSSH host keys (the built-in SSH server's keys live in data/ and are included), the git user's ~/.ssh/authorized_keys, the systemd unit, your reverse proxy and TLS certificates, and the binary. Record gitea --version with the backup; you restore into the same version.

app.ini holds SECRET_KEY. Gitea's config reference says that if you lose it, data encrypted with it, such as two-factor secrets, can't be decrypted. Gitea writes the dump readable only by its owner; keep it encrypted wherever you store it (how).

Run gitea dump as the git user

Gitea's guide says to shut the instance down for a consistent backup: a push or migration mid-dump can leave repositories and database disagreeing. The dump command doesn't need the web server. Make a folder git can write to, then stop, dump and start:

Terminal
sudo install -d -o git -g git -m 700 /var/backups/gitea
Terminal
sudo systemctl stop gitea
Terminal
sudo -u git /usr/local/bin/gitea dump -c /etc/gitea/app.ini --type tar.gz --file /var/backups/gitea/gitea-dump-$(date +%F).tar.gz --skip-log
Terminal
sudo systemctl start gitea
  • -c points at app.ini. If it has no WORK_PATH line, add --work-path /var/lib/gitea.
  • --type: zip (the default), tar, tar.gz, tar.xz, tar.zst and a few more. Set it whenever you set --file; Gitea and Forgejo both take the format from --type, not the file name.
  • --file names the archive (- means standard output). The default, gitea-dump-<timestamp>.zip, lands in the current directory. Gitea won't overwrite an existing file.
  • --tempdir (default /tmp) holds the database dump while the archive is built.
  • --skip-log, --skip-index, --skip-attachment-data, --skip-package-data, --skip-lfs-data, --skip-custom-dir and --skip-repository leave parts out; --skip-repository skips LFS objects too.
  • --database writes the SQL for another database type; --quiet prints only warnings and errors.

Run it as root and Gitea stops with Gitea is not supposed to be run as root. Run it as another user, say ubuntu, and it stops with Expect user 'git' (RUN_USER in app.ini) but current user is: ubuntu.

Add a native database dump

Both projects warn about the SQL inside the dump. Gitea's guide says it has open issues that can cause problems on restore; Forgejo's calls them serious long-standing bugs and recommends a psql or MySQL dump as well. With PostgreSQL, take one while Gitea is stopped:

Terminal
sudo -u postgres pg_dump -Fc gitea > /var/backups/gitea/gitea-db-$(date +%F).dump

-Fc writes PostgreSQL's compressed custom format, restored with pg_restore. For MySQL use mysqldump --single-transaction gitea; see pg_dump and mysqldump. With SQLite, Forgejo's guide notes no separate dump is needed: the database file is in data/, consistent because Gitea was stopped. To copy SQLite while Gitea runs, use SQLite's backup API.

Forgejo differences

Forgejo keeps Gitea's layout and commands, and its container image uses the same /data layout, config path and git user. Its command-line reference and source show these differences:

GiteaForgejo
Commandgitea dumpforgejo dump
Default filegitea-dump-<timestamp>.zipforgejo-dump-<timestamp>.zip
SQL file insidegitea-db.sqlforgejo-db.sql
Skip the database--skip-dbNo such flag
Generated repository archivesAlways left outKept unless --skip-repo-archives
--database targetssqlite3, mysql, mssql, postgressqlite3, mysql, postgres
Regenerate Git hooksgitea admin regenerate hooksNo such command: admin regenerate only has keys

Docker installs

The official image keeps config and data in /data/gitea, repositories in /data/git/repositories and SSH host keys in /data/ssh, all in the /data volume; the database usually runs in its own container. Gitea's guide dumps inside the running container as git, from the temp folder to avoid a permission error:

Terminal
docker exec -u git -w /tmp gitea /usr/local/bin/gitea dump -c /data/gitea/conf/app.ini --type tar.gz --file /tmp/gitea-dump.tar.gz
Terminal
docker cp gitea:/tmp/gitea-dump.tar.gz /var/backups/gitea/gitea-dump-$(date +%F).tar.gz
Terminal
docker exec gitea rm /tmp/gitea-dump.tar.gz

gitea is the container name in Gitea's compose example; leave out the documented -it in cron, which has no terminal. This dump skips /data/ssh, so a server restored from it alone gets new SSH host keys.

Cleaner: stop the Gitea container, copy the whole volume, and take a native dump from the database container. With the compose file in /opt/gitea, the ./gitea:/data bind mount and a PostgreSQL service named db:

Terminal
cd /opt/gitea && docker compose stop server
Terminal
docker compose exec -T db pg_dump -U gitea -Fc gitea > /var/backups/gitea/gitea-db-$(date +%F).dump
Terminal
tar -czf /var/backups/gitea/gitea-data-$(date +%F).tar.gz -C /opt/gitea gitea
Terminal
docker compose start server

-T turns off the pseudo-terminal, which you need whenever you redirect output. More in Docker volume backups and database containers.

Large installs: back up repositories and LFS directly

A full dump packs every repository into a new archive each night. When that gets too slow or too big, dump the rest with --skip-repository (which also leaves out LFS objects) and back up the repository root and LFS folder with restic or Borg, which upload only what changed:

Terminal
sudo -u git /usr/local/bin/gitea dump -c /etc/gitea/app.ini --type tar.gz --file /var/backups/gitea/gitea-meta-$(date +%F).tar.gz --skip-repository --skip-log
Terminal
restic backup /var/lib/gitea/data/gitea-repositories /var/lib/gitea/data/lfs

Those are the default paths; check ROOT under [repository] and PATH under [lfs] in app.ini. Take both while Gitea is stopped, or from a filesystem snapshot, so database and repositories match; Forgejo's guide calls a synchronized point-in-time snapshot of all storage the reliable way.

Restore on a new server

Install the same Gitea version, layout and database type, but skip the web installer. Create an empty database and user matching app.ini's [database] section. For PostgreSQL:

Terminal
sudo -u postgres createuser --pwprompt gitea
Terminal
sudo -u postgres createdb --owner=gitea gitea

Unpack the archive into an empty folder (unzip <file> -d <folder> for a zip):

Terminal
sudo mkdir /root/gitea-restore && sudo tar -xzf gitea-dump-2026-10-03.tar.gz -C /root/gitea-restore

Check where the config keeps data and repositories. With no APP_DATA_PATH line, data lives in /var/lib/gitea/data; with no ROOT line, repositories live in its gitea-repositories folder:

Terminal
sudo grep -E '^(APP_DATA_PATH|ROOT) *=' /root/gitea-restore/app.ini

As root, from /root/gitea-restore, put each part back:

Terminal
install -o root -g git -m 640 app.ini /etc/gitea/app.ini
Terminal
cp -a data/. /var/lib/gitea/data/
Terminal
cp -a repos/. /var/lib/gitea/data/gitea-repositories/
Terminal
chown -R git:git /var/lib/gitea

Copy custom/ to /var/lib/gitea/custom if the archive has one, and move data/lfs if app.ini sets its own LFS PATH. Then import the database. From the native dump:

Terminal
pg_restore -h 127.0.0.1 -U gitea -d gitea --no-owner gitea-db-2026-10-03.dump

Or from the dump's own SQL: psql -h 127.0.0.1 -U gitea -d gitea -v ON_ERROR_STOP=1 -f gitea-db.sql for PostgreSQL, mysql --default-character-set=utf8mb4 -u gitea -p gitea < gitea-db.sql for MySQL. SQLite needs nothing: the file came back with data/.

Import the database before you start Gitea. Started against an empty database, Gitea creates its own tables and the import then collides with them.

Terminal
sudo systemctl start gitea

For Docker, start only the db container, restore into it with docker compose exec -T db pg_restore -U gitea -d gitea --no-owner < gitea-db.dump, unpack the volume copy into /opt/gitea, then start server. From a gitea dump, data/ goes to /data/gitea and repos/ to /data/git/repositories, owned by UID 1000.

After the restore: hooks, keys and doctor

With Gitea running, rewrite every repository's Git hooks. Gitea's guide says pushes fail if the install method or directory changed and hooks still point at old paths:

Terminal
sudo -u git /usr/local/bin/gitea -c /etc/gitea/app.ini admin regenerate hooks

If Git over SSH goes through the server's OpenSSH, rebuild authorized_keys from the database:

Terminal
sudo -u git /usr/local/bin/gitea -c /etc/gitea/app.ini admin regenerate keys

Then run every doctor check, and add --fix only after reading what it reports:

Terminal
sudo -u git /usr/local/bin/gitea -c /etc/gitea/app.ini doctor check --all

On Forgejo, skip the hooks command and run forgejo admin regenerate keys and forgejo doctor check --all. In Docker, prefix each with docker exec -u git gitea and use -c /data/gitea/conf/app.ini.

Test the restore

Restore the newest backup on a throwaway server every month. Before starting it, stop the copy emailing your users or syncing mirrors by adding this to its app.ini:

/etc/gitea/app.ini
[mailer]
ENABLED = false

[cron.update_mirrors]
ENABLED = false
  • Sign in as a user and as an admin; compare the repository list with the live server.
  • Clone a repository and run git fsck in the clone. In an LFS repository, git lfs pull fetches real files.
  • Open an issue attachment and download a release asset.
  • Time the whole restore: that is your real recovery time (RPO and RTO, testing restores).

Automate it

This script stops Gitea, dumps, starts it again even if the dump fails, copies the new files off the server with rclone and keeps seven days locally:

/usr/local/sbin/gitea-backup.sh
#!/usr/bin/env bash
set -euo pipefail

OUT=/var/backups/gitea
STAMP=$(date +%F-%H%M)

systemctl stop gitea
trap 'systemctl start gitea' EXIT

sudo -u git /usr/local/bin/gitea dump -c /etc/gitea/app.ini --quiet --skip-log \
  --type tar.gz --file "$OUT/gitea-dump-$STAMP.tar.gz"
sudo -u postgres pg_dump -Fc gitea > "$OUT/gitea-db-$STAMP.dump"

systemctl start gitea
trap - EXIT

rclone copy "$OUT" offsite:gitea-backups --include "*-$STAMP.*"
find "$OUT" -type f -mtime +7 -delete
/etc/cron.d/gitea-backup
30 3 * * * root /usr/local/sbin/gitea-backup.sh >> /var/log/gitea-backup.log 2>&1

Make the script executable with chmod 700. Gitea is down while the dump runs, so time a manual run and pick a quiet hour (cron schedules).

Frequently asked questions

Can I run gitea dump while Gitea is running?
It runs, but Gitea's backup guide says to shut the instance down for a consistent backup. A push or migration during the dump can leave the repositories and the database out of step.
Does gitea dump include LFS files?
Yes, when LFS is enabled. They go in data/lfs, even when stored in S3-compatible storage. --skip-lfs-data or --skip-repository leaves them out.
Is there a gitea restore command?
No. Gitea's documentation describes a manual restore: unpack the archive, move the files back, import the SQL, fix ownership and regenerate the Git hooks.
Why does gitea dump say it is not supposed to be run as root?
Gitea refuses to run as root. Run the dump as the user named in RUN_USER, usually git: sudo -u git gitea dump -c /etc/gitea/app.ini.

How this was checked

Commands, limits and prices were checked against these official pages, on October 4, 2026: