Self-Hosting Gitea: A Complete Guide to Running an Open-Source Git Forge

Gitea is a lightweight open-source Git forge for repositories, pull requests, issues, packages, releases, and optional CI-style actions. This guide walks through a practical Docker Compose deployment on Ubuntu 24.04 LTS with PostgreSQL, SSH clone URLs, Nginx, HTTPS, backups, upgrades, and day-two operations.

· Updated · 5 min read #self-hosted #open-source #gitea #git #code-hosting #go #deployment #docker #vps

 Gitea self-hosted Git forge overview

Caption: Gitea provides repositories, issues, pull requests, packages, releases, SSH access, and optional automation from a small self-hosted Git forge.

Introduction

Gitea is a lightweight open-source Git forge for teams and individuals who want repository hosting without running a large DevOps platform. It provides Git repositories, pull requests, code review, issues, milestones, project boards, releases, package registries, webhooks, repository mirroring, user management, and optional Gitea Actions in a single web application. The interface feels familiar to anyone who has used GitHub, GitLab, Forgejo, or Bitbucket, but the operational footprint is small enough for a VPS, homelab server, internal tooling box, or private development network.

Self-hosting Gitea makes sense when source code, deployment manifests, infrastructure notes, internal scripts, or client projects should stay under your control. You decide where repositories live, who can register, how backups are stored, whether anonymous visitors can browse code, which runners can execute automation jobs, and when upgrades happen. For small teams, Gitea can replace ad hoc bare repositories over SSH with a friendlier review workflow. For homelab administrators, it becomes a private place to version Docker Compose files, Ansible playbooks, dotfiles, and documentation.

This guide installs Gitea on Ubuntu 24.04 LTS with Docker Compose using the official docker.gitea.com/gitea image, PostgreSQL, host-mounted data directories, Nginx, Let's Encrypt certificates, and a dedicated SSH clone port. The example publishes the site at https://git.example.com, maps container SSH to host port 222, stores data under /opt/gitea, and keeps configuration in environment variables that the official container writes into app.ini on startup. Replace example domains, email addresses, passwords, and network rules before using the commands in production.

Why Choose Gitea?

  • Lightweight Git hosting: Gitea is written in Go and is comfortable on a modest server compared with heavier all-in-one platforms.
  • Complete collaboration flow: Repositories, pull requests, reviews, issues, labels, milestones, projects, releases, and webhooks are built in.
  • Docker-friendly operations: The official Docker image supports environment-variable configuration and persistent /data storage.
  • PostgreSQL support: A dedicated database container keeps repository metadata separate from application files and Git object storage.
  • Private by design: Registration, anonymous access, repository visibility, SSH clone URLs, and organization membership are under your control.
  • Automation when needed: Gitea Actions can run CI-style workflows with compatible runners when you are ready to operate them safely.
  • Good migration target: Git remotes, repository mirrors, release assets, and webhooks make it practical for moving smaller projects into a self-hosted forge.

Gitea is still a source-code system, so treat it as sensitive infrastructure. The server may hold application secrets in private repositories, deployment keys, release artifacts, build logs, and webhook URLs. A successful install is only the beginning; backups, access control, SSH configuration, update testing, and runner isolation matter.

Prerequisites

Hardware Recommendations:

  • 1 vCPU and 1 GB RAM for a personal instance with a handful of repositories
  • 2 vCPU and 2-4 GB RAM for a small team, package registry use, or frequent repository indexing
  • SSD storage for Git object databases, attachments, avatars, packages, logs, and PostgreSQL data
  • Off-server backup storage such as another VPS, NAS, encrypted object storage bucket, or backup appliance
  • Additional CPU and memory if you later add Gitea Actions runners on the same host

Software and Accounts:

  • Ubuntu 24.04 LTS server with sudo access
  • A domain such as git.example.com
  • DNS A or AAAA record pointing the hostname to the server
  • Docker Engine with the Docker Compose v2 plugin
  • Nginx, Certbot, OpenSSL, curl, ufw, jq, git, rsync, and the PostgreSQL client tools
  • A password manager for the site administrator account, database password, SMTP password, and recovery notes

