Skip to Content

Self-Host Gitea: Run Your Own Git Server and Own Every Line of Code You Ship

Learn how to deploy Gitea with Docker, configure repositories and SSH access, set up CI/CD with Gitea Actions, and run a fully self-hosted Git platform your team can rely on.
Gitea setup guide

Self-Host Gitea: Run Your Own Git Server and Own Every Line of Code You Ship

GitHub is convenient — until you hit rate limits, need air-gapped deployments, want to avoid per-seat costs for a growing team, or simply don't want your proprietary code living on someone else's servers. Gitea is a lightweight, self-hosted Git service that gives you everything you actually use: repositories, pull requests, code review, issue tracking, webhooks, SSH access, and a full CI/CD pipeline via Gitea Actions. It's fast, runs on minimal hardware, and takes under 30 minutes to deploy. This guide walks you through a complete self-host Gitea setup from scratch.


Prerequisites

  • A Linux server (Ubuntu 22.04 LTS recommended) with at least 1 vCPU and 512MB RAM — 1GB+ recommended for team use
  • Docker Engine and Docker Compose v2 installed
  • A domain name with DNS access — HTTPS is required for Git operations to work correctly over the web
  • Ports 80, 443, and 22 (or a custom SSH port) available
  • Basic familiarity with Docker Compose and Git

Verify your starting point:

docker --version
docker compose version
free -h
df -h /

# Check if port 22 (SSH) is already in use by the host
sudo ss -tlnp | grep ':22'
# If it is, you'll use a different port for Gitea SSH (2222 is common)

What Is Gitea and Why Self-Host It?

Gitea is an open-source, self-hosted Git service written in Go. It's a fork of Gogs with active development, a large community, and a feature set that now includes Gitea Actions — a GitHub Actions-compatible CI/CD engine you can run entirely on your own infrastructure.

What Gitea Gives You

  • Full Git hosting — repositories, branches, tags, releases, LFS support
  • Pull requests and code review — review workflows, inline comments, protected branches, required reviews
  • Issue tracking — issues, milestones, labels, projects board
  • SSH and HTTPS access — standard Git remote protocols, both supported
  • Gitea Actions — GitHub Actions-compatible CI/CD workflows running on your own runners
  • Webhooks — trigger external systems on push, PR, issue events
  • Organizations and teams — fine-grained access control per repo and per team
  • Packages — npm, Docker, PyPI, Maven, and more — Gitea can serve as your private package registry
  • Mirror repositories — automatically mirror repos from GitHub, GitLab, or Bitbucket

Gitea vs. GitLab

GitLab self-hosted is powerful but heavy — it needs 4GB+ RAM just to start and has significant operational overhead. Gitea runs comfortably on a $5 VPS with 512MB RAM. If you need the full GitLab DevSecOps suite, GitLab is worth the overhead. If you need solid Git hosting with CI/CD and don't want to manage a complex platform, Gitea is the right call.


Deploying Gitea with Docker Compose

Project Structure

mkdir -p ~/gitea
cd ~/gitea

The Docker Compose File

Gitea works fine with SQLite for small teams, but PostgreSQL is recommended for anything beyond a handful of users. This setup uses PostgreSQL:

# docker-compose.yml
version: '3.8'

services:

  gitea:
    image: gitea/gitea:latest
    container_name: gitea
    restart: unless-stopped
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - GITEA__database__DB_TYPE=postgres
      - GITEA__database__HOST=postgres:5432
      - GITEA__database__NAME=gitea
      - GITEA__database__USER=gitea
      - GITEA__database__PASSWD=${POSTGRES_PASSWORD}
      - GITEA__server__DOMAIN=git.yourdomain.com
      - GITEA__server__SSH_DOMAIN=git.yourdomain.com
      - GITEA__server__ROOT_URL=https://git.yourdomain.com
      - GITEA__server__HTTP_PORT=3000
      - GITEA__server__SSH_PORT=2222
      - GITEA__server__START_SSH_SERVER=true
      - GITEA__service__DISABLE_REGISTRATION=false
      - GITEA__service__REQUIRE_SIGNIN_VIEW=false
    ports:
      - "3000:3000"    # Web UI (proxied via Nginx)
      - "2222:22"      # SSH Git access
    volumes:
      - gitea_data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - gitea_net

  postgres:
    image: postgres:15-alpine
    container_name: gitea_db
    restart: unless-stopped
    environment:
      - POSTGRES_DB=gitea
      - POSTGRES_USER=gitea
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U gitea"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - gitea_net

volumes:
  gitea_data:
  postgres_data:

networks:
  gitea_net:

Create your .env file:

# .env
POSTGRES_PASSWORD=a-strong-postgres-password

# Generate with:
# openssl rand -base64 24

Start the stack:

docker compose up -d
docker compose logs -f gitea

Wait for Starting new Web server: tcp:0.0.0.0:3000 in the logs. Then open http://localhost:3000 to complete the initial setup wizard.

