TL;DR — Quick Summary

Deploy Gitea as a self-hosted Git forge. Covers Docker Compose, systemd, Gitea Actions, LDAP, OAuth2, package registries, mirroring, and Caddy reverse proxy.

Gitea is a lightweight, self-hosted Git forge written in Go that ships as a single binary and runs comfortably on hardware as modest as a Raspberry Pi. If you have ever wanted full control over your source code, CI/CD pipelines, issue tracking, and package registries without paying GitHub or GitLab cloud prices — and without the operational weight of a GitLab CE instance — Gitea is the answer. This guide covers everything from a production Docker Compose deployment to Gitea Actions CI, OAuth2 SSO, package registries, and repository mirroring.

Prerequisites

  • Docker and Docker Compose v2 installed on the host
  • A domain name pointed at the server (for TLS with Caddy)
  • At least 512 MB RAM and 2 GB disk (Gitea itself needs ~150 MB; reserve the rest for PostgreSQL and repos)
  • Basic familiarity with Docker volumes and YAML syntax
  • Port 3000 (or 80/443 via reverse proxy) reachable from your network

Gitea Architecture

Gitea is a Go application compiled to a single binary with no runtime dependencies. It bundles a web server, Git hooks, SSH daemon, and background workers. Storage options:

BackendBest forNotes
SQLiteSingle-user / small teamsZero config; not suitable for high concurrency
PostgreSQLTeams and productionRecommended; best performance
MySQL / MariaDBExisting MySQL infraSupported; PostgreSQL preferred

Repository data is stored as bare Git repos on disk under $GITEA_WORK_DIR/repositories. LFS objects go to $GITEA_WORK_DIR/lfs. Attachments, avatars, and packages use $GITEA_WORK_DIR/data.

Gitea vs Forgejo vs Alternatives

ForgeLanguageRAM (idle)Actions CIPackage RegistryLicense
GiteaGo~120 MBYes (act_runner)Yes (11 types)MIT
ForgejoGo~120 MBYes (act_runner)YesMIT
GitLab CERuby/Go~2–4 GBYes (native)YesMIT
GogsGo~50 MBNoNoMIT
OneDevJava~500 MBYesLimitedMIT
SourcehutPython/Go~100 MBYes (builds.sr.ht)NoAGPL

Forgejo is API-compatible with Gitea and runners are interchangeable. Migration between the two is seamless. GitLab CE is far more feature-complete but requires 10–20× more resources.


Step 1: Deploy with Docker Compose and PostgreSQL

Create a project directory and a docker-compose.yml:

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=db:5432
      - GITEA__database__NAME=gitea
      - GITEA__database__USER=gitea
      - GITEA__database__PASSWD=changeme
    volumes:
      - gitea-data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "3000:3000"
      - "222:22"
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    container_name: gitea-db
    restart: unless-stopped
    environment:
      - POSTGRES_USER=gitea
      - POSTGRES_PASSWORD=changeme
      - POSTGRES_DB=gitea
    volumes:
      - postgres-data:/var/lib/postgresql/data

volumes:
  gitea-data:
  postgres-data:
docker compose up -d

Open http://your-server:3000 in a browser. The web installer pre-fills database settings from environment variables — just set your base URL, admin username, admin email, and admin password, then click Install. Gitea initialises the database schema and redirects you to the dashboard in seconds.


Step 2: Configure app.ini

After the installer runs, app.ini lives at gitea-data/_volumes_/gitea/conf/app.ini (on the host, via the Docker volume). Edit it to harden the configuration:

[server]
DOMAIN           = git.example.com
ROOT_URL         = https://git.example.com/
HTTP_PORT        = 3000
SSH_DOMAIN       = git.example.com
SSH_PORT         = 22
SSH_LISTEN_PORT  = 22
DISABLE_SSH      = false
LFS_START_SERVER = true

[repository]
DEFAULT_BRANCH          = main
ENABLE_PUSH_CREATE_USER = false
DEFAULT_PRIVATE         = private

[security]
INSTALL_LOCK              = true
SECRET_KEY                = <generate-with-openssl-rand-hex-32>
INTERNAL_TOKEN            = <generate-with-gitea-generate-secret>
MIN_PASSWORD_LENGTH       = 12
PASSWORD_COMPLEXITY       = lower,upper,digit,spec

[service]
DISABLE_REGISTRATION              = false
REQUIRE_SIGNIN_VIEW               = false
REGISTER_EMAIL_CONFIRM            = true
ENABLE_NOTIFY_MAIL                = true
ALLOW_ONLY_EXTERNAL_REGISTRATION  = false