Security Notes:

  • Put the web UI behind HTTPS before creating real users or repositories.
  • Use a separate SSH host port such as 222 unless you dedicate host port 22 to Git traffic.
  • Disable open registration unless you intentionally run a public community forge.
  • Keep /opt/gitea/.env, backups, private repositories, and SSH host keys private.
  • Do not run untrusted Gitea Actions jobs on the same host as the main application.
  • Back up both the Gitea data directory and PostgreSQL before upgrades.

Start with an updated host and a tight firewall:

sudo apt update
sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg openssl ufw nginx certbot python3-certbot-nginx jq git rsync postgresql-client

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 222/tcp
sudo ufw enable
sudo ufw status

Installation Guide

This deployment uses the official Docker image with PostgreSQL. The Gitea container stores application files, Git repositories, attachments, logs, templates, and generated app.ini content under /data. PostgreSQL stores relational metadata such as users, issues, pull requests, organizations, permissions, releases, webhooks, and settings.

1. Install Docker Engine

Install Docker and confirm that Compose v2 is available:

curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER"
newgrp docker

docker --version
docker compose version

If docker compose version fails, fix Docker before continuing. The examples in this guide use the modern docker compose plugin syntax, not the older docker-compose Python command.

2. Create the Project Directory

Create a private working directory and subdirectories for Gitea and PostgreSQL data:

sudo mkdir -p /opt/gitea/{data,postgres,backups}
sudo chown -R "$USER":"$USER" /opt/gitea
chmod 700 /opt/gitea
cd /opt/gitea

Gitea's Docker documentation notes that host-mounted /data directories should be writable by the UID and GID used inside the container. The Compose file below sets USER_UID=1000 and USER_GID=1000; adjust those values if your project directory is owned by a different service user.

3. Generate Environment Values

Create a .env file for Compose interpolation. Generate a strong database password and choose your public hostname:

cd /opt/gitea

GITEA_DB_PASSWORD="$(openssl rand -base64 36 | tr -d '\n')"
cat > .env <<EOF
GITEA_DOMAIN=git.example.com
GITEA_ROOT_URL=https://git.example.com/
GITEA_SSH_PORT=222
GITEA_DB_PASSWORD=${GITEA_DB_PASSWORD}
POSTGRES_PASSWORD=${GITEA_DB_PASSWORD}
EOF

chmod 600 .env

Store the generated password in your password manager. The same value is used by Gitea and PostgreSQL in this single-host example. For a larger deployment, use separate secret handling and avoid placing production credentials in shell history.

4. Write Docker Compose Configuration

Create docker-compose.yml with a Gitea service, PostgreSQL service, named network, health check, and persistent host mounts:

networks:
  gitea:
    external: false

services:
  server:
    image: docker.gitea.com/gitea:1.27.1
    container_name: gitea
    restart: always
    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=${GITEA_DB_PASSWORD}
      - GITEA__server__DOMAIN=${GITEA_DOMAIN}
      - GITEA__server__SSH_DOMAIN=${GITEA_DOMAIN}
      - GITEA__server__SSH_PORT=${GITEA_SSH_PORT}
      - GITEA__server__ROOT_URL=${GITEA_ROOT_URL}
      - GITEA__service__DISABLE_REGISTRATION=true
      - GITEA__service__REQUIRE_SIGNIN_VIEW=false
      - GITEA__repository__DEFAULT_PRIVATE=last
      - GITEA__session__COOKIE_SECURE=true
    networks:
      - gitea
    volumes:
      - ./data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "127.0.0.1:3000:3000"
      - "222:22"
    depends_on:
      db:
        condition: service_healthy

  db:
    image: docker.io/library/postgres:16
    container_name: gitea-db
    restart: always
    environment:
      - POSTGRES_USER=gitea
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=gitea
    networks:
      - gitea
    volumes:
      - ./postgres:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U gitea -d gitea"]
      interval: 10s
      timeout: 5s
      retries: 5

 Gitea Docker Compose stack