First-Run Setup Wizard

The Gitea installer page pre-fills most settings from the environment variables. Confirm:

  • Database settings match your Compose config
  • Site URL is set to https://git.yourdomain.com
  • SSH server domain is git.yourdomain.com

Create your admin account at the bottom of the page and click Install Gitea. Installation completes in seconds and you're redirected to the dashboard.


Configuring HTTPS with Nginx

server {
    listen 80;
    server_name git.yourdomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name git.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/git.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/git.yourdomain.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    # For LFS and large repo pushes
    client_max_body_size 1G;

    # Longer timeouts for large clones and pushes
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;
    proxy_connect_timeout 300s;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        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;
        proxy_buffering off;
    }
}
sudo ln -s /etc/nginx/sites-available/gitea /etc/nginx/sites-enabled/
sudo nginx -t

# Get Let's Encrypt certificate
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d git.yourdomain.com

sudo systemctl reload nginx

# Test HTTPS
curl -I https://git.yourdomain.com
# Should return HTTP/2 200

SSH Port for Git Operations

Gitea's SSH server listens on port 2222 (mapped from container port 22 in the Compose file). Users need to configure their SSH client to use port 2222 for your domain. Tell your team to add this to their ~/.ssh/config:

# Add to ~/.ssh/config on each developer's machine:
Host git.yourdomain.com
    HostName git.yourdomain.com
    Port 2222
    User git
    IdentityFile ~/.ssh/id_ed25519

# After adding this, standard Git SSH URLs work:
# git clone [email protected]:username/repo.git

# Test SSH connectivity:
ssh -T [email protected] -p 2222
# Should respond: Hi username! You've successfully authenticated...

Managing Users, Organizations, and Repositories

User Management

As admin, go to Site Administration → User Accounts to manage all users. For a private team setup, disable open registration after your team has joined:

# Disable open registration via environment variable:
# Update docker-compose.yml:
- GITEA__service__DISABLE_REGISTRATION=true

# Or set via the admin UI:
# Site Administration → Configuration → Service Settings
# Uncheck "Allow Registration"

# Create users as admin via CLI:
docker exec -it gitea gitea admin user create \
  --username newuser \
  --password securepassword \
  --email [email protected]

# Create an admin user via CLI:
docker exec -it gitea gitea admin user create \
  --username newadmin \
  --password securepassword \
  --email [email protected] \
  --admin

Organizations and Teams

Organizations in Gitea are shared accounts that own repositories, with teams controlling access. Create an org for your company and set up teams with appropriate permissions:

  • Owners — full admin access to all org repos
  • Developers — read/write access, can push to non-protected branches
  • Reviewers — read access only, can comment on PRs

Protected Branches and Required Reviews

For main or production branches, enable branch protection under Repository → Settings → Branches → Add Rule:

  • Enable Require pull request reviews before merging
  • Set minimum number of required approvals
  • Enable Dismiss stale approvals when new commits are pushed
  • Enable Require status checks to pass to block merges when CI fails

Setting Up Gitea Actions (CI/CD)

Enabling Gitea Actions

Gitea Actions is GitHub Actions-compatible CI/CD built into Gitea. Enable it and deploy a runner:

# Enable Actions via environment variable in docker-compose.yml:
- GITEA__actions__ENABLED=true

# Restart Gitea to apply:
docker compose up -d --force-recreate gitea

# Get a runner registration token from:
# Site Administration → Runners → Create new runner token
# Or for a specific repo:
# Repository → Settings → Actions → Runners

Deploying the Act Runner

The act runner executes workflow jobs. Add it to your Compose stack:

# Add to docker-compose.yml services:
  runner:
    image: gitea/act_runner:latest
    container_name: gitea_runner
    restart: unless-stopped
    environment:
      - GITEA_INSTANCE_URL=https://git.yourdomain.com
      - GITEA_RUNNER_REGISTRATION_TOKEN=${RUNNER_TOKEN}
      - GITEA_RUNNER_NAME=main-runner
      - GITEA_RUNNER_LABELS=ubuntu-latest:docker://node:20-bullseye
    volumes:
      - runner_data:/data
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - gitea_net

# Add to volumes:
  runner_data:

# Add to .env:
# RUNNER_TOKEN=your-runner-registration-token

Your First Workflow

Gitea Actions uses the same YAML syntax as GitHub Actions. Create .gitea/workflows/ci.yml in any repo:

# .gitea/workflows/ci.yml
name: CI

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Build
        run: npm run build

  docker:
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4

      - name: Build Docker image
        run: |
          docker build -t git.yourdomain.com/org/app:${{ gitea.sha }} .
          docker push git.yourdomain.com/org/app:${{ gitea.sha }}

Push this file to your repo and watch it run under the repo's Actions tab. The workflow syntax is identical to GitHub Actions — most existing GitHub workflows run on Gitea with minimal changes.


