Backup and Restore Gitea server

Backup and restore Gitea, the right way

Page content

Gitea backups touch three moving parts - db, git repos, and app files - and since 2024 the built-in gitea dump command handles all of them in one go.

very nice photo of opened hdd

In the Testing Gitea post we installed the gitea server. This is the follow-up: how to actually back it up and, more importantly, restore it when something goes wrong.

For the complete developer tools collection including Git workflows and Docker management, see Developer Tools: The Complete Guide to Modern Development Workflows.

If you’re setting up Gitea for the first time, check out Choosing free on-prem git server - Gitea is the winner! for installation details, and Gitea SSL with Apache as reverse proxy for secure deployment.

When to rehearse this

Now, just as a precaution against terrible things happening, is a good time to rehearse the backup and restore procedure - before you actually need it, not after a disk dies.

Better be safe than sorry.

Where the data lives

A Gitea instance is really three components that all need to stay in sync:

  • code (git repositories)
  • filestore (attachments, avatars, LFS objects, indexers)
  • db (users, issues, PRs, settings)

In our test environment all together they take a bit more than 700MB:

gitea disk usage

Because these three pieces reference each other, Gitea’s own docs are explicit about backup consistency: the instance should be stopped for the duration of the backup, otherwise a repo copied mid-migration can end up out of sync with what the database thinks happened. Restore, for the same reason, needs to happen as one transaction too.

The easy way - gitea dump

This is the part that changed since the previous version of this article: instead of juggling pg_dump, tar, and separate folders by hand, Gitea ships a single dump command that bundles the database, repositories, LFS data, attachments, and config into one archive.

gitea dump -c /path/to/app.ini

That produces a timestamped file like gitea-dump-1610949662.zip containing:

  • app.ini - copy of the config file
  • custom/ - customizations under custom/
  • data/ - attachments, avatars, LFS objects, indexers (and the SQLite file, if that’s your db)
  • repos/ - full copy of the repository directory
  • gitea-db.sql - SQL dump of the database
  • log/ - logs (not needed for restore)

Useful flags worth knowing about:

  • --file name / -f name - output file name (- for stdout, handy for piping straight to scp or object storage)
  • --type - output format: zip (default), tar, tar.gz, tar.xz, tar.zst, etc.
  • --database / -d - force the SQL dialect in gitea-db.sql (sqlite3, mysql, mssql, postgres) - useful when migrating between database engines
  • --skip-repository / -R, --skip-lfs-data, --skip-attachment-data, --skip-package-data, --skip-log, --skip-db - trim the dump down to just what you need
  • --tempdir / -t - where the intermediate files are staged (default /tmp or $TMPDIR) - make sure this has enough free space for the whole instance

Running gitea dump in Docker

Since most self-hosted setups run Gitea via docker-compose, the command needs to run inside the container, as the git user, from the container’s temp dir:

cd ~/gitea-srv-local

# create a dump inside the running container
sudo docker exec -u git -it -w /tmp gitea bash -c \
  '/usr/local/bin/gitea dump -c /data/gitea/conf/app.ini'

# copy the resulting zip out of the container
sudo docker cp gitea:/tmp/gitea-dump-*.zip ./gitea-backups/

-w /tmp matters: dump needs to write and zip its temp working directory, and running it anywhere without write permissions will fail with a permission error.

Then, as before, get the archive off the box:

scp uname@gitea-srv-ip-addr:~/gitea-srv-local/gitea-backups/gitea-dump-*.zip ~/gitea-backups/

If you’d rather dump the database with native tools (Gitea’s own docs recommend this for MySQL/PostgreSQL, since the XORM-based SQL dump inside gitea dump has known edge cases on restore), you can still do it side by side:

sudo docker exec -t gitea-srv-local_db_1 bash -c \
  'pg_dump gitea -U gitea --file=/var/lib/postgresql/backups/gitea-db-$(date +%Y-%m-%d).sql'

See the PostgreSQL Cheatsheet for more pg_dump/pg_restore options, and the Docker Compose Cheatsheet if any of the docker exec/docker cp syntax above looks unfamiliar.

