Use case · Git hosting

Gitea self-hosted on a dedicated server: sizing, Compose, SSH and Actions

Self-hosting Gitea takes very little hardware: the project's documentation says 2 CPU cores and 1 GB of RAM is typically sufficient for small teams, and a Raspberry Pi 3 handles small workloads. What decides the machine is everything around it: the database, repository storage, and the Gitea Actions runners that build your code, which the docs advise keeping off the Gitea host. This page sizes those parts, sets Gitea up with Docker Compose and PostgreSQL, keeps the installer private until an admin exists, and adds a runner.

See the machines

What Gitea needs from a machine

Gitea is one Go binary with a web UI, an SSH endpoint and a database behind it. Its system requirements are short: 2 CPU cores and 1 GB of RAM for small teams, Git 2.0.0 or newer, and a dedicated non-root system account on Unix-like systems. The official Docker image takes care of that last point by running Gitea as the git user inside the container.

ComponentWhat the docs sayWhat drives the size
Gitea server2 cores, 1 GB RAM typical for small teamsUsers, open pull requests, indexers
DatabasePostgreSQL 12+, MySQL 8.0+, MariaDB 10.4+, MSSQL 2012 SP4+, or SQLite built inRow counts grow with issues, PRs and Actions logs
Repository storageNo figure; repos, LFS objects and packages live under the data volumeYour repositories and any container images you push
Actions runnerRun it on a separate machine from GiteaThe builds themselves: compilers, test suites, images

The figures in the last column are the variables to measure on your own code; the docs give no per-user number, and neither do we.

Why put a light service on a whole machine

Gitea alone would run in a small virtual machine. The case for bare metal is the rest of the stack. A Git host is where every clone, fetch, LFS download and registry pull lands, and on a dedicated server the port is unmetered: no transfer allowance, no overage line, however many CI jobs pull the same images. You get root on the whole box, so Docker, the database and the reverse proxy sit next to Gitea with nothing else competing for the disks.

Recovery matters for a service your whole team pushes to. Every racked single-, dual- and quad-socket line has an out-of-band console (HPE iLO, Dell iDRAC or a Supermicro BMC), as do the m510 and m710x cartridges; the other cartridges have power control only. The operating system can be reinstalled from the control panel as often as you need, and Linux images are free.

Matching the line to the setup

  • Gitea alone, a handful of users: a Moonshot cartridge. The range describes them as one service per machine, which is exactly this job; SQLite or a local PostgreSQL fits beside it.
  • Gitea plus PostgreSQL for a larger team: a single-socket server, the class the service page names for an application with its database on the same box.
  • The Actions runner: a second machine. The desktop platform line is described for build runners, with a dedicated GPU and NVMe; a cartridge suits lighter jobs.
  • Many services on one host: a dual-socket server, if Gitea is one of a dozen internal tools.

Racked machines are handed over in about 30 minutes once payment is approved; built-to-order lines take 4 to 24 hours. Terms are month to month.

SQLite, PostgreSQL or MySQL

Pick the database before the first install. The Gitea docs warn that converting one database type to another is not a well-tested process, and that SQLite does not scale: if the instance will grow, choose another engine from the start.

  • SQLite needs no extra container and suits a personal or very small instance. gitea dump includes the database file.
  • PostgreSQL is the usual production choice and has its own dump tooling; tuning it is covered on our PostgreSQL server page.
  • MySQL or MariaDB must use the utf8mb4 character set. Gitea's own Actions quick start notes that emoji in workflow names fail with error 1366 on any other charset.

Installing with Docker Compose

This is the PostgreSQL example from the Gitea Docker guide with three changes: the web port is bound to 127.0.0.1 so it is not public before an admin exists, the database password comes from a .env file instead of the literal gitea, and SSH_PORT is set so clone URLs show port 222.

networks:
  gitea:
    external: false
services:
  server:
    image: docker.gitea.com/gitea:28.2.0
    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=${DB_PASS}
      - GITEA__server__SSH_PORT=222
    restart: always
    networks:
      - gitea
    volumes:
      - ./gitea:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "127.0.0.1:3000:3000"
      - "222:22"
    depends_on:
      - db
  db:
    image: docker.io/library/postgres:18
    restart: always
    environment:
      - POSTGRES_USER=gitea
      - POSTGRES_PASSWORD=${DB_PASS}
      - POSTGRES_DB=gitea
    networks:
      - gitea
    volumes:
      - ./postgres:/var/lib/postgresql
  1. Install Docker Engine with the Compose plugin, create a gitea directory, and save the file above as docker-compose.yml.
  2. Put DB_PASS= followed by a long random string in .env next to it and run chmod 600 .env. Make ./gitea owned by UID and GID 1000, or the container may not start.
  3. Pin a release tag rather than latest, as above, so upgrades happen when you choose. Bring the stack up: docker compose up -d, then docker compose ps.
  4. Open the installer through an SSH tunnel, ssh -L 3000:127.0.0.1:3000 root@your-server, then browse to http://localhost:3000. The database host is db. Set the base URL to the public HTTPS address and the SSH port to 222.

Docker writes its own iptables rules, so a port published as 3000:3000 is reachable from the internet even with ufw denying it. Keep the 127.0.0.1: prefix on anything that should stay private.