Tips, Gotchas, and Troubleshooting

SSH Push Rejected with Permission Denied

# Test SSH auth directly
ssh -T [email protected] -p 2222 -v

# Common causes:
# 1. SSH key not added to Gitea account
#    → Settings → SSH Keys → Add Key

# 2. Wrong port — missing ~/.ssh/config entry
#    Test with explicit port:
git clone ssh://[email protected]:2222/username/repo.git

# 3. Key type not supported — use ed25519:
ssh-keygen -t ed25519 -C "[email protected]"
cat ~/.ssh/id_ed25519.pub  # Copy this to Gitea

Large Repository Clone Fails or Hangs

Check Nginx's client_max_body_size and proxy timeouts. For repos with LFS objects, also enable LFS in Gitea's config:

# Enable LFS in docker-compose.yml environment:
- GITEA__server__LFS_START_SERVER=true
- GITEA__lfs__PATH=/data/lfs

# Set LFS content path inside container (persisted in gitea_data volume)

# Check git LFS is installed on developer machines:
git lfs version

# If clones timeout, increase Nginx proxy timeouts:
# proxy_read_timeout 600s;
# proxy_send_timeout 600s;

Actions Runner Not Picking Up Jobs

# Check runner logs
docker logs gitea_runner --tail 50

# Verify runner registered correctly in Gitea:
# Site Administration → Runners → should show your runner as online

# Common issues:
# 1. Wrong registration token — regenerate and update RUNNER_TOKEN in .env
# 2. Runner can't reach Gitea — check network config
docker exec gitea_runner curl -s https://git.yourdomain.com/api/v1/version

# 3. Docker socket not mounted — required for running Docker-based jobs
ls -la /var/run/docker.sock
# Must be accessible to the runner container

Updating Gitea

cd ~/gitea

# Pull the latest image
docker compose pull

# Restart — database migrations run automatically on startup
docker compose up -d

# Watch migration logs
docker compose logs gitea --tail 30

# Verify the new version
docker exec gitea gitea --version

# Pin to a specific version for production stability:
# image: gitea/gitea:1.22.0

Mirroring from GitHub

Gitea can mirror repositories from GitHub automatically — useful for keeping forks in sync or creating internal copies of public dependencies:

# Create a mirror via the UI:
# Explore → Repositories → New Migration → GitHub
# Or via API:
curl -X POST https://git.yourdomain.com/api/v1/repos/migrate \
  -H 'Content-Type: application/json' \
  -H 'Authorization: token YOUR_GITEA_API_TOKEN' \
  -d '{
    "clone_addr": "https://github.com/owner/repo",
    "repo_name": "repo",
    "mirror": true,
    "mirror_interval": "8h",
    "private": true,
    "uid": YOUR_USER_ID
  }'

Pro Tips

  • Use Gitea as a package registry — beyond Git hosting, Gitea serves as a private registry for Docker images, npm packages, PyPI packages, and more. Enable it under Site Administration → Configuration → Packages and use it as your private artifact store.
  • Back up via the admin API — Gitea has a built-in dump command that exports everything: docker exec gitea gitea dump -c /data/gitea/conf/app.ini. Schedule this daily and send the archive to S3 or MinIO.
  • Mirror your repos to GitHub for redundancy — set up push mirrors so your Gitea repos automatically sync to GitHub. If your VPS goes down, developers can still access code from the GitHub mirror.
  • Enable two-factor authentication — go to Site Administration → Authentication Sources and require 2FA for admin accounts at minimum. Users can enable it per-account under their profile settings.
  • Use Gitea's webhook integrations with your CI/CD stack — if you're not using Gitea Actions, webhooks let you trigger builds in Jenkins, Drone, Woodpecker CI, or any other system on push events.

Wrapping Up

A complete self-host Gitea deployment gives your team a full-featured Git platform — repositories, pull requests, CI/CD, package registry, and issue tracking — running on hardware you control with no per-seat costs and no vendor lock-in. It runs on minimal resources, handles teams of any size, and the GitHub Actions-compatible workflow syntax means you don't need to learn a new CI/CD language.

Start with the Docker Compose stack, get HTTPS working with Nginx, create your first organization and repositories, and run a simple Actions workflow to confirm CI is working end to end. From that point, every project your team ships can live entirely on infrastructure you own — from first commit to production deploy.


Need a Self-Hosted DevOps Platform Built for Your Team?

If you're moving your team's code infrastructure in-house — Gitea, CI runners, package registries, deployment pipelines, and access controls all wired together — the sysbrix team can design and deploy it. We build self-hosted DevOps stacks that engineering teams can rely on from day one.

Talk to Us →
OpenClaw Docker VPS Deploy: Run Your AI Personal Assistant in the Cloud 24/7
Learn how to deploy OpenClaw on a VPS using Docker, configure your AI agents, connect messaging channels, and keep your personal AI assistant running around the clock on infrastructure you own.