How to Install Gitea with Docker
Table of contents
- Key takeaways
- What is Gitea and what is it for?
- What does the docker-compose.yml for Gitea with PostgreSQL look like?
- Why does Gitea expose two ports (HTTP and SSH)?
- How do you do the first-run setup and the first user?
- How do Gitea Actions and webhooks work?
- Frequently asked questions
- Why should I not use the Gitea latest tag?
- Which database should I choose: SQLite, MySQL or PostgreSQL?
- How do I back up Gitea?
- Conclusion
- Sources
Gitea is a self-hosted Git service written in Go, lightweight and a single binary, an alternative to GitHub and GitLab. With Docker you install it from a docker-compose.yml alongside PostgreSQL: it publishes the web interface on port 3000 and Git over SSH on port 22, and is configured with GITEA__ variables. It ships Actions for CI and webhooks.
Gitea gives you your own GitHub at home: you host repositories, review code, manage issues and run CI, all on a server you control and with a tiny resource footprint. In this guide you install Gitea with Docker through a docker-compose.yml that pairs it with PostgreSQL, you understand why it exposes two ports (3000 for the web and 22 for Git over SSH), you complete the first-run setup by creating the first admin user, you close open registration and you enable Gitea Actions to get continuous integration without leaving your server. The same explanation is available in Spanish.
Key takeaways
- Gitea is a self-hosted, open-source (MIT licence) Git service written in Go; it started as a fork of Gogs in November 2016 and is today the go-to lightweight alternative to GitHub and GitLab for modest servers.
- It is a single binary with no dependencies: the official documentation defines it as "a painless, self-hosted, all-in-one software development service", and it runs comfortably in 1 GB of RAM, even on a Raspberry Pi 3.
- The most recent stable release is 1.27.0 (13 July 2026), but it carries a regression on code pages, so this guide pins 1.26.4 (21 June 2026), which also patches an important security flaw.
- It publishes the web interface on port 3000 and Git over SSH on container port 22; configuration is injected through environment variables prefixed with
GITEA__. - Gitea Actions, available since version 1.19, offers CI compatible with GitHub Actions syntax and runs on the Gitea Act Runner; webhooks trigger deployments and notifications to other services.
What is Gitea and what is it for?
Gitea is a Git hosting platform you can run on your own server: it plays the same role as GitHub or GitLab (repositories, branches, pull requests, issues, wiki, releases and a package registry), but the data and control are yours. It is written in Go and shipped as a single static binary with no external dependencies, which explains its most-cited trait: it barely uses any resources. Where GitLab asks for several gigabytes of RAM, Gitea runs comfortably in 1 GB and boots in a couple of seconds, so it fits on a home server, a small virtual machine or even a Raspberry Pi 3.
The project started in November 2016 as a community fork of Gogs, with the aim of speeding up development and opening it to more people. Almost all the code has been rewritten since then and it is now a mature project with frequent releases. The official documentation sums it up as "a painless, self-hosted, all-in-one software development service", and that word, painless, is what sets it apart from GitLab: fewer moving parts to maintain, less memory and an installation that fits in a single file with Docker.
In a homelab, Gitea fits as the heart of your workflow: you store your project code and your Docker Compose files there, trigger deployments with Actions or webhooks and, if you also run a private Docker image registry, you get a full code-to-container loop without relying on external services.
What does the docker-compose.yml for Gitea with PostgreSQL look like?
Gitea can use SQLite for testing, but for serious use a real database is better. This docker-compose.yml brings up two services: Gitea and a PostgreSQL with its persistent volume. Gitea waits for the database to be healthy before starting and all configuration is passed through environment variables prefixed with GITEA__section__KEY, which map to the sections of the app.ini file:
services:
gitea:
image: gitea/gitea:1.26.4
container_name: gitea
restart: unless-stopped
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: "change-this-db-password"
GITEA__server__DOMAIN: "git.mydomain.com"
GITEA__server__ROOT_URL: "https://git.mydomain.com/"
GITEA__server__SSH_PORT: "2222"
GITEA__service__DISABLE_REGISTRATION: "true"
GITEA__actions__ENABLED: "true"
volumes:
- gitea_data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "2222:22"
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:3000/api/healthz"]
interval: 30s
timeout: 5s
retries: 5
db:
image: postgres:17.5
container_name: gitea-db
restart: unless-stopped
environment:
POSTGRES_USER: "gitea"
POSTGRES_PASSWORD: "change-this-db-password"
POSTGRES_DB: "gitea"
volumes:
- gitea_db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U gitea"]
interval: 10s
timeout: 5s
retries: 5
volumes:
gitea_data:
gitea_db:
The key pieces are worth understanding. The image is pinned to gitea/gitea:1.26.4 rather than the moving :latest tag, so recreating the container does not jump your version without warning; on a service that holds all your code that is non-negotiable. USER_UID and USER_GID set to 1000 align the internal user with the usual file owner, although with a named volume Docker handles permissions for you. The GITEA__database__* variables point to PostgreSQL by the service name db, which resolves inside the Compose internal network. DISABLE_REGISTRATION: "true" closes public sign-up from the very first boot, an important setting if the server will be exposed. The depends_on block with condition: service_healthy delays Gitea’s start until pg_isready confirms the database accepts connections, and Gitea’s healthcheck queries its own /api/healthz endpoint. Gitea’s single /data volume holds the repositories, the app.ini configuration, the SSH keys and the session database. The password is repeated in both services: if you would rather not expose it in the file, move it to a .env file or a Docker secret.
Bring up the stack and follow the boot:
docker compose up -d
docker compose ps
docker compose logs -f gitea
If you need to review how PostgreSQL is prepared in a container (user, volume and backups with pg_dump), the dedicated PostgreSQL with Docker guide complements this one.
Why does Gitea expose two ports (HTTP and SSH)?
A Git server needs two paths, which is why the container publishes two ports. Port 3000 serves the web interface and the HTTP API: it is the one you open in the browser and the one you clone over https:// from. Port 22 is Gitea’s built-in SSH server, which handles git clone, git push and git pull over SSH using the public keys each user uploads to their profile. That SSH server lives inside the container and has nothing to do with the host system’s SSH.
Here is the practical catch: the host usually has its own sshd occupying port 22, so you cannot map 22:22 without clashing with it. The cleanest fix is to publish Gitea’s SSH on another host port, for example 2222:22, and tell Gitea about it with GITEA__server__SSH_PORT: "2222". That variable does not change the port the container listens on (it is still 22 inside), it only makes the clone URLs shown in the interface include the correct port, so that a git clone ssh://git@git.mydomain.com:2222/user/repo.git works first time. With this setup, your repositories are reachable over the web on 3000 and over SSH on 2222.
To avoid exposing port 3000 in the clear to the Internet, the usual approach is to put Gitea behind a reverse proxy such as Traefik that adds a domain and a Let’s Encrypt HTTPS certificate. Remember that the reverse proxy only routes the HTTP traffic on port 3000; the SSH port has to stay published directly on the machine, because SSH does not go through the HTTP proxy.
How do you do the first-run setup and the first user?
With the stack running, open http://YOUR-SERVER-IP:3000 in the browser. The first time, Gitea shows the installation screen with the configuration already filled in from the environment variables: the database type, host and credentials appear set, so you do not have to touch them. Scroll down to the optional settings, expand the administrator account section and create the first user there: that user will be the system administrator. This is the recommended way, because if you do not define an administrator during installation, the first user who registers becomes one by default, and that is exactly what you want to avoid on an accessible server.
Check that the domain and base URL (ROOT_URL) match how you will reach the service; if you are going to use HTTPS behind a proxy, they should point to https://your-domain/. Press the install button and Gitea writes the app.ini file into the /data volume, migrates the database schema and takes you to the sign-in screen. From that moment, log in with the admin user you created. Since you already set DISABLE_REGISTRATION: "true" in the Compose file, nobody else can create an account: you add new users yourself from the admin panel. To manage passwords and access tokens securely, a password manager like Vaultwarden is a good companion for this setup.
How do Gitea Actions and webhooks work?
Gitea Actions is the continuous-integration system built into Gitea since version 1.19. Its big advantage is that it uses a syntax almost identical to GitHub Actions: workflows are defined in YAML files inside .gitea/workflows/ of the repository, with the same on, jobs and steps, and you can even reuse many actions from the GitHub ecosystem. In the Compose file you already turned on the engine with GITEA__actions__ENABLED: "true", but that only enables the server side; you still need something to run the jobs.
That piece is Gitea’s Act Runner, a separate program, also written in Go, which is a fork of nektos/act and runs each job inside a container. You deploy it as another service, register it against your Gitea with a token you get in the admin area and, from there, every push matching a workflow triggers a run. It is the self-hosted equivalent of GitHub’s runners.
Webhooks cover the other side of automation: in each repository’s settings you define a URL that Gitea will send an HTTP request to when an event happens (a push, a new issue, a release). With that you trigger a deployment on your server, an image rebuild or a notification. The combination of Actions to build and webhooks to notify turns Gitea into the centre of a complete workflow without relying on the cloud.
Frequently asked questions
Why should I not use the Gitea latest tag?
Because gitea/gitea:latest always points to the newest published release, and recreating the container could move you up to a major version with schema or configuration changes without you deciding it. On a service that hosts all your code that is an unnecessary risk. Pin a specific tag such as gitea/gitea:1.26.4, which also includes the fix for CVE-2026-20896 (older images shipped REVERSE_PROXY_TRUSTED_PROXIES with a wildcard that allowed impersonating users), read the release notes and upgrade by recreating the container when it suits you.
Which database should I choose: SQLite, MySQL or PostgreSQL?
SQLite needs no extra container and is fine for testing or very light personal use, but it performs worse under concurrency and complicates hot backups. For any team use, PostgreSQL (or MariaDB) is the recommended option for its robustness and performance, and it is the one this guide uses. If you already have a database container, you can reuse it by creating a dedicated database and user for Gitea.
How do I back up Gitea?
You need two things: the /data volume, which contains the repositories, the app.ini and the keys, and a database dump with pg_dump. Gitea also ships the gitea dump command, which packs repositories, configuration and database into a single compressed file; run it with docker exec -u git gitea gitea dump -c /data/gitea/conf/app.ini and store the result off the server. Restoring is as simple as recovering the volume and reimporting the dump.
Conclusion
With a single docker-compose.yml you have your own self-hosted Git service: Gitea on PostgreSQL, with the web on port 3000, Git over SSH on 2222, open registration closed and Gitea Actions ready for CI. The full path is to bring up the stack, create the first admin user on the installation screen, put it behind a reverse proxy with HTTPS and add an Act Runner whenever you want to build. From there you control your code from end to end, with a resource footprint that fits even on a Raspberry Pi. To publish it with a domain and certificate, use Traefik; and to close the code-to-container loop, add a private Docker image registry.