From GitHub to Git Home: Self-Hosting Your Repositories with Gitea

Own your code, history and all

Contents

Git was designed to be distributed. Every clone is a full copy of the history, which means no single server is special and no company holds your project hostage. Yet somewhere along the way the world decided that “git” and “GitHub” were synonyms, and a vast amount of the world’s source code now lives on infrastructure owned by a single corporation. That is convenient right up until it is not. If you have ever wanted a home for your repositories that you fully control, that runs on a Raspberry Pi or a spare VPS, and that boots in milliseconds, Gitea is the answer. This guide gets you from nothing to a running, self-hosted git forge you can push to over SSH.

Why Self-Host Your Git

Advertisement

The most obvious reason is ownership. When your code lives on someone else’s platform, you live by their terms of service, their pricing changes, their outages, and their decisions about what is and is not acceptable. Self-hosting puts the repository, the issues, the pull requests, and the entire history on hardware you control. If you can reach the box, you can reach your code.

Privacy is the second reason. Private repositories on a hosted service are private only in the sense that the company promises not to look. For genuinely sensitive work, internal tooling, client projects under NDA, or simply a personal preference to keep your half-finished experiments off the public cloud, running your own forge removes a whole category of trust assumptions.

Then there is sheer practicality. A self-hosted forge that you run is also a forge that you understand, back up, and bend to your needs. There is no rate limiting on your own clones, no surprise feature deprecation, and no monthly bill scaling with the number of collaborators.

What Gitea Actually Is

Gitea is a lightweight, self-hosted git service written in Go. It offers the things you actually use from a hosted forge: a web interface for browsing code, pull requests, issue tracking, a wiki, organisations and teams, fine-grained access control, and increasingly a built-in CI system. It looks and feels familiar enough that anyone who has used GitHub will be productive within minutes.

Its defining trait is how little it asks of your server. Because it ships as a single self-contained binary with no heavy runtime, it runs happily on a Raspberry Pi or the smallest VPS your provider sells, consuming a fraction of the resources of a full GitLab installation. GitLab is a magnificent, all-encompassing DevOps platform, but it expects gigabytes of RAM and a small zoo of background services. Gitea does the core job in a sliver of that footprint, which is why it has become the default choice for home labs and small teams.

Installing with Docker Compose

Advertisement

The cleanest way to run Gitea is with Docker Compose alongside a proper database. SQLite works for a solo setup, but PostgreSQL scales better and is the sensible default if more than one person will use it. Create a directory and a docker-compose.yml:

 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
services:
  server:
    image: gitea/gitea:1.25
    container_name: gitea
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - GITEA__database__DB_TYPE=postgres
      - GITEA__database__HOST=db:5432
      - GITEA__database__NAME=gitea
      - GITEA__database__USER=gitea
      - GITEA__database__PASSWD=changeme
    restart: always
    volumes:
      - ./gitea:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "3000:3000"
      - "2222:22"
    depends_on:
      - db

  db:
    image: postgres:16
    container_name: gitea-db
    environment:
      - POSTGRES_USER=gitea
      - POSTGRES_PASSWORD=changeme
      - POSTGRES_DB=gitea
    restart: always
    volumes:
      - ./postgres:/var/lib/postgresql/data

Note the SSH port mapped to 2222 on the host so it does not collide with the server’s own sshd on port 22. Bring it up:

1
2
docker compose up -d
docker compose logs -f

Gitea is now listening on port 3000.

First-Run Setup

Open http://your-server:3000 in a browser and you are greeted by the installation page. Most defaults are already correct because the compose file injected the database settings through environment variables, but a few fields matter.

  1. Confirm the database connection details match your compose file.
  2. Set the Server Domain and the Gitea Base URL to the address users will actually type, for example git.example.com. Getting this wrong now means clone URLs come out malformed later.
  3. Set the SSH Server Port to 2222 to match the mapping above.
  4. Expand the optional settings and create your administrator account immediately. If you skip this, the first user to register becomes admin, which is rarely what you want on a public-facing instance.

Submit, and after a moment of initialisation you land on a fully working forge with your admin account ready.

Creating a Repository and Pushing

Click the plus icon, choose New Repository, give it a name, and decide whether it is private. Gitea hands you the clone URLs exactly as GitHub does. To push an existing local project over HTTPS:

1
2
3
cd my-project
git remote add origin https://git.example.com/yourname/my-project.git
git push -u origin main

For day-to-day work, SSH is smoother because it avoids typing credentials. Add your public key under Settings, SSH Keys, then remember that custom port:

1
2
3
git remote set-url origin \
  ssh://[email protected]:2222/yourname/my-project.git
git push

To avoid specifying the port every time, add a host entry to your ~/.ssh/config:

1
2
3
Host git.example.com
    Port 2222
    User git

Now plain git push over SSH just works.

Actions and Built-In CI

Recent Gitea versions include Gitea Actions, a CI system deliberately compatible with the GitHub Actions workflow syntax. That means many existing .github/workflows YAML files run with little or no modification. Actions are disabled by default and require a separate runner. Enable them in your configuration, then register a runner:

1
2
3
4
5
docker run -d --name gitea-runner \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -e GITEA_INSTANCE_URL=https://git.example.com \
  -e GITEA_RUNNER_REGISTRATION_TOKEN=<token> \
  gitea/act_runner:latest

Drop a workflow file into .gitea/workflows/ (or reuse a .github/workflows/ one) and pushes trigger builds on your own hardware, with no third-party minutes to budget.

Mirroring from GitHub

You do not have to choose abruptly between platforms. Gitea can mirror an external repository, pulling changes on a schedule so your instance always holds a current copy. When creating a repository, choose New Migration, paste a GitHub URL, and tick This repository will be a mirror. Gitea will clone it and keep it synchronised.

This is an excellent migration strategy and an even better backup strategy: keep developing on GitHub for visibility while your self-hosted Gitea quietly mirrors everything, so if the upstream ever disappears you lose nothing. The reverse, push mirroring, lets your Gitea instance push to an external remote, handy for keeping a public GitHub copy of a project you primarily develop at home.

Backing It Up

A self-hosted service is only as trustworthy as its backups. Gitea’s state lives in two places: the data volume holding repositories and configuration, and the database. The simplest robust approach is to stop the containers and archive both directories:

1
2
3
docker compose stop
tar czf gitea-backup-$(date +%F).tar.gz gitea postgres
docker compose start

For a hot backup that avoids downtime, use Gitea’s built-in dump command, which bundles repositories, the database, and configuration into one archive:

1
docker compose exec server gitea dump -c /data/gitea/conf/app.ini

Whichever route you take, copy the resulting archive off the machine. A backup that only exists on the server it is meant to protect is not a backup.

Troubleshooting the Common Snags

Gitea is forgiving, but a few setup mistakes account for the overwhelming majority of “why won’t it work” moments.

Clone URLs come out with the wrong host or port. This is the number-one issue, and it traces back to the Server Domain, Base URL, and SSH Server Port fields on the install page. If you set the base URL to http://your-server:3000 during setup and later put it behind a proxy at https://git.example.com, every clone URL Gitea hands out will be wrong. Fix it in gitea/conf/app.ini under the [server] section — set ROOT_URL, DOMAIN, and SSH_PORT to match reality, then restart the container.

SSH pushes fail with “permission denied (publickey)” or connect to the wrong sshd. Remember the port mapping: the container’s SSH listens on host port 2222, while the host’s own sshd owns 22. If you forget the ~/.ssh/config host entry, git quietly tries port 22 and hits the host’s SSH, which has never heard of your Gitea key. The symptom is a confusing auth failure that has nothing to do with Gitea. Set the port explicitly and the error vanishes.

The web UI is up but pushes over HTTPS hang behind a reverse proxy. Git over HTTP needs the proxy to allow larger request bodies and not buffer indefinitely. On nginx, set client_max_body_size generously (Git packs can be large) and disable request buffering for the Git endpoints. If your hardening setup is aggressive, double-check that the proxy is not stripping the Authorization header — that produces silent push failures that look like permission problems.

Actions runners register but jobs never start. The runner needs access to the Docker socket to spawn job containers, which is why the registration command mounts /var/run/docker.sock. If you omit that, the runner connects, shows as online, and then every job sits queued forever. Check docker logs gitea-runner for socket-permission errors.

If you are putting this on the public internet, harden the host around it the same way you would any exposed service — the principles in hardening a Linux server apply directly, and pairing the box with fail2ban and CrowdSec keeps the brute-force noise off your login page.

When GitHub Is Still the Right Call

Self-hosting is not a moral victory to be pursued at all costs. If your goal is maximum visibility for an open-source project, GitHub’s network effect is real: that is where contributors already are, where the ecosystem of integrations lives, and where a star count signals legitimacy. If you need bulletproof uptime without becoming your own sysadmin, a hosted platform absorbs that operational burden for you. And the moment you self-host, you own the patching, the backups, and the 3am pager when the disk fills. The pragmatic answer for most setups I have run is to use both: public projects on GitHub for reach, and private or personal work on Gitea for control, with mirroring keeping the two in sync. That hybrid is not a compromise; it is just using the right tool for each job.

Conclusion

Gitea gives you a real git forge that runs on hardware you control, sips resources, and feels instantly familiar. In one Docker Compose file you have repositories, issues, pull requests, CI, and mirroring, all owned outright. Push your first repo over SSH, set up a nightly backup, mirror your important GitHub projects as insurance, and you have brought your code home without giving up any of the conveniences that made hosted forges popular in the first place.

Is it worth it? If you push code most days, value privacy on your works-in-progress, and already keep a box running, the answer is an easy yes — the resource footprint is so small that Gitea will happily share a host with half a dozen other services, and the daily experience is genuinely indistinguishable from the hosted forges it replaces. If you maintain a single public open-source project chasing contributors and stars, self-hosting buys you very little and costs you reach; stay on GitHub. For everyone in between, the hybrid wins: develop privately on Gitea, mirror outward for visibility, and keep the one piece of infrastructure that holds your life’s work on a machine whose plug you can personally reach. That, in the end, is the whole point.

Advertisement
Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.