How - Restore

There’s still no one-command restore - Gitea’s docs are upfront that this remains a manual process of moving files back into place and restoring the database dump. But! Always check the original doco first, since paths and container layouts change between releases.

# install/start a fresh gitea (same version as the backup) then take it down
sudo docker-compose down

# unzip the dump
unzip gitea-dump-1610949662.zip -d gitea-restore
cd gitea-restore

# restore repos and data into the volumes gitea uses
sudo cp -r repos/* ../gitea/git/
sudo cp -r data/* ../gitea/gitea/
sudo chown -R 1000:1000 ../gitea/git ../gitea/gitea

# bring gitea back up
sudo docker-compose up -d

# restore the database
sudo docker exec -i gitea-srv-local_db_1 psql -U gitea gitea < gitea-db.sql

If you dumped the db separately with pg_dump/pg_restore instead, restore that dump the same way you would for any other Postgres instance - see the PostgreSQL Cheatsheet for the exact commands.

Regenerate hooks after restore

If you restored to a different installation method (binary vs Docker) or a different path, the git hooks baked into each repository will still point at the old paths. Fix that with:

sudo docker exec -u git -it gitea bash -c '/usr/local/bin/gitea admin regenerate hooks'

Skipping this step is the classic cause of push failing right after a restore with no obvious error in the UI.

Verify the restore

Before calling it done:

  1. Log in to the UI and confirm users, issues, and settings look right.
  2. Clone one repo and push a trivial commit to confirm hooks work.
  3. Run gitea doctor check (add --fix if it flags anything) to catch path or permission mismatches early.

Restoring a single repository - restore-repo

The other genuinely new piece since the last version of this article is a pair of commands for repository-level backup and restore, separate from the full-instance dump/restore workflow above. They’re handy when you only need to recover one project - say, someone force-deleted a repo - instead of rolling back the whole server.

dump-repo pulls a single repository (plus, optionally, its issues, PRs, wiki, and other metadata) out to a local directory:

gitea dump-repo \
  --git_service gitea \
  --clone_addr https://gitea.example.com/owner/repo.git \
  --auth_token <token> \
  --repo_dir /backup/repos/owner/repo \
  --units wiki,issues,labels,releases,milestones,pull_requests,comments

restore-repo then replays that directory back into an instance, into a chosen owner/repo:

gitea restore-repo \
  --repo_dir /backup/repos/owner/repo \
  --owner_name owner \
  --repo_name repo \
  --units issues,labels,milestones,pull_requests,comments

Both --units lists are optional - omit them and everything supported gets migrated. This pair is really a migration tool at heart (it also works against GitHub/GitLab as the source via --git_service), but it doubles nicely as a lightweight, per-repo alternative to a full gitea dump when a full restore would be overkill.

Automating it

A daily dump plus off-box copy is a two-line cron job once the manual steps above work:

# /etc/cron.d/gitea-backup
0 2 * * * root docker exec -u git -w /tmp gitea gitea dump -c /data/gitea/conf/app.ini -f - > /home/uname/gitea-backups/gitea-dump-$(date +\%F).zip 2>> /var/log/gitea-backup.log

Pair it with a retention cleanup (find ~/gitea-backups -mtime +14 -delete) and an off-site copy via scp/rsync so a single-host failure doesn’t take the backups with it.

Troubleshooting

  • permission denied during dump - you’re not running as the git user, or --tempdir/-w points at a directory the container user can’t write to.
  • Restore succeeds but git push fails - hooks weren’t regenerated; run gitea admin regenerate hooks.
  • Database restore errors on a big instance - prefer native pg_dump/mysqldump over the SQL embedded in gitea dump’s zip; it’s known to have rough edges on restore for large or unusual schemas.
  • Restored to a new major version - run gitea doctor check --all --fix and check the release notes for that version’s migration steps before assuming the restore is complete.

For quick reference on Git commands, see GIT Cheatsheet: Most useful GIT commands.

Subscribe

Get new posts on AI systems, Infrastructure, and AI engineering.