← Blog

Self-host Gitea for your own Git server

Railyard

May 25, 2026 · 8 min read

Follow this on your own server. Start free, no card needed

You want your repositories on a machine you control: private code that never leaves your infrastructure, no per-seat pricing, and a web interface your team already knows how to use. Gitea gives you that in one container: repository browsing, pull requests, issues, a package registry and Git over HTTPS and SSH. This guide takes you from an empty server to Gitea at your own domain, with HTTPS through Caddy, SSH pushes on their own port, and a backup you have restored at least once. The last part shows the one-click version on Railyard.

This guide uses Gitea 28.2.0, the current release, from Gitea’s own registry at docker.gitea.com. Gitea moved from 1.x version numbers to 28.0.0 in September 2026, so a “28” tag is the newer release, not a typo.

What you’ll need

  • A server. Gitea is light. A Hetzner CX23 (2 vCPU, 4 GB RAM, 40 GB NVMe) costs €5.99/month excluding VAT and is plenty for a small team. Repositories grow, so watch the disk.
  • A domain or subdomain you can point at the server. This guide uses git.example.com.
  • SSH access as root or a user with sudo.
  • A free host port for Git over SSH. The server’s own SSH stays on port 22, so Gitea’s SSH server gets port 222, the mapping Gitea’s documentation uses.

Step 1: Point DNS at the server and update it

Create the server and add an A record for git.example.com pointing at its public IPv4 address. Do it first: Caddy can only get a certificate once the name resolves to your server.

ssh root@203.0.113.20
apt update && apt upgrade -y

From your own machine, check the record:

dig +short git.example.com

You should see your server’s IP. If not yet, continue and check again before Step 5.

Step 2: Install Docker

Gitea and Caddy run as containers, so Docker Engine with the Compose plugin is all you need:

curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
docker compose version

You should see a Docker Compose version line.

Step 3: Write the Compose file

Gitea runs on SQLite unless you point it at another database. SQLite keeps everything in one volume and is fine for a small team, so this guide uses it (the FAQ covers Postgres). Create the directory:

mkdir -p /opt/gitea/backups
cd /opt/gitea

Save this as /opt/gitea/compose.yaml, with your own domain:

services:
  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config
    depends_on:
      - gitea

  gitea:
    image: docker.gitea.com/gitea:28.2.0
    restart: unless-stopped
    environment:
      USER_UID: "1000"
      USER_GID: "1000"
      GITEA__server__DOMAIN: git.example.com
      GITEA__server__ROOT_URL: https://git.example.com/
      GITEA__server__SSH_DOMAIN: git.example.com
      GITEA__server__SSH_PORT: "222"
    volumes:
      - gitea-data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "222:22"

volumes:
  gitea-data:
  caddy-data:
  caddy-config:

What it does:

  • The image, USER_UID/USER_GID, the time zone mounts and the 222:22 mapping come straight from Gitea’s Docker documentation. Inside the container, an SSH server listens on 22 for Git; the host exposes it as 222.
  • GITEA__section__KEY variables set values in Gitea’s app.ini. ROOT_URL makes Gitea build HTTPS links, and SSH_PORT makes the clone URLs it shows say 222. They also pre-fill the setup wizard.
  • Gitea’s web port, 3000, isn’t published. Caddy reaches it over the Compose network, so the only way in on the web is HTTPS.
  • Everything Gitea stores (repositories, the SQLite database, uploads and app.ini) lives in the gitea-data volume. A named volume also takes care of file ownership for you.

Step 4: Write the Caddyfile

Save this as /opt/gitea/Caddyfile:

git.example.com {
	encode zstd gzip
	reverse_proxy gitea:3000
}

Caddy gets a Let’s Encrypt certificate for the domain, redirects HTTP to HTTPS, and proxies the web interface and Git over HTTPS to Gitea. Git over SSH doesn’t pass through Caddy: it goes straight to port 222. Check both files:

docker compose config --quiet && echo "compose ok"
docker compose run --rm --no-deps caddy caddy validate --config /etc/caddy/Caddyfile

You should see compose ok, then Valid configuration.

Step 5: Open the ports and start it

