How to Self-Host a Git Server with Gitea
Why this matters
GitHub is great until it isn’t — until you need a private repo and don’t want to pay, until a corporation controls your code, or until you’re offline and can’t push. A self-hosted Git server fixes all three.
Gitea is the lightweight choice: it looks and feels like GitHub, runs on a Raspberry Pi, and doesn’t demand a cluster.
What I tested on
- OS: Debian 13 (trixie)
- Gitea: 1.22.x
- Database: SQLite (built-in, no extra setup)
- Hardware: 2 GB RAM VPS / Raspberry Pi 5
- Duration: daily driver for 9 months
⚠️ Git over SSH needs port 22. If you run SSH on a non-standard port, configure
SSH_PORTin Gitea’s config.
Step 1: Install with Docker
services:
gitea:
image: gitea/gitea:1.22
container_name: gitea
restart: unless-stopped
environment:
- USER_UID=1000
- USER_GID=1000
- DB_TYPE=sqlite3
volumes:
- ./gitea:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "2222:22"
Port 2222 maps container SSH to a host port, so you don’t conflict with the host’s own SSH.
Step 2: First-run setup
Open http://your-server:3000, and Gitea walks you through:
- Database type (SQLite is fine for small setups)
- Site title and admin account
- SSH server port (set to 2222 to match above)
- Email settings — skip if you don’t need notifications
Create the admin account immediately; the first registered user becomes an admin by default, so register quickly before someone else does.
Step 3: Git over SSH
Test SSH access:
ssh -p 2222 git@your-server.com
If it says “Hi there, You’ve successfully authenticated,” it works. Add the port to your SSH config so you don’t type it every time:
# ~/.ssh/config
Host gitea
HostName your-server.com
User git
Port 2222
Step 4: Back it up
SQLite is just a file inside the /data volume. Stop Gitea, copy the volume, restart:
docker compose stop gitea
tar czf /backup/gitea-$(date +%F).tar.gz gitea/
docker compose start gitea
Step 5: CI with Gitea Actions
Gitea has a built-in CI system compatible with GitHub Actions syntax. Enable it in app.ini:
[actions]
ENABLED = true
Then add a workflow file in .gitea/workflows/ci.yaml. It’s not as rich as GitHub’s runner ecosystem, but it covers builds and tests.
Backups that actually restore
A git server holds two very different kinds of data: the repositories, which are just files, and the forge database, which holds issues, users, keys and webhooks. Copying repos/ alone gets you the code and loses everything that makes it a forge.
The supported path is a dump, and it produces one file you can move anywhere:
docker exec -u git gitea gitea dump -c /data/gitea/conf/app.ini -f /data/dump.zip
What matters after that is the same thing that matters for every backup on this site: restore it somewhere else and check that a clone works. I do this quarterly into a throwaway container, clone one repo over SSH, open one issue. Ten minutes, and it is the only test that counts — the general version of this habit is in the backup guide.
Two details that catch people:
- The dump is not incremental. It is a full archive, so a 20 GB instance produces a 20 GB file every run. Keep the last two and rotate, or accept the disk cost.
- Uploaded release assets and LFS objects may live outside the default dump scope depending on your paths. If you use either, back up those directories explicitly.
SSH access and the two things it breaks
Git over SSH is the part that works for a decade and then fails during an upgrade, usually because of the container port.
If Gitea runs in Docker, the container’s SSH port 22 is published as something else on the host — 2222 in the compose file above. The clone URL the UI shows includes that port, and it is correct. It breaks when:
Someone clones with a URL from another host. The same repository has one URL for HTTPS and one for SSH, and pasting a GitHub-style git@host:user/repo.git silently ignores the port and hangs on 22.
known_hosts keeps a stale key. Rebuilding the container regenerates host keys unless you persist /data/ssh. Then every clone warns about a changed fingerprint, which trains people to type yes without reading — a habit worth not teaching.
To make the port stick, either publish 2222:22 and let the UI advertise it (SSH_PORT=2222 plus the external domain setting), or move the host’s own SSH to a non-standard port and give 22 to the container. Pick one deliberately; half-configuring both is how you lock yourself out of the box that hosts your code.
If you are weighing this against the fork that shares the same codebase, the Gitea vs Forgejo comparison covers what actually differs.
Pros and cons
Pros:
- Familiar GitHub-like interface
- Runs on very modest hardware
- Full control over your code and issues
- Built-in CI, wiki, and pull requests
Cons:
- Smaller plugin ecosystem than GitHub
- You maintain it — updates, backups, uptime
- CI runners are less feature-complete than GitHub’s
Best for / Skip if
Best for: developers who want private repos without paying, or who care about data sovereignty.
Skip if: you only host open source. GitHub is free for public repos and has the better ecosystem.
Alternatives
- Forgejo — a community fork of Gitea, governance-focused
- GitLab CE — vastly more powerful, but heavy; needs 4+ GB RAM
- Plain Git + SSH — a bare repo and a few scripts; sometimes enough
Verdict
Gitea hits a sweet spot: enough features to replace GitHub for a small team, light enough to run on a Pi, and simple enough that maintenance is measured in minutes per month. If you’ve been thinking about self-hosting code, this is the easiest on-ramp.
References
- Gitea documentation — installation, configuration, and upgrade paths
- Backup and restore in Gitea — the dump command this guide uses
FAQ
Can I import from GitHub?
Yes, Gitea has a built-in GitHub importer for repos, issues, and pull requests.
Is Gitea secure?
Keep it updated, put it behind a reverse proxy with HTTPS, and don't expose the SSH port wider than needed.
How big is a Gitea install?
Around 100 MB idle, plus your repos. A Pi 4 handles it easily.
Can I use it for commercial projects?
Yes, the MIT license permits it.