First admin, locked installer, closed sign-ups

Do this before a reverse proxy makes the site public. Create the administrator from the shell; --random-password prints a generated password once instead of putting one on the command line, where ps and shell history would keep it:

docker exec -u git gitea gitea admin user create --admin \
  --username alice --email alice@example.com --random-password

# then confirm in ./gitea/gitea/conf/app.ini
[security]
INSTALL_LOCK = true

[service]
DISABLE_REGISTRATION = true

INSTALL_LOCK = true makes the installation page unreachable. DISABLE_REGISTRATION = true means only an admin can create accounts. The new user must change the password at first login, which is the CLI's default. The finished configuration lives at /data/gitea/conf/app.ini inside the container; chmod 640 it, because it holds SECRET_KEY and INTERNAL_TOKEN.

Then put Nginx in front. The Gitea reverse-proxy guide passes / to http://localhost:3000 with HTTP/1.1, the Host, X-Forwarded-Proto, Upgrade and Connection headers forwarded, and client_max_body_size raised so large pushes and container images do not fail with a 413. Set [server] ROOT_URL to the HTTPS address.

Git over SSH on port 222

The Gitea image runs its own OpenSSH daemon on port 22 inside the container. Mapping it to host port 222 leaves the host's sshd on 22 for administration, and SSH_PORT = 222 makes the clone URL read ssh://git@git.example.com:222/owner/repo.git. Open 222 in your firewall; it is meant to be public.

If you want clone URLs on plain port 22, the Docker guide's alternative is a second IP address: bind the host sshd to one with ListenAddress and publish the container's port 22 on the other. That needs an extra IPv4 on the server, a priced line on the order form.

Gitea Actions and the runner

Gitea Actions has been enabled by default since Gitea 1.21 and is mostly compatible with GitHub Actions workflows, kept in .gitea/workflows/. Each repository still has Actions switched off until you enable it in its settings. Jobs run on Gitea Runner, the gitea-runner program, which earlier documentation calls act_runner. If your code lives on GitHub rather than your own Gitea, the setup is different: see our GitHub Actions runner page.

The docs recommend running runners on separate machines from Gitea so builds cannot starve the web UI. The 5-dind image bundles its own Docker daemon for jobs, so jobs never touch the host's Docker socket, which is equivalent to root on the host. It requires --privileged. Take a registration token from Site Administration, an organization's settings or a repository's settings, depending on how widely the runner should serve.

services:
  runner:
    image: docker.io/gitea/runner:5-dind
    restart: always
    privileged: true
    environment:
      GITEA_INSTANCE_URL: https://git.example.com
      GITEA_RUNNER_REGISTRATION_TOKEN_FILE: /run/secrets/token
    volumes:
      - ./data:/data
      - ./token:/run/secrets/token:ro
      - runner-docker:/var/lib/docker
volumes:
  runner-docker:

Keep ./data: it holds the .runner registration file, which is a secret; lose it and the container registers as a new runner. Jobs with the host label run on the runner's machine without isolation, so use them only for trusted repositories, and use ephemeral runners for code you do not fully trust.

Backups, upgrades, and when Gitea is the wrong tool

gitea dump writes repositories, the data directory, configuration and a SQL dump to one ZIP, but the docs state that a consistent backup needs Gitea stopped while it runs. With PostgreSQL they also suggest pg_dump over the XORM-generated SQL. Keep SECRET_KEY with the backup: without it, encrypted data such as 2FA secrets cannot be decrypted.

Upgrading means editing the tag, fetching the image (docker compose pull) and recreating the container (docker compose up -d); the data stays on the host volume. Never switch between the rootful and rootless images; the docs say they are not compatible.

Gitea is the wrong pick if you want one product with built-in security scanning and a larger bundled feature set and are prepared to give it far more memory. That is the trade made on our self-hosted GitLab page.

Questions

How much RAM does a Gitea server need?

The Gitea documentation says 2 CPU cores and 1 GB of RAM is typically sufficient for small teams and projects. The database, indexers and any runner on the same host add to that, so size for the whole stack. The docs recommend putting runners on a separate machine.

Can I keep using SQLite as the team grows?

The docs say SQLite does not scale and recommend another database type if you expect growth. They also warn that converting between database types is not well tested. Starting on PostgreSQL avoids that migration.

Will my GitHub Actions workflows run on Gitea?

Gitea Actions is designed to be compatible with GitHub Actions wherever possible, and workflows go in .gitea/workflows/. Some actions may not work in SHA256 repositories or when Gitea runs on a sub-path, actions/checkout among them. Enable Actions per repository first; it is off by default.

Why does the runner image need privileged mode?

The 5-dind image starts its own Docker daemon so jobs never get the host's Docker socket. Running a daemon inside a container requires --privileged. The basic 5 image avoids that by sharing the host daemon, which the docs say to do only for trusted repositories.

Can Gitea host container images?

Yes. Its package registry covers more than 20 formats, container images among them. Raise client_max_body_size on the reverse proxy for large layers, and if Gitea sits on a sub-path, route /v2 at the domain root to it as the docs require.

Pick the machine for your Git host

Compare every dedicated line by processor, memory, disks and port, then configure the one that fits Gitea, its database and a runner.

See the machines

Related on Server Room

Other use cases