Caption: A small Gitea deployment separates the web application, SSH service, PostgreSQL metadata, Git repository storage, Nginx proxy, and backup destination.

The web port is bound to 127.0.0.1:3000 so only Nginx can reach it locally. SSH is published on host port 222 so Git users clone with URLs like ssh://git@git.example.com:222/owner/repo.git. If you need standard SSH on port 22, dedicate the host to Gitea SSH or configure your host OpenSSH service carefully before changing the port mapping.

Validate and start the stack:

docker compose config
docker compose pull
docker compose up -d
docker compose ps
docker compose logs -f --tail=100 server

Wait until the server logs show the web service is listening. Then visit http://SERVER-IP:3000 from a trusted network or configure Nginx before completing the browser installation wizard.

5. Configure Nginx and HTTPS

Create an Nginx reverse proxy for the Gitea web interface:

sudo tee /etc/nginx/sites-available/gitea >/dev/null <<'EOF'
server {
    listen 80;
    server_name git.example.com;

    client_max_body_size 512m;

    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;
    }
}
EOF

sudo ln -s /etc/nginx/sites-available/gitea /etc/nginx/sites-enabled/gitea
sudo nginx -t
sudo systemctl reload nginx

sudo certbot --nginx -d git.example.com

After Certbot finishes, confirm the HTTPS site loads:

curl -I https://git.example.com/

If you plan to push large releases, packages, or Git LFS objects, increase client_max_body_size to match your policy. For normal source repositories, 512 MB is often enough, but teams using package registries may need more.

6. Complete the First-Run Wizard

Open https://git.example.com/ and review the installer. Most database settings are already provided by environment variables:

  • Database type: PostgreSQL
  • Host: db:5432
  • Database name: gitea
  • Username: gitea
  • Password: value from /opt/gitea/.env
  • Site title: your organization or personal forge name
  • Server domain: git.example.com
  • Gitea base URL: https://git.example.com/
  • SSH server port: 222

Create the initial administrator account during the wizard. Because registration is disabled in the Compose environment, this account becomes your way to invite or create additional users.

Configuration

The official Docker image can turn environment variables such as GITEA__server__ROOT_URL into app.ini settings on startup. This keeps common settings visible in Compose while still allowing deeper tuning in /opt/gitea/data/gitea/conf/app.ini after installation.

 Gitea security configuration map

Caption: Production configuration should line up public URLs, SSH clone settings, registration policy, cookie security, SMTP delivery, and backup ownership.

Review the generated configuration:

docker compose exec server bash -lc 'grep -E "ROOT_URL|SSH_DOMAIN|SSH_PORT|DISABLE_REGISTRATION|COOKIE_SECURE" /data/gitea/conf/app.ini'

For a private team forge, consider these settings:

[service]
DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = true

[repository]
DEFAULT_PRIVATE = private

[server]
ROOT_URL = https://git.example.com/
SSH_DOMAIN = git.example.com
SSH_PORT = 222

[session]
COOKIE_SECURE = true

Set up SMTP before inviting users so password resets, mention notifications, and repository events can be delivered:

services:
  server:
    environment:
      - GITEA__mailer__ENABLED=true
      - GITEA__mailer__PROTOCOL=smtps
      - GITEA__mailer__SMTP_ADDR=smtp.example.com
      - GITEA__mailer__SMTP_PORT=465
      - GITEA__mailer__FROM=git@example.com
      - GITEA__mailer__USER=git@example.com
      - GITEA__mailer__PASSWD=${GITEA_SMTP_PASSWORD}

Restart after configuration changes:

docker compose up -d
docker compose logs --tail=100 server

Usage

Start with a small operational checklist before moving real projects:

  1. Log in as the administrator and create a normal user account for daily work.
  2. Add your SSH public key under Settings > SSH / GPG Keys.
  3. Create a private test repository and clone it through the SSH URL.
  4. Push a commit, open an issue, create a branch, and submit a pull request.
  5. Confirm email notifications if SMTP is enabled.
  6. Confirm anonymous visitors can or cannot see repositories according to your policy.

Test SSH clone and push from your workstation:

ssh -T -p 222 git@git.example.com

