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.
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 archive | What it is |
|---|---|
app.ini | The config file passed with -c, including SECRET_KEY and the database password |
gitea-db.sql | A 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).
Running GitLab instead? See how to back up and restore self-managed GitLab.
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:
sudo install -d -o git -g git -m 700 /var/backups/giteasudo systemctl stop giteasudo -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-logsudo systemctl start gitea-cpoints at app.ini. If it has noWORK_PATHline, add--work-path /var/lib/gitea.--type:zip(the default),tar,tar.gz,tar.xz,tar.zstand a few more. Set it whenever you set--file; Gitea and Forgejo both take the format from--type, not the file name.--filenames 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-dirand--skip-repositoryleave parts out;--skip-repositoryskips LFS objects too.--databasewrites the SQL for another database type;--quietprints 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:
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:
| Gitea | Forgejo | |
|---|---|---|
| Command | gitea dump | forgejo dump |
| Default file | gitea-dump-<timestamp>.zip | forgejo-dump-<timestamp>.zip |
| SQL file inside | gitea-db.sql | forgejo-db.sql |
| Skip the database | --skip-db | No such flag |
| Generated repository archives | Always left out | Kept unless --skip-repo-archives |
--database targets | sqlite3, mysql, mssql, postgres | sqlite3, mysql, postgres |
| Regenerate Git hooks | gitea admin regenerate hooks | No 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:
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.gzdocker cp gitea:/tmp/gitea-dump.tar.gz /var/backups/gitea/gitea-dump-$(date +%F).tar.gzdocker exec gitea rm /tmp/gitea-dump.tar.gzgitea 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:
cd /opt/gitea && docker compose stop serverdocker compose exec -T db pg_dump -U gitea -Fc gitea > /var/backups/gitea/gitea-db-$(date +%F).dumptar -czf /var/backups/gitea/gitea-data-$(date +%F).tar.gz -C /opt/gitea giteadocker 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:
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-logrestic backup /var/lib/gitea/data/gitea-repositories /var/lib/gitea/data/lfsThose 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:
sudo -u postgres createuser --pwprompt giteasudo -u postgres createdb --owner=gitea giteaUnpack the archive into an empty folder (unzip <file> -d <folder> for a zip):
sudo mkdir /root/gitea-restore && sudo tar -xzf gitea-dump-2026-10-03.tar.gz -C /root/gitea-restoreCheck 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:
sudo grep -E '^(APP_DATA_PATH|ROOT) *=' /root/gitea-restore/app.iniAs root, from /root/gitea-restore, put each part back:
install -o root -g git -m 640 app.ini /etc/gitea/app.inicp -a data/. /var/lib/gitea/data/cp -a repos/. /var/lib/gitea/data/gitea-repositories/chown -R git:git /var/lib/giteaCopy 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:
pg_restore -h 127.0.0.1 -U gitea -d gitea --no-owner gitea-db-2026-10-03.dumpOr 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.
sudo systemctl start giteaFor 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:
sudo -u git /usr/local/bin/gitea -c /etc/gitea/app.ini admin regenerate hooksIf Git over SSH goes through the server's OpenSSH, rebuild authorized_keys from the database:
sudo -u git /usr/local/bin/gitea -c /etc/gitea/app.ini admin regenerate keysThen run every doctor check, and add --fix only after reading what it reports:
sudo -u git /usr/local/bin/gitea -c /etc/gitea/app.ini doctor check --allOn 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:
[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 fsckin the clone. In an LFS repository,git lfs pullfetches 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/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 -delete30 3 * * * root /usr/local/sbin/gitea-backup.sh >> /var/log/gitea-backup.log 2>&1Make 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:
- Gitea documentation: Backup and Restore
- Gitea documentation: Gitea Command Line
- Gitea documentation: Configuration Cheat Sheet
- Gitea documentation: Installation from binary
- Gitea documentation: Installation with Docker
- Gitea source v28.0.0: cmd/dump.go
- Gitea source v28.0.0: modules/setting/setting.go (run-user checks)
- Gitea source v28.0.0: Docker OpenSSH setup (/data/ssh)
- Forgejo documentation: Command-line interface
- Forgejo documentation: Upgrade guide (Backup)
- Forgejo documentation: Installation with Docker
- Forgejo source v16.0.5: cmd/dump.go
- PostgreSQL documentation: pg_restore
- PostgreSQL documentation: createuser
- PostgreSQL documentation: createdb
- Docker CLI reference: docker container exec
- Docker CLI reference: docker compose exec