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:
| Backend | Best for | Notes |
|---|---|---|
| SQLite | Single-user / small teams | Zero config; not suitable for high concurrency |
| PostgreSQL | Teams and production | Recommended; best performance |
| MySQL / MariaDB | Existing MySQL infra | Supported; 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
| Forge | Language | RAM (idle) | Actions CI | Package Registry | License |
|---|---|---|---|---|---|
| Gitea | Go | ~120 MB | Yes (act_runner) | Yes (11 types) | MIT |
| Forgejo | Go | ~120 MB | Yes (act_runner) | Yes | MIT |
| GitLab CE | Ruby/Go | ~2–4 GB | Yes (native) | Yes | MIT |
| Gogs | Go | ~50 MB | No | No | MIT |
| OneDev | Java | ~500 MB | Yes | Limited | MIT |
| Sourcehut | Python/Go | ~100 MB | Yes (builds.sr.ht) | No | AGPL |
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(or636for 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:
| Provider | Client ID source | Scope |
|---|---|---|
| GitHub | github.com/settings/developers | read:user,user:email |
| GitLab | gitlab.com/-/profile/applications | read_user |
| console.cloud.google.com | openid 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:
| Role | Repositories | Admin panel | Create repos |
|---|---|---|---|
| Owner | All in org | Yes | Yes |
| Admin (team) | Team repos | No | Team-scoped |
| Write (team) | Team repos | No | No |
| Read (team) | Team repos | No | No |
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
| Problem | Solution |
|---|---|
| Web UI returns 502 | Check docker logs gitea; verify DB connection env vars are correct |
| SSH clone fails | Confirm SSH_PORT matches the host port mapping; test with ssh -T -p 222 git@host |
| Actions workflow queued forever | Verify act_runner is running and registered; check runner labels match runs-on value |
| Package push 401 | Generate a token with package scope; use it as the registry password |
| Mirror sync not working | Check the mirror URL is reachable from the container; verify credentials for private repos |
| Admin password forgotten | docker 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.inicontrols every aspect: security, mailer, LFS, packages, and SSH- Gitea Actions uses GitHub Actions YAML syntax;
act_runnerruns 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