Posted on

Self-hosting your code on Gitea

Afraid of GitHub suddenly enshittifying their product? The best way to shield yourself from that is to start self-hosting your code repositories. I have been running a self-hosted instance of Gitea for around two months, and so far the process has been pretty close to painless.

A cup of tea and a teapot
Photo by Content Pixie

Preparing a machine

To install Gitea, all you need is a server with 2 GB RAM and 2 CPU cores. The official documentation even says a single GB of RAM is enough, which I can believe, but a bit of margin will not hurt.

Once your server is started and configured as you like, you’ll need to prepare a database. Gitea supports both MySQL and PostgreSQL. Personally, I like MySQL a lot, so I decided to go with that.

Using docker-compose, you can launch a MySQL container like this:

  mysql:
    image: mysql:8.4
    restart: always
    volumes:
      - ./mysql_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: "SUPER_SECRET_PASSWORD"
    healthcheck:
      test: mysqladmin ping -pSUPER_SECRET_PASSWORD

It will write data to the mysql_data folder and use the SUPER_SECRET_PASSWORD password. You can then connect to this database by doing:

docker compose exec mysql mysql -uroot -pSUPER_SECRET_PASSWORD

From the database, you just have to create a gitea user on the gitea database:

CREATE USER 'gitea'@'%' IDENTIFIED BY 'TOP_SECRET_PASSWORD';

CREATE DATABASE gitea CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_bin';

GRANT ALL PRIVILEGES ON gitea.* TO 'gitea';
FLUSH PRIVILEGES;

Installing Gitea

Installing Gitea itself is very straightforward. It is a single process that you can launch through a single binary.

I use docker-compose to make networking easier, since I launched MySQL that way:

  gitea:
    image: docker.gitea.com/gitea:1.24.6-rootless
    restart: always
    volumes:
      - ./gitea-data:/var/lib/gitea
      - ./gitea-config:/etc/gitea
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "80:3000"
      - "2222:2222"
    depends_on:
      mysql:
        condition: service_healthy

It will write files and other unstructured data to gitea-data and read its configuration file from gitea-config. HTTP traffic will be received on the port 80 and forwarded to 3000, while SSH traffic (used by git itself) will use port 2222. We do not use port 22 to avoid conflicts with the server’s own SSH daemon.

The gitea installation wizard

As soon as the container is ready, you can head connect to Gitea with your web browser at your server’s IP/domain. It will open an installation wizard, and you just have to follow the instructions. Through this installation wizard, you should be able to connect to the previously configured MySQL server using the address mysql:3306 and the user you created earlier.

Setting up TLS

Just because we are self-hosting doesn’t mean we have to forgo basic security rules. Using Traefik is, in my opinion, the simplest way to set up TLS on your own server.

First we will need to launch a Traefik container:

  traefik:
    image: traefik:v3.5
    restart: always
    command:
      - "--providers.docker=true"
      - "--entrypoints.websecure.address=:443"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.web.http.redirections.entryPoint.to=websecure"
      - "--entrypoints.web.http.redirections.entryPoint.scheme=https"
      - "--entrypoints.web.http.redirections.entrypoint.permanent=true"
      - "--certificatesresolvers.myresolver.acme.tlschallenge=true"
      - "--certificatesresolvers.myresolver.acme.email=YOUR@EMAIL.COM"
      - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./letsencrypt:/letsencrypt
      - /var/run/docker.sock:/var/run/docker.sock
    depends_on:
      - gitea

This container will create two entry points:

  • websecure, that will receive the TLS-traffic on port 443
  • web, that will receive the unsecured traffic on port 80 and simply redirect to websecure

It will also use TLS challenges to prove you own the server to Let’s Encrypt and receive a valid certificate.

You will need to add labels on the gitea container to receive connections from the websecure entry point on port 3000:

  gitea:
    image: docker.gitea.com/gitea:1.24.6-rootless
    restart: always
    volumes:
      - ./gitea-data:/var/lib/gitea
      - ./gitea-config:/etc/gitea
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "2222:2222"
    labels:
      - "traefik.http.routers.gitea.rule=Host(`COOL.DOMAIN.COM`)"
      - "traefik.http.routers.gitea.entrypoints=websecure"
      - "traefik.http.routers.gitea.tls.certresolver=myresolver"
      - "traefik.http.services.gitea.loadbalancer.server.port=3000"
    depends_on:
      mysql:
        condition: service_healthy

Lastly, if you didn’t do it during installation, you should indicate the proper domain and URL in Gitea’s config. It is found in the gitea-config/app.ini file.

[server]
SSH_DOMAIN = COOL.DOMAIN.COM
ROOT_URL = https://COOL.DOMAIN.COM/
DOMAIN = COOL.DOMAIN.COM

After restarting everything, crossing fingers and waiting a bit, you should receive a valid TLS certificate and be able to use Gitea safely.

Importing repositories from GitHub

You can automatically import repositories from GitHub into Gitea. You can find the import tool on the + button located in the top right corner of your screen.

In addition to importing the code, Gitea can automatically retrieve existing labels, issues, pull requests, releases, and milestones using a Personal Access Token.

The Gitea import menu

Compared to alternatives like GitLab, the killer feature of Gitea is that you do not need to rewrite your GitHub Action workflows. So you can just leave your YAML files untouched in .github/workflows.

You will need to start some Runner processes to run your Actions.

First, you’ll have to generate a Runner registration token from your instance, organization or repository setting. As you may have guessed, a Runner with a registration token created from a repository will only be able to access this repository, while a runner created from the instance settings will be able to access every repository.

The runners settings page

When you have a token, you can launch a runner container like this:

  gitea-runner:
    image: docker.io/gitea/act_runner:0.2.13
    environment:
      CONFIG_FILE: /config.yaml
      GITEA_INSTANCE_URL: "https://COOL.DOMAIN.COM"
      GITEA_RUNNER_REGISTRATION_TOKEN: "TOKEN_YOU_JUST_GENERATED"
      GITEA_RUNNER_NAME: "actions-runner"
    volumes:
      - ./runner-config.yaml:/config.yaml
      - ./runner-data:/data
      - /var/run/docker.sock:/var/run/docker.sock

For scalability and security reasons, this can, and probably should, be done from a second machine.

Backing up Gitea

While I’ve not encountered any issues operating Gitea so far, it is important to do regular backups to avoid losing work in case something goes wrong.

Gitea provides a built-in backup utility called gitea dump. You can invoke it using Docker like that:

docker exec -u git -it -w --tempdir ./temp $(docker ps -qf 'name=gitea-1$') bash -c '/usr/local/bin/gitea dump -c /etc/gitea/app.ini'

Alternatively, you should be able to back up the MySQL database using mysqldump and make zip of Gitea local volumes.

Backups have to be copied to a service you trust, and that is independent of where your server is hosted. Every major cloud provider offers reasonably cheap object storage that can be used for this.