Allow 80, 443 and 222 in your cloud provider’s firewall. Then start the stack:

docker compose up -d
docker compose ps
docker compose logs -f caddy

You should see caddy and gitea with a status of Up, and Caddy logging that it obtained a certificate for git.example.com. Press Ctrl+C and open https://git.example.com. You should see Gitea’s installation page.

Step 6: Finish the setup wizard

Gitea has no default account: the first visit opens a one-time installation page. Check the values, most of which the environment variables already filled in:

  • Database Type: SQLite3, with the path /data/gitea/gitea.db.
  • SSH Server Port 222 and Gitea Website URL https://git.example.com/. The Environment Configuration box at the bottom lists the GITEA__ variables that will be written into /data/gitea/conf/app.ini.
  • Only administrators can create user accounts (no self-registration): tick it if only your team should have accounts.
  • Administrator Account Settings, under Optional Settings: set a username, email and password. The page says it plainly: if you skip this, the first user to register becomes an administrator.

Click Install Gitea. You should land on your dashboard, signed in as the administrator.

Now test both ways of pushing. Add your public key under Settings → SSH / GPG Keys, create a repository called test-repo, then from your machine:

git clone ssh://git@git.example.com:222/youradmin/test-repo.git
git clone https://git.example.com/youradmin/test-repo.git test-repo-https

You should see both clones succeed. Use the ssh:// form with the port: the short git@host:owner/repo form always uses port 22.

Step 7: Back up Gitea

gitea dump writes repositories, the database, app.ini and uploaded files into one archive. It has to run as the git user, because Gitea refuses to run as root. Save this as /opt/gitea/backup.sh:

#!/usr/bin/env bash
set -euo pipefail
cd /opt/gitea
docker compose exec -T -u git -w /tmp gitea gitea dump -c /data/gitea/conf/app.ini --file - > "backups/gitea-$(date +%F).zip"
find backups -name 'gitea-*.zip' -mtime +14 -delete

--file - streams the archive to standard output, so it lands on the host directly. Make it executable, run it, and schedule it at 03:00 with crontab -e:

chmod +x /opt/gitea/backup.sh
/opt/gitea/backup.sh
unzip -l /opt/gitea/backups/gitea-$(date +%F).zip | tail -3
0 3 * * * /opt/gitea/backup.sh

You should see a list of files ending in a total count. Copy the archives off the server as well, for example with rsync to another machine.

To restore into a fresh install, start Gitea on an empty volume, copy the archive in, and put each part back where it lives:

docker compose up -d gitea
docker compose cp backups/gitea-2026-05-25.zip gitea:/tmp/restore.zip
docker compose exec gitea sh -c 'mkdir -p /tmp/r && cd /tmp/r && unzip -q /tmp/restore.zip && mkdir -p /data/gitea/conf /data/git/repositories && cp app.ini /data/gitea/conf/app.ini && cp -a data/. /data/gitea/ && cp -a repos/. /data/git/repositories/ && chown -R git:git /data/gitea /data/git'
docker compose restart gitea
docker compose exec -u git gitea gitea -c /data/gitea/conf/app.ini admin regenerate hooks

This follows the restore steps in Gitea’s backup documentation for Docker. You should see your repositories and your administrator login back, with no installation page.

Step 8: Upgrade

Gitea runs its database migrations when it starts. Read the release notes, take a backup, change the image tag in compose.yaml, then:

/opt/gitea/backup.sh
docker compose pull gitea
docker compose up -d gitea
docker compose logs -f gitea

You should see Gitea start and serve requests again. If it fails to start, the log names the setting or migration at fault; put the old tag back and restore the backup if you need to roll back.

The shortcut: do it with Railyard

Steps 2 through 5 are what the Railyard Gitea template does for you: deploy Gitea on Railyard. Railyard builds Gitea from its repository with its own Dockerfile, runs it on a server you own, and gives it an HTTPS address. You can add your own domain from the app’s Domains tab (Domains and HTTPS). Then you finish the same setup wizard from Step 6 at the app’s URL: set Gitea Website URL to the app’s HTTPS address and create the administrator account.