[mailer]
ENABLED   = true
SMTP_ADDR = smtp.example.com
SMTP_PORT = 587
FROM      = "Gitea <gitea@example.com>"
USER      = gitea@example.com
PASSWD    = `your-smtp-password`

Restart to apply:

docker compose restart gitea

Step 3: Repository Management

Git LFS

LFS is enabled via LFS_START_SERVER = true in [server]. Users configure their local client once:

git lfs install
git lfs track "*.psd" "*.zip" "*.tar.gz"

Gitea stores LFS objects in $GITEA_WORK_DIR/data/lfs. Back this directory up separately from the database.

Repository Mirroring

Pull mirrors let Gitea clone and periodically sync from GitHub, GitLab, or any Git remote. In the web UI: New Repository → Migrate. Select the source (GitHub, GitLab, Gitea, Bitbucket, Gogs, or plain Git URL). Tick This repository will be a mirror and set the sync interval (minimum 10 minutes).

Push mirrors send every commit to a remote automatically. Configure under Repository Settings → Mirror Settings → Push Mirrors.

Protected Branches and Merge Strategies

Under Repository Settings → Branches → Add Branch Protection Rule:

  • Require pull request reviews — set minimum approvals
  • Require status checks — block merge if Actions workflow fails
  • Restrict push — whitelist teams or users
  • Merge strategies — enable/disable merge commit, squash, and rebase per repository

Step 4: Gitea Actions CI

Gitea Actions is the built-in CI system using the same YAML syntax as GitHub Actions.

Register an act_runner

Download the latest act_runner binary for your OS from https://gitea.com/gitea/act_runner/releases. On the Gitea admin panel go to Site Administration → Actions → Runners → Create new runner token.

# Register the runner
act_runner register \
  --instance https://git.example.com \
  --token <runner-token> \
  --name my-runner \
  --labels ubuntu-latest:docker://node:20-bullseye

# Start as a service
act_runner daemon

Example Workflow

# .gitea/workflows/build.yml
name: Build and Test

on:
  push:
    branches: [main]
  pull_request:

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

  docker:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build Docker image
        run: docker build -t myapp:${{ gitea.sha }} .
      - name: Push to Gitea registry
        run: |
          docker login git.example.com -u ${{ secrets.REGISTRY_USER }} -p ${{ secrets.REGISTRY_TOKEN }}
          docker push git.example.com/myorg/myapp:${{ gitea.sha }}

Most GitHub Actions marketplace actions work in Gitea Actions because act_runner uses the same runner protocol. Actions referencing github.com context variables need minimal adjustments (github.*gitea.*).


Step 5: User Management and Authentication

Local Accounts and 2FA

Users self-register (or admins create accounts). 2FA via TOTP is available in User Settings → Security → Two-Factor Authentication. Site admins can mandate 2FA org-wide via the admin panel.

LDAP / Active Directory

Site Administration → Identity & Access → Authentication Sources → Add Authentication Source → LDAP (via BindDN):

  • Host: ldap.example.com
  • Port: 389 (or 636 for LDAPS)
  • Bind DN: cn=gitea,ou=service-accounts,dc=example,dc=com
  • User Search Base: ou=users,dc=example,dc=com
  • User Filter: (&(objectClass=person)(sAMAccountName=%s))
  • Email attribute: mail
  • Admin filter (optional): (memberOf=cn=git-admins,ou=groups,dc=example,dc=com)

OAuth2 Providers

Add GitHub, GitLab, or Google login under Authentication Sources → Add Authentication Source → OAuth2:

ProviderClient ID sourceScope
GitHubgithub.com/settings/developersread:user,user:email
GitLabgitlab.com/-/profile/applicationsread_user
Googleconsole.cloud.google.comopenid email profile

Users can link external OAuth2 accounts to existing local accounts from User Settings → Security → Linked Accounts.


Step 6: Caddy Reverse Proxy with TLS

Add this to your Caddyfile:

git.example.com {
    reverse_proxy gitea:3000

    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Frame-Options "SAMEORIGIN"
        X-Content-Type-Options "nosniff"
    }
}

Run Caddy in the same Docker Compose network:

  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy-data:/data
    networks:
      - gitea-net

Caddy fetches Let’s Encrypt certificates automatically on first request to the domain.


Organizations, Teams, and Permissions

Gitea’s permission model mirrors GitHub’s:

RoleRepositoriesAdmin panelCreate repos
OwnerAll in orgYesYes
Admin (team)Team reposNoTeam-scoped
Write (team)Team reposNoNo
Read (team)Team reposNoNo

Create organizations via the + menu → New Organization. Create teams inside the org and assign repositories and members. Fine-grained repository permissions override team defaults.

Gitea Packages Registry

Enable the package registry in app.ini:

[packages]
ENABLED = true

Supported package types: npm, PyPI, Maven, NuGet, Docker/OCI, Composer, Cargo, Conan, Helm, RubyGems, and Alpine/Debian APT.

npm example

npm config set registry https://git.example.com/api/packages/myorg/npm/
npm config set //git.example.com/api/packages/myorg/npm/:_authToken <token>
npm publish

Docker/OCI registry

docker login git.example.com
docker tag myapp:latest git.example.com/myorg/myapp:latest
docker push git.example.com/myorg/myapp:latest

The container registry is OCI-compliant. Image signing with Cosign works by pushing the signature manifest to the same registry endpoint.

Webhooks and REST API

Gitea’s REST API v1 is documented at https://git.example.com/api/swagger. Every resource (repos, issues, PRs, branches, releases) has full CRUD endpoints compatible with the Gitea/GitHub API shape.

Webhooks are configured per-repository or org-wide under Settings → Webhooks → Add Webhook. Supported events include push, pull request, issue, release, registry package, and workflow run. Payload format matches GitHub webhooks for easy integration with external CI tools.

# Create a repo via API
curl -X POST https://git.example.com/api/v1/user/repos \
  -H "Authorization: token <your-token>" \
  -H "Content-Type: application/json" \
  -d '{"name":"my-project","private":true,"default_branch":"main"}'

Backup and Restore

# Inside the container
docker exec -u git gitea gitea dump -c /data/gitea/conf/app.ini \
  -t /tmp --type tar.gz

# Copy the archive out
docker cp gitea:/tmp/gitea-dump-*.tar.gz ./backups/

The dump includes the database export, repositories, LFS data, attachments, and app.ini. For PostgreSQL, also take a pg_dump separately for point-in-time restore capability.

To restore: extract the archive, restore the database, copy repositories to the correct path, and update app.ini if the domain changed.

Migration from GitHub, GitLab, or Gogs

Site Administration → Migrations (or New Repository → Migrate) supports full import preserving:

  • Issues and comments
  • Pull requests (as issues if branch is gone)
  • Labels and milestones
  • Releases and release assets
  • Wiki pages

For GitHub: generate a fine-grained PAT with repo read scope. For large repos (>1 GB), migrate in off-peak hours as LFS migration is sequential.

Gogs-to-Gitea migration is seamless: Gitea reads Gogs’s database directly. Stop Gogs, point Gitea at the same database and repository directory, and run Gitea — it auto-migrates the schema.

Gotchas and Edge Cases

SSH port conflict — If port 22 is already used by the host SSH daemon, map Gitea’s SSH to a different host port (e.g., 222:22) and update SSH_PORT in app.ini so clone URLs show the correct port.

act_runner Docker-in-Docker — For workflows that build Docker images, the runner needs access to the Docker socket. Mount /var/run/docker.sock into the runner container and set privileged: true. Be aware of the security implications in multi-tenant setups.

LFS storage growth — LFS objects are never garbage-collected automatically. Schedule a periodic gitea admin repo-lfs-prune or set object storage lifecycle rules if using S3-compatible storage.

Email confirmation loop — If SMTP is misconfigured, users get stuck in unconfirmed state. Admins can bypass by setting REGISTER_EMAIL_CONFIRM = false temporarily or manually confirming users via the admin panel.

Forgejo compatibility — Gitea and Forgejo share the same app.ini format and database schema through version parity. Migrating from Gitea to Forgejo is documented and supported; the reverse is also possible through the same process.

Troubleshooting

ProblemSolution
Web UI returns 502Check docker logs gitea; verify DB connection env vars are correct
SSH clone failsConfirm SSH_PORT matches the host port mapping; test with ssh -T -p 222 git@host
Actions workflow queued foreverVerify act_runner is running and registered; check runner labels match runs-on value
Package push 401Generate a token with package scope; use it as the registry password
Mirror sync not workingCheck the mirror URL is reachable from the container; verify credentials for private repos
Admin password forgottendocker exec -it gitea gitea admin user change-password -u admin -p newpassword

Summary

  • Gitea runs on ~120 MB RAM as a single Go binary — far lighter than GitLab CE
  • Docker Compose with PostgreSQL is the recommended production deployment
  • app.ini controls every aspect: security, mailer, LFS, packages, and SSH
  • Gitea Actions uses GitHub Actions YAML syntax; act_runner runs jobs in Docker containers
  • LDAP, OAuth2 (GitHub/GitLab/Google), and TOTP 2FA are built-in
  • Package registries support npm, PyPI, Maven, NuGet, Docker/OCI, and more
  • Repository mirroring (push and pull) keeps external repos in sync automatically
  • Forgejo is a drop-in API-compatible alternative with community-focused governance