git clone ssh://git@git.example.com:222/your-user/test-repo.git
cd test-repo
printf '# Test Repository\n' > README.md
git add README.md
git commit -m "docs: add readme"
git push origin main

For an existing project, add Gitea as another remote and push the branches you want to host:

git remote add gitea ssh://git@git.example.com:222/your-user/project.git
git push gitea main
git push gitea --tags

If you enable Gitea Actions, use a separate runner host or a locked-down container environment. CI jobs execute code from repositories, so they should not share the same trust boundary as the forge database, repository storage, or backup credentials.

Backups, Upgrades, and Maintenance

Backups must include both PostgreSQL and /opt/gitea/data. The database contains relational state; the data directory contains Git repositories, attachments, avatars, logs, package files, custom templates, SSH keys, and the generated configuration.

 Gitea backup and restore workflow

Caption: A useful backup plan captures PostgreSQL, Gitea's persistent data, repository objects, release assets, configuration, verification logs, and off-server storage.

Create a simple backup script:

sudo tee /opt/gitea/backup.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

cd /opt/gitea
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p backups

docker compose exec -T db pg_dump -U gitea -d gitea | gzip > "backups/gitea-db-${stamp}.sql.gz"
tar --one-file-system -czf "backups/gitea-data-${stamp}.tar.gz" data

find backups -type f -mtime +14 -delete
EOF

sudo chmod 700 /opt/gitea/backup.sh
/opt/gitea/backup.sh

Copy the resulting files away from the server:

rsync -avz /opt/gitea/backups/ backup-user@backup.example.net:/srv/backups/gitea/

For upgrades, read the Gitea release notes, take a fresh backup, pull the new image, and restart:

cd /opt/gitea
/opt/gitea/backup.sh
docker compose pull
docker compose up -d
docker compose logs --tail=100 server

Pinning docker.gitea.com/gitea:1.27.1 makes upgrades deliberate. When you decide to move, edit the image tag in docker-compose.yml, test on a staging copy if possible, and keep the pre-upgrade backup until clone, push, issue, pull request, release, package, and login workflows are verified.

Troubleshooting

  • Web UI shows wrong clone URLs: Confirm GITEA__server__ROOT_URL, GITEA__server__SSH_DOMAIN, and GITEA__server__SSH_PORT match the public hostname and host SSH port.
  • SSH clone fails: Verify sudo ufw status, the Compose ports mapping, and ssh -T -p 222 git@git.example.com. Port 222 must be reachable from clients.
  • Uploads fail with 413 errors: Increase client_max_body_size in the Nginx site and reload Nginx.
  • Container cannot write to /data: Check ownership of /opt/gitea/data and make sure USER_UID and USER_GID match the owner.
  • Database connection fails: Run docker compose ps, check the db health status, and verify the password values in .env match the Compose environment.
  • Registration policy is wrong: Review [service] DISABLE_REGISTRATION in app.ini and restart the container after Compose changes.
  • Emails do not send: Check SMTP host, port, protocol, username, password, sender address, and container logs with docker compose logs server.
  • Backups are incomplete: Restore into a temporary server and confirm repositories, issues, attachments, releases, package files, users, and settings are present.

Scaling, Securing, and Next Steps

Following this guide gives you a working self-hosted Git forge with HTTPS, SSH clone support, PostgreSQL-backed metadata, disabled open registration, persistent storage, and a backup routine. From here, the most valuable next step is operational polish: document your restore process, test a restore on a clean host, schedule backups through systemd timers or cron, monitor disk usage, and keep an upgrade log.

As your usage grows, split responsibilities carefully. Move backups to dedicated storage, run Actions runners on separate machines, place the database on managed or separately monitored PostgreSQL if the team depends on it daily, and use groups or organizations to avoid permission sprawl. Gitea stays pleasant because it is simple; keep the deployment understandable enough that you can rebuild it when a server fails.

References

Share:

Get new posts in your inbox

No spam. One short email per new article — practical PHP, Laravel, devops, and AI-assisted workflows.

Comments

Powered by GitHub Discussions via Giscus. A free GitHub account is required.