Two things to set before you create repositories:

  • Keep /data on a volume. The template doesn’t attach one, so add data:/data under Volumes in the app’s settings. Volumes are kept across deploys, and you can back one up with railyard volume:backup <app> data.
  • Choose the database. The template attaches no database add-on, so Gitea uses SQLite inside /data. If you want Postgres instead, add it from the app’s Resources tab first and enter its host, name, user and password in the wizard’s database section, since moving databases after repositories exist is more work. Railyard’s Postgres backups are covered in Databases and backups.

Railyard publishes Gitea’s web port over HTTPS, so on Railyard you clone and push over HTTPS rather than SSH. Railyard’s Free plan covers one server plus a database server and three apps, with no card. Pro is $19/month and Team $49/month, per team.

Troubleshooting

address already in use on port 80, 443 or 222. Find what’s listening with ss -tlnp | grep -E ':(80|443|222)\s'. For 222, you can pick another host port (2222:22, say) as long as GITEA__server__SSH_PORT matches it.

Clone URLs show the wrong host, port or http://. Gitea writes these settings into /data/gitea/conf/app.ini. Check ROOT_URL, DOMAIN, SSH_DOMAIN and SSH_PORT in the environment, recreate the container with docker compose up -d gitea, and confirm the values in app.ini changed.

Caddy can’t get a certificate. Check that dig +short git.example.com returns your server’s IP and that port 80 is open in your provider’s firewall, since Let’s Encrypt validates over it. Caddy retries by itself once DNS is right.

Permission denied (publickey) on SSH clone. Make sure the key is added in Gitea (not on the server’s own authorized_keys), that you’re connecting to port 222 with the ssh:// URL form, and that the user is git, not your Gitea username.

gitea dump says Gitea is not supposed to be run as root. You left out -u git. docker compose exec runs as root unless you say otherwise.

The server runs out of memory. Gitea is small at rest, but cloning or pushing a very large repository can spike memory. Check dmesg | grep -i "out of memory" and docker stats, and resize if it happens again.

FAQ

Is SQLite enough, or should I use Postgres? SQLite is fine for a small team. Gitea’s Docker docs include a Postgres variant: add a db service and set GITEA__database__DB_TYPE=postgres, GITEA__database__HOST=db:5432, GITEA__database__NAME, GITEA__database__USER and GITEA__database__PASSWD on the Gitea service. Decide before the first install, because the wizard writes the choice into app.ini.

Can I bring my repositories from GitHub or GitLab? Yes. Gitea’s New Migration page has importers for GitHub, GitLab, Gitea and other services, plus a plain Git option for any URL. The service-specific importers can bring more than the code, such as issues and releases, depending on the source.

Does Gitea have CI like GitHub Actions? Gitea Actions uses workflow files close to GitHub Actions syntax, and runs them on a separate runner you install yourself. Setting up a runner is beyond this guide.

Why port 222? It keeps the server’s own SSH login on 22. Any free port works if SSH_PORT matches the host side of the mapping.

A self-hosted Git server mostly comes down to three things: the right URLs in app.ini, an SSH port that doesn’t collide with your own, and a backup you have actually restored. Get those right on day one and Gitea mostly takes care of itself.

Why people pick Railyard for this

  • Your own cloud account. Deploy to DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, or any Linux server over SSH, billed directly by your provider with no markup.
  • One flat price per team. Free is free forever: one server plus a database server, three apps, no card. Pro is $19 a month for up to five servers and unlimited apps. Team is $49 a month with unlimited servers. No per-app, per-deploy, per-seat or per-gigabyte charges.
  • One-click templates. Deploy open-source apps, like databases, analytics and dev tools, to your own server in one click.
  • Managed databases. Postgres, MySQL and Redis with scheduled, restore-verified backups and point-in-time recovery.
  • Push-to-deploy. Deploy from GitHub, GitLab or the CLI, with automatic HTTPS and zero-downtime releases.

Deploy Gitea in one click

The Railyard template builds Gitea on a server you own; finish its setup wizard in the browser and start pushing.

Free plan, no card, and 14 days of Pro for every new team. One flat price per team.