Skip to main content

Self-Host Gitea: Lightweight GitHub Alternative 2026

·OSSAlt Team
giteagitgithubself-hostingdockerdevops2026

TL;DR

Gitea (MIT, ~43K GitHub stars, Go) is a lightweight self-hosted Git service — the most popular GitHub alternative for self-hosters. Runs in a single Go binary or Docker container under 100MB RAM. Full Git hosting: repos, branches, pull requests, issues, wikis, releases, webhooks, CI/CD via Gitea Actions (GitHub Actions-compatible YAML), and organizations. GitHub charges $4/user/month for Teams. Gitea is free on your own hardware.

Key Takeaways

  • Gitea: MIT, ~43K stars, Go — full GitHub alternative in <100MB RAM
  • Gitea Actions: GitHub Actions-compatible YAML — reuse your existing workflows
  • SSH + HTTP: Standard Git push/pull over both SSH and HTTPS
  • Migration: Import repos from GitHub, GitLab, Bitbucket with one click
  • Packages: Container registry, npm, PyPI, Maven registries built-in
  • Federation: Gitea supports basic ActivityPub federation (experimental)

Gitea vs Forgejo vs GitLab CE

FeatureGiteaForgejoGitLab CE
LicenseMITGPL 3.0MIT (CE)
GitHub Stars~43K~11K~23K
RAM usage~100MB~100MB~4GB+
CI/CDGitea ActionsForgejo ActionsGitLab CI
Actions compatibilityGitHub Actions YAMLGitHub Actions YAMLCustom format
Container registryYesYesYes
Package registriesYes (npm/PyPI/Maven/etc.)YesYes
FederationExperimentalYes (ActivityPub)No
Setup complexityLowLowHigh

Part 1: Docker Setup

# docker-compose.yml
services:
  gitea:
    image: gitea/gitea:latest
    container_name: gitea
    restart: unless-stopped
    ports:
      - "3000:3000"
      - "2222:22"        # SSH — map host port 2222 to Gitea SSH
    volumes:
      - gitea_data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    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__ROOT_URL: "https://git.yourdomain.com"
      GITEA__server__SSH_DOMAIN: "git.yourdomain.com"
      GITEA__server__SSH_PORT: 2222
      GITEA__server__HTTP_PORT: 3000
      GITEA__service__DISABLE_REGISTRATION: "true"   # Disable after setup
      GITEA__service__REQUIRE_SIGNIN_VIEW: "false"
    depends_on:
      postgres:
        condition: service_healthy

  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: gitea
      POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
      POSTGRES_DB: gitea
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U gitea"]
      interval: 10s
      start_period: 20s

volumes:
  gitea_data:
  postgres_data:
# .env
POSTGRES_PASSWORD=your-secure-db-password

docker compose up -d

Visit http://your-server:3000 → complete the installation wizard.


Part 2: HTTPS with Caddy

git.yourdomain.com {
    reverse_proxy localhost:3000
}

SSH access

Users push over SSH using port 2222 (or any port you mapped):

# Clone via SSH:
git clone ssh://git@git.yourdomain.com:2222/username/repo.git

# Or add to ~/.ssh/config for convenience:
# Host git.yourdomain.com
#   Port 2222

Part 3: First-Time Setup

  1. Visit https://git.yourdomain.com
  2. Complete the installation wizard (DB settings are pre-populated from env vars)
  3. Create the admin account
  4. After setup, disable registration:
environment:
  GITEA__service__DISABLE_REGISTRATION: "true"
  GITEA__service__ALLOW_ONLY_EXTERNAL_SELF_REGISTRATION: "false"

Or via app.ini:

[service]
DISABLE_REGISTRATION = true

Part 4: SSH Key Setup

# Generate SSH key (if you don't have one):
ssh-keygen -t ed25519 -C "your@email.com"

# Copy public key:
cat ~/.ssh/id_ed25519.pub

# In Gitea: User Settings → SSH / GPG Keys → Add Key
# Paste public key content

# Test connection:
ssh -T git@git.yourdomain.com -p 2222
# Returns: Hi username! You've successfully authenticated.

# Clone:
git clone ssh://git@git.yourdomain.com:2222/myorg/myrepo.git

# Or set remote:
git remote add origin ssh://git@git.yourdomain.com:2222/myorg/myrepo.git

Part 5: Gitea Actions (CI/CD)

Gitea Actions uses GitHub Actions-compatible YAML. Enable it:

environment:
  GITEA__actions__ENABLED: "true"

Run an Act Runner

