How to Set Up a Git server using Gitea

16th Feb 2026 • Marco Cetica

In an age when your data is constantly being used to train LLMs, having a private and self-hosted alternative to host your personal repositories has become extremely important, especially if you use Git for more than just hosting open-source projects and you want to be 100% sure than your secret keys, sensitive documents and personal information are not used in the training process of some machine learning model. While migrating an entire infrastructure takes time, planning and multiple days (if not weeks) of effort, progressively moving your data off centralized platforms is an invaluable step towards data sovereignty and personal freedom.

In order to do this, we will deploy a self-hosted instance of Gitea: a private, fast, reliable DevOps platform (that's their current headline at the time of writing) using Docker, PostgreSQL and Gitea Act Runners to manage CI/CD pipelines. Furthermore, we will also try to customize the web interface aesthetic by changing the logo and the welcome screen on the homepage. If you want to see the final result, you can check out my personal Gitea instance to get an idea of what we'll accomplish.

Requirements

I've tested this tutorial on a VPS with 8GB of RAM and approximately 80GiB of storage; however, Gitea is know for being very lightweight. You can probably run it on any spare computer you've got at home or on that old Raspberry Pi buried on the bottom of a drawer.

Base Configuration

Let's start by creating a working directory to store the Docker configuration file and the data directories:

$ mkdir -p gitserver/

Inside it, let's create a new file called compose.yml with the following two services:

networks: gitea: external: false services: gitea_base: image: docker.gitea.com/gitea:latest container_name: gitea-base environment: USER_ID: 1000 USER_GID: 1000 GITEA__database_DB_TYPE: postgres GITEA__database_HOST: gitea_db:5432 GITEA__database_NAME: gitea GITEA__database_USER: gitea GITEA__database_PASSWD: qwerty1234 # <- Set this restart: always networks: - gitea volumes: - ./gitea:/data - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro ports: - "127.0.0.1:3000:3000/tcp" # <- Set this - "127.0.0.1:2222:22/tcp" # <- You may want to change this depends_on: gitea_db: condition: service_healthy gitea_db: image: postgres:latest container_name: gitea-db restart: always environment: POSTGRES_USER: gitea POSTGRES_PASSWORD: qwerty1234 # <- Set this POSTGRES_DB: gitea networks: - gitea volumes: - ./postgres:/var/lib/postgresql healthcheck: test: ["CMD", "pg_isready", "-U", "gitea", "-d", "gitea"] interval: 10s timeout: 5s retries: 5

Be sure to change the values that I've flagged with a comment. In particular, this configuration binds the following two ports on localhost:

Address Port Meaning
127.0.0.1 3000 Gitea listening port
127.0.0.1 2222 Builtin SSH server

As you can see, I've specified a different SSH port for the Gitea builtin SSH server in order to avoid conflicts with the host SSH server. Of course, if your host uses a different SSH port, you can safely bind port 22 on both the guest and the host without worrying about port conflicts. Besides this, port 3000 is used by Gitea to serve the web interface (again, you can change this as well).

Reverse Proxy

Before deploying the containers, we will configure a web server to act as a reverse proxy. Below, you can find a sample configuration for the most popular HTTP servers available right now (my favorite first):

Caddy

git.<YOUR_DOMAIN>.com { reverse_proxy 127.0.0.1:3000 }

NGINX

server { listen 443; server_name git.<YOUR_DOMAIN>.com; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }

Apache

<VirtualHost *:443> ServerName git.<YOUR_DOMAIN>.com SSLEngine on SSLCertificateFile /path_to_cert.pem SSLCertificateKeyFile /path_to_key.pem ProxyPreserveHost On RequestHeader set X-Forwarded-Proto "https" ProxyPass / http://127.0.0.1:3000/ ProxyPassReverse / http://127.0.0.1:3000/ </VirtualHost>

Deploy

Let's now deploy the infrastructure by issuing the following command:

$ docker compose up -d

You should see the following output:

container_setup.gif

After that, docker compose ps should yield the following status:

$ docker-compose ps NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS gitea-base docker.gitea.com/gitea:latest "/usr/bin/entrypoint…" gitea_base 17 seconds ago Up 6 seconds 127.0.0.1:3000->3000/tcp, 127.0.0.1:2222->22/tcp gitea-db postgres:latest "docker-entrypoint.s…" gitea_db 18 seconds ago Up 17 seconds (healthy) 5432/tcp

Additionally, you should have the following directories on your local path:

. ├── compose.yml ├── gitea │ ├── git │ ├── gitea │ │ ├── conf │ │ │ └── app.ini │ │ └── log │ └── ssh └── postgres └── 18 └── docker

The gitea/gitea directory will store the Gitea configuration files as well as the templates, the themes and the icons. We will come back there later to customize our server. The gitea/git path is where Gitea stores the actual bare repositories, while the postgres directory is where PostgreSQL saves its data.

Installation

Right now the infrastructure is up and running but we have yet have to install Gitea. To do this, open your web browser, go to https://git.<YOUR_DOMAIN>.com (or whatever you've chosen) and follow these instructions:

Database settings

db_conf.webp

Be sure to choose "PostgreSQL" as the database type and to set gitea_db (the name of the database container) as the host value. Finally, set the username and the password accordingly.

General settings

general_conf.webp

The only thing that you need to change from this section is the "Gitea Base URL" and the "Site Title". Be also sure to set up a admin account from the collapsed "Admin Account Settings".

After that, click the "Install Gitea" button and wait for the installer to complete, you should see the following loading screen:

progress.webp

Advanced Configuration

Let's now configure our brand new Git server from the web interface. Gitea provides a lot of settings, so we will only cover the most important ones.

SSH / GPG keys

In order to add your SSH and GPG keys, go to the top-right profile picture, click on "Settings" and then on "GPG Keys" menu from the left pane.

keys_conf.webp

Repository migration

To migrate repositories from another Git platform (such as GitHub, GitLab, etc.), go to the top-right "plus symbol", click on "New Repository", then "migrate repository". From there, you can easily transfer your existing repository following the interactive interface.

migration.webp

Gitea Runner

Gitea doesn't support CI/CD pipelines out of the box. To do this, you will need to set up an additional component called Gitea Runner. The good news is that it's extremely lightweight, simple to set up and can be integrated seamlessly with our existing Docker Compose set up. Let's see how.

Let's start by retrieving the "Registration Token". To do this, go to:

Site Administration->Actions->Runners

From there, click on "Create new Runner" and copy the token to your clipboard. After that, let's modify the compose.yml file by appending the following service at the bottom:

# Existing services ... gitea_runner: image: gitea/act_runner:latest container_name: gitea-runner restart: always environment: GITEA_INSTANCE_URL: https://git.<YOUR_DOMAIN>.com GITEA_RUNNER_REGISTRATION_TOKEN: "<YOUR_REGISTRAION_TOKEN>" GITEA_RUNNER_NAME: runner-alpha GITEA_RUNNER_LABELS: "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest" networks: - gitea volumes: - ./gitea-runner/data:/data - /var/run/docker.sock:/var/run/docker.sock depends_on: - gitea_base

Be sure to replace these values with the appropriate value:

In particular, the GITEA_RUNNER_LABELS should be formatted using the following syntax:

<LABEL_NAME>:docker://<OCI_REGISTRY_URL>:<CONTAINER_TAG>

In the given example, we have defined a label called ubuntu-latest which is mapped to a Docker image called runner-images:ubuntu-latest from the docker.gitea.com OCI registry. This will allow us to use the following setting inside workflow files:

name: my-build on: [push,pull_request,workflow_dispatch] jobs: my-build: runs-on: ubuntu-latest # ...

Let's now restart the whole infrastructure:

$ docker compose down $ docker compose up gitea-base | 2026/02/16 11:14:07 modules/graceful/server.go:50:NewServer() [I] Starting new Web server: tcp:0.0.0.0:3000 on PID: 16 gitea-runner | level=debug msg="Successfully pinged the Gitea instance server" gitea-base | 2026/02/16 11:14:08 HTTPRequest [I] router: completed POST /api/actions/runner.v1.RunnerService/Register for 172.20.0.4:47546, 200 OK in 14.2ms @ <autogenerated>:1(http.Handler.ServeHTTP-fm) gitea-runner | level=info msg="Runner registered successfully." gitea-runner | SUCCESS gitea-runner | time="2026-02-16T10:14:08Z" level=info msg="Starting runner daemon"

As you can see from the logs, the registration process was successful! If you now go back to the web interface, you should see a new Runner correctly registered and waiting for new jobs.

runner_online.webp

Now, every time you push a new commit on a repository configured for CI/CD, the Runner called runner-alpha should pick up the request and execute the steps of the jobs. You can try to go to one of my repository to see such setup in production.

Customization

Gitea provides an overlay system that allows you to customize pretty much every single aspect of the web interface, let it be the logo, the homepage, the header or the footer. To keep this guide small and bearable, we will only cover the logo and the homepage customization, but you can easily adapt the following instructions to change other details as well.

Let's start with the logo. To do this, you will need to provide standard logo image and a favicon icon, both in SVG format. To install them, create the following directories inside the gitea/ path and then copy there the SVG files mentioned before:

$ mkdir -p gitea/gitea/public/assets/img $ cp -R ~/{logo,favicon}.svg gitea/gitea/public/assets/img

Be sure to restart the infrastructure and to force-reload the page (CTRL+F5) to discard the browser cache. In order to customize the welcome screen, instead, create the following directories:

$ mkdir -p mkdir -p gitea/gitea/templates

Then, create a file there called home.tmpl with the following content:

{{template "base/head" .}} <div role="main" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home"}}{{end}}" class="page-content home"> <div class="tw-mb-8 tw-px-8"> <div class="center"> <div class="hero"> <img class="logo" width="64" height="64" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{ctx.Locale.Tr "logo"}}"> <h2 class="ui icon header title tw-text-balance">Git Server</h2> <p class="large tw-text-balance" style="max-width: 800px; margin: 0 auto; font-size: 1.5em;"> Welcome to my Git server! </p> </div> </div> </div> </div> {{template "base/footer" .}}

Restart the containers once again and you should get something like this:

custom_homepage.webp

Of course this is just an example and you can style it however you want.

Conclusions

You should now have a fully working Gitea server with CI/CD support ready to host your personal projects. Try to dive into the web interface to discover all the available settings.

In the meanwhile, I will try to keep this guide as updated as possible with the latest updates. Let me know if you spot any kind of issue. 🙃👋