# running my own git server with forgejo

> Forgejo 17 in Docker on a home server, reached over Tailscale: one compose file, git over SSH on port 2222, pull mirrors of GitHub repos, and backups that don't rely on forgejo dump.

By Harshal Ranjhani · [HTML version](https://harshalranjhani.in/blog/running-my-own-git-server-with-forgejo)

This post is about running [Forgejo](https://forgejo.org), a self-hosted git forge, on a home server. I wanted my repositories on a machine I control, with a web UI, and with copies of my GitHub repositories that keep themselves up to date.

The setup is one Docker Compose file. Forgejo stores everything in one folder, uses SQLite, and is only reachable over [Tailscale](https://harshalranjhani.in/blog/ssh-home-server-with-tailscale), so nothing is forwarded on the router. The guide covers the compose file, the first-run page, SSH access on port 2222, mirroring from GitHub, backups, upgrades, and what this setup doesn't do.

![Diagram of the setup. A laptop reaches the home server over the tailnet, using SSH on home-server port 2222 and the web UI on port 3000. On the server, Docker Compose runs the Forgejo 17.0.1 container with host port 3000 mapped to 3000 and host port 2222 mapped to the container's OpenSSH on 22. The ./forgejo folder is mounted at /data and holds gitea/conf/app.ini, gitea/gitea.db, git/repositories and the ssh host keys. Below it, GitHub feeds the server through a pull mirror over HTTPS every 8 hours by default.](https://assets.harshalranjhani.in/images/blog/running-my-own-git-server-with-forgejo/forgejo-home-server-layout.png)

## why forgejo, and which version

Forgejo is a community fork of Gitea. It gives you repositories, issues, pull requests, a package registry and a CI system called Forgejo Actions, in a single Go binary or container.

There are two release lines right now, listed on the [releases page](https://forgejo.org/releases/):

- **17.0.1** is the current stable release, from 26 October 2026. It is supported until 28 January 2027.
- **15.0.11** is the long term support release. It is supported until 15 July 2027.

[Forgejo 17.0](https://forgejo.org/2026-10-release-v17-0/) came out on 15 October 2026. Major versions ship every three months, and a stable version gets about three months of fixes. I'm using 17.0.1 in this guide. If you'd rather upgrade across major versions less often, the same steps work with `15.0.11`.

## how it works

The official image is `codeberg.org/forgejo/forgejo`. Inside the container there are two things that matter here:

- The Forgejo web server on port 3000.
- An OpenSSH server on port 22, which handles `git clone` and `git push` over SSH.

All state lives under `/data` in the container. With the default SQLite setup that means the config file at `/data/gitea/conf/app.ini`, the database at `/data/gitea/gitea.db`, the bare repositories under `/data/git/repositories`, and the container's SSH host keys under `/data/ssh`. I mount a local folder there, so the whole instance is one directory on the host.

My laptop and the server are both on my tailnet. The laptop reaches Forgejo with the server's MagicDNS name, `home-server`, the same name I use for regular SSH.

## what you need

- A Linux server with Docker and the Docker Compose plugin
- Tailscale on the server and on the device you'll push from
- A GitHub fine-grained token, only if you want to mirror private repositories

## step 1: write the compose file

Make a folder for the instance:

```bash
mkdir -p ~/forgejo
cd ~/forgejo
```

Then save this as `docker-compose.yml`. It's the example from the [Forgejo Docker installation docs](https://forgejo.org/docs/latest/admin/installation/docker/), with the version pinned and a few settings added through environment variables:

```yaml
networks:
  forgejo:
    external: false

services:
  server:
    image: codeberg.org/forgejo/forgejo:17.0.1
    container_name: forgejo
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - FORGEJO__server__DOMAIN=home-server
      - FORGEJO__server__SSH_DOMAIN=home-server
      - FORGEJO__server__ROOT_URL=http://home-server:3000/
      - FORGEJO__server__SSH_PORT=2222
      - FORGEJO__service__DISABLE_REGISTRATION=true
    restart: always
    networks:
      - forgejo
    volumes:
      - ./forgejo:/data
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "3000:3000"
      - "2222:22"
```

Here's what the additions do:

- `USER_UID` and `USER_GID` are the user that owns the files in `./forgejo`. Use the output of `id -u` and `id -g` for your own user. The docs warn that the container may not start if the volume is owned by someone else.
- Any variable named `FORGEJO__section__KEY` is written into `app.ini`, so `FORGEJO__server__SSH_PORT=2222` is the same as `SSH_PORT = 2222` under `[server]`.
- `DOMAIN`, `SSH_DOMAIN` and `ROOT_URL` are the address Forgejo puts in clone URLs and links. Replace `home-server` with your server's tailnet name.
- `SSH_PORT` only changes the port shown in clone URLs. OpenSSH inside the container still listens on 22, and the `ports` section maps host port 2222 to it. The docs example uses 222. I picked 2222 because the host's own SSH server already has 22.
- `DISABLE_REGISTRATION=true` turns off the sign-up page. You'll create the only account in step 2.

The image tag is pinned to `17.0.1`. The docs also publish a `17` tag that follows the latest 17.x release. I prefer to change the version myself so an upgrade only happens when I run one.

## step 2: start it and finish the setup page

Start the container:

```bash
docker compose up -d
docker compose ps
```

`docker compose ps` should list the `forgejo` container as running. On the first start the container writes `app.ini` and generates its SSH host keys inside `./forgejo`.

Open `http://home-server:3000` from a device on your tailnet. You'll get the **Initial configuration** page.

1. Under **Database settings**, keep SQLite3.
2. Under **General settings**, check that **Server domain**, **SSH server port** and **Base URL** match the compose file.
3. Open **Optional settings**, then **Administrator account settings**, and create your account. Registration is off, so this is how the first account gets made.
4. Click **Install Forgejo**.

The [recommended settings page](https://forgejo.org/docs/latest/admin/setup/recommendations/) suggests SQLite for an instance with low to moderate activity. One user pushing to their own repositories is well within that.

## step 3: push over ssh

In Forgejo, open **Settings**, then **SSH / GPG keys**, and add your public key. Then add an alias to `~/.ssh/config` on your laptop, so you don't have to type the port:

```text
Host forgejo
    HostName home-server
    Port 2222
    User git
```

Every user connects as `git`. Forgejo works out who you are from the key. Test the connection:

```bash
ssh -T forgejo
```

It should reply with a line that starts with `Hi there` and your username, and ends by saying Forgejo does not provide shell access. That means the key was accepted.

Create an empty repository in the web UI, then add it as a remote to an existing project and push. `your-repo` is a placeholder:

```bash
git remote add forgejo forgejo:harshal/your-repo.git
git push forgejo main
```

The `forgejo:` prefix is the SSH alias, so git connects to `home-server` on port 2222 as `git`.

## step 4: mirror repositories from github

A pull mirror is a repository that Forgejo keeps in sync with a remote. I use it to keep a copy of GitHub repositories on the server.

From the [repository mirror docs](https://forgejo.org/docs/latest/user/repo-mirror/):

1. Click the **+** menu at the top right and pick **New migration**.
2. Choose **GitHub** and paste the repository URL.
3. For a private repository, add an access token. A [fine-grained token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) limited to those repositories with read-only **Contents** access is enough. Public repositories don't need one.
4. Tick **This repository will be a mirror**.
5. Click **Migrate repository**.

![Screenshot from a throwaway test instance running Forgejo 17.0.1, not my home server. The repository page for harshal/orpeek, a pull mirror of a public GitHub repository, with the line "mirror of https://github.com/harshalranjhani/orpeek.git synced 1 minute ago" under the title, and an HTTP clone URL on home-server port 3000.](https://assets.harshalranjhani.in/images/blog/running-my-own-git-server-with-forgejo/forgejo-pull-mirror-test-instance.png)

The screenshot above is from a throwaway test instance I ran to check these steps, mirroring one of my public repositories.

A few things about pull mirrors:

- They sync every 8 hours by default. That's `DEFAULT_INTERVAL` in the `[mirror]` section of the [config cheat sheet](https://forgejo.org/docs/latest/admin/config-cheat-sheet/), and you can set it per repository, down to `MIN_INTERVAL`, which defaults to 10 minutes. **Synchronize now** in the repository settings forces a sync.
- They copy git data: branches, tags and commits. When the mirror box is ticked, the GitHub importer only lets you add the wiki. Issues and pull requests stay on GitHub.
- You can't turn an existing repository into a pull mirror. It has to be created as one.

The opposite direction also works. A push mirror sends a Forgejo repository to GitHub after each push or on a timer, which is useful for a project that lives on Forgejo but should still show up on GitHub. It's set up under **Settings**, **Repository**, **Mirror settings**. The docs warn that it force pushes, so anything committed directly on the GitHub side gets overwritten.

## step 5: back it up

Forgejo has a `forgejo dump` command, and plenty of guides use it for backups. Don't. In October 2026 a Forgejo maintainer [wrote on the issue tracker](https://codeberg.org/forgejo/forgejo/issues/14706) that the command was only meant for sending reproductions to maintainers, that restoring from it isn't supported, and that the plan is to deprecate it.

The [backup docs](https://forgejo.org/docs/latest/admin/setup/backup/) recommend shutting Forgejo down while you back it up. With SQLite and the default config, the `data` folder covers the database, repositories, storage, queues and indexes. In this setup that's the `./forgejo` folder, so the backup is: stop, archive, start.

```bash
cd ~/forgejo
docker compose stop server
sudo tar --numeric-owner -czf ~/forgejo-backup-$(date +%F).tar.gz docker-compose.yml forgejo
docker compose start server
```

`sudo` is there because the SSH host keys are owned by root inside the volume. `--numeric-owner` keeps the user and group IDs as numbers, so ownership comes back correctly on a machine with different user names.

The archive includes `app.ini`. That file holds `SECRET_KEY`, which encrypts things like mirror credentials and two-factor settings in the database. The docs say a restore without it breaks those. So the archive is both necessary and sensitive. Keep it encrypted, and keep a copy somewhere other than the server. The docs mention `restic` as one tool for that.

To restore, stop the container, move the old folder aside, and unpack the archive in its place:

```bash
cd ~/forgejo
docker compose down
sudo mv forgejo forgejo.old
sudo tar --numeric-owner -xzf ~/forgejo-backup-YYYY-MM-DD.tar.gz
docker compose up -d
docker compose exec -u git server forgejo doctor check --all
```

`doctor check --all` is the check the [upgrade guide](https://forgejo.org/docs/latest/admin/upgrade/) uses to confirm an instance is healthy. It should finish without reporting problems.

## upgrading

For a patch release, take a backup, change the tag in `docker-compose.yml`, and pull:

```bash
docker compose pull
docker compose up -d
```

Forgejo runs any database migrations on startup. A major upgrade, like 17 to 18, follows the same steps with two more from the upgrade guide: read the breaking changes in the release notes first, and run `docker compose exec -u git server forgejo manager flush-queues` before stopping the old version. There's no going back afterwards. Forgejo refuses to start an older version on a database a newer one has migrated, so the backup is the rollback.

## optional: forgejo actions

Forgejo Actions is enabled by default and reads workflows from `.forgejo/workflows`. Forgejo doesn't run the jobs itself. That's the job of [Forgejo Runner](https://code.forgejo.org/forgejo/runner/releases), a separate program, currently at v13.2.1. The 17.0 release shows job summaries from runners on 13.2 or newer.

A runner executes whatever code a workflow contains, so read the [Actions security page](https://forgejo.org/docs/latest/admin/actions/security/) before you install one. I've left the runner out of this guide.

## what it misses

**It only works on the tailnet.** Nobody outside my Tailscale network can open the web UI or clone anything. For a private git server that's the point, but it also means no outside contributors and no public project pages.

**The web UI is plain HTTP.** WireGuard encrypts the traffic between my devices, but the browser still marks the page as not secure. [Tailscale Serve](https://tailscale.com/kb/1312/serve) can put HTTPS with a tailnet certificate in front of port 3000, and then `ROOT_URL` should change to match.

**Clone URLs carry a port.** Sharing the host's port 22 with the container is possible, but it takes extra SSH setup on the host. I kept 2222 because it's simpler.

**Mirrors are git only, and they lag.** Issues and pull requests stay on GitHub, and a pull mirror can be up to one interval behind.

**A backup means a few moments of downtime.** The container is stopped while `tar` runs. For one person that hardly matters, but it's still a gap.

**SQLite is hard to leave.** The same maintainer comment says moving an instance to another database isn't officially supported yet. If more people might use the instance later, start with PostgreSQL. The Docker docs have a compose example for it.

**One server is one disk.** If the backups sit on the same machine as Forgejo, they go when that disk goes.

## next step

Before pushing anything that only exists on this server, run the restore steps once into a scratch folder or on another machine, sign in, and clone a repository from the restored copy. That tells you the backup works before you need it.

Last updated on November 3, 2026