# Add to docker-compose.yml:
services:
  gitea-runner:
    image: gitea/act_runner:latest
    container_name: gitea_runner
    restart: unless-stopped
    volumes:
      - ./runner-config.yaml:/config.yaml
      - gitea_runner_data:/data
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      CONFIG_FILE: /config.yaml
      GITEA_INSTANCE_URL: "https://git.yourdomain.com"
      GITEA_RUNNER_REGISTRATION_TOKEN: "${RUNNER_TOKEN}"
      GITEA_RUNNER_NAME: "docker-runner"
      GITEA_RUNNER_LABELS: "ubuntu-latest:docker://node:20-bullseye"
# Get runner token from Gitea:
# Site Administration → Runners → Create new runner → copy token

# Register:
docker exec gitea_runner gitea-act-runner register \
  --instance https://git.yourdomain.com \
  --token YOUR_RUNNER_TOKEN \
  --name docker-runner \
  --labels ubuntu-latest:docker://node:20-bullseye

Example GitHub Actions-compatible workflow

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

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

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm test
      - run: npm run build

  docker:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build and push Docker image
        run: |
          docker build -t git.yourdomain.com/${{ gitea.repository }}:${{ gitea.sha }} .
          docker push git.yourdomain.com/${{ gitea.repository }}:${{ gitea.sha }}

Part 6: Container Registry

Gitea includes a container registry at git.yourdomain.com:

# Login:
docker login git.yourdomain.com -u username -p your-password

# Push:
docker tag myimage:latest git.yourdomain.com/username/myrepo:latest
docker push git.yourdomain.com/username/myrepo:latest

# Pull:
docker pull git.yourdomain.com/username/myrepo:latest

Part 7: Migrate from GitHub

Single repository

# Gitea UI: + → New Migration → GitHub
# Enter: GitHub repo URL + access token (for private repos)
# Gitea mirrors: issues, PRs, releases, labels, milestones

Bulk migration script

#!/bin/bash
# Migrate all repos from a GitHub org to Gitea

GITHUB_TOKEN="ghp_yourtoken"
GITHUB_ORG="your-github-org"
GITEA_URL="https://git.yourdomain.com"
GITEA_TOKEN="your-gitea-token"
GITEA_ORG="your-gitea-org"

# List all repos:
REPOS=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
  "https://api.github.com/orgs/${GITHUB_ORG}/repos?per_page=100" \
  | jq -r '.[].full_name')

for REPO in $REPOS; do
  echo "Migrating $REPO..."
  curl -s -X POST "${GITEA_URL}/api/v1/repos/migrate" \
    -H "Authorization: token ${GITEA_TOKEN}" \
    -H "Content-Type: application/json" \
    -d "{
      \"clone_addr\": \"https://github.com/${REPO}\",
      \"auth_token\": \"${GITHUB_TOKEN}\",
      \"uid\": 1,
      \"repo_name\": \"$(echo $REPO | cut -d'/' -f2)\",
      \"mirror\": false,
      \"issues\": true,
      \"pull_requests\": true,
      \"releases\": true,
      \"labels\": true,
      \"milestones\": true
    }"
done

Part 8: Organization and Team Access

Organization: mycompany
├── Team: Owners (full access)
│   └── Users: alice, bob
├── Team: Developers (write access)
│   ├── Users: charlie, diana
│   └── Repos: backend, frontend, infra
└── Team: Contractors (read access)
    ├── Users: external1
    └── Repos: frontend (read only)

Create via API:

# Create org:
curl -X POST "${GITEA_URL}/api/v1/orgs" \
  -H "Authorization: token ${TOKEN}" \
  -d '{"username": "mycompany", "visibility": "private"}'

# Create team:
curl -X POST "${GITEA_URL}/api/v1/orgs/mycompany/teams" \
  -H "Authorization: token ${TOKEN}" \
  -d '{"name": "Developers", "permission": "write", "units": ["repo.code", "repo.issues"]}'

Maintenance

# Update Gitea:
docker compose pull
docker compose up -d

# Backup:
# Stop Gitea first for consistent backup:
docker compose stop gitea
tar -czf gitea-backup-$(date +%Y%m%d).tar.gz \
  $(docker volume inspect gitea_gitea_data --format '{{.Mountpoint}}')
docker exec gitea-postgres-1 pg_dump -U gitea gitea \
  | gzip > gitea-db-$(date +%Y%m%d).sql.gz
docker compose start gitea

# Logs:
docker compose logs -f gitea

# Admin CLI:
docker exec -u git gitea gitea admin user create \
  --username alice --email alice@company.com --password secret --admin

# Gitea version:
docker exec gitea gitea --version

See all open source DevOps and Git tools at OSSAlt.com/categories/devops.

Comments