$docker.recipes
·11 min read·Updated February 2026

Self-Hosting Your Own Git Server with Gitea or Forgejo

Run your own Git server with Gitea or Forgejo using Docker Compose. Private repos, built-in CI/CD, and only 50MB of RAM — a practical alternative to GitHub and GitLab.

giteaforgejogitself-hostingdocker-compose

01Why Self-Host a Git Server?

GitHub is excellent. So why would you run your own Git server? For me, three reasons: unlimited private repositories with no per-user pricing, complete data sovereignty (my code never leaves my infrastructure), and the surprisingly low resource footprint. Gitea uses about 50-80MB of RAM — compare that to GitLab, which needs at least 4GB to even start. I started self-hosting Git because I wanted a private mirror of all my GitHub repositories. If GitHub goes down or changes its terms, I have a complete backup with issues, pull requests, and CI configuration. Over time, my self-hosted Gitea became my primary Git server for personal projects, and I push to GitHub only when I want something public. Both Gitea and Forgejo (a community fork of Gitea) provide 90% of what most developers need: repository hosting, pull requests, issues, a package registry, and built-in CI/CD with Actions-compatible workflows. The missing 10% is mostly GitHub's ecosystem — Actions marketplace, Copilot integration, and the social features like trending repos.

02Docker Compose Setup with PostgreSQL

Here's a production-ready setup. Gitea runs alongside PostgreSQL for the database — while Gitea supports SQLite, PostgreSQL handles concurrent access much better and is what I recommend for anything beyond a single-user setup.
[docker-compose.yml]
1services:
2 gitea:
3 image: gitea/gitea:1.23
4 environment:
5 - USER_UID=1000
6 - USER_GID=1000
7 - GITEA__database__DB_TYPE=postgres
8 - GITEA__database__HOST=db:5432
9 - GITEA__database__NAME=gitea
10 - GITEA__database__USER=gitea
11 - GITEA__database__PASSWD=${DB_PASSWORD}
12 - GITEA__server__ROOT_URL=https://git.example.com/
13 - GITEA__server__SSH_DOMAIN=git.example.com
14 - GITEA__server__SSH_PORT=2222
15 - GITEA__service__DISABLE_REGISTRATION=true
16 volumes:
17 - gitea-data:/data
18 - /etc/timezone:/etc/timezone:ro
19 - /etc/localtime:/etc/localtime:ro
20 ports:
21 - "3000:3000"
22 - "2222:22"
23 depends_on:
24 db:
25 condition: service_healthy
26 restart: unless-stopped
27
28 db:
29 image: postgres:16-alpine
30 environment:
31 - POSTGRES_USER=gitea
32 - POSTGRES_PASSWORD=${DB_PASSWORD}
33 - POSTGRES_DB=gitea
34 volumes:
35 - postgres-data:/var/lib/postgresql/data
36 healthcheck:
37 test: ["CMD", "pg_isready", "-U", "gitea"]
38 interval: 10s
39 timeout: 5s
40 retries: 5
41 restart: unless-stopped
42
43volumes:
44 gitea-data:
45 postgres-data:

For solo use or very small teams (2-3 people), SQLite works fine and simplifies the setup — just remove the db service and set DB_TYPE to sqlite3. I ran SQLite for my first year with zero issues. Switch to PostgreSQL when you notice search or concurrent push operations slowing down.

03Essential Configuration

Create a .env file for your secrets and basic configuration. The most important settings control registration (disable it after creating your admin account), SSH access, and email notifications.
[.env]
1# Database
2DB_PASSWORD=use-a-strong-random-password-here
3
4# Generate with: openssl rand -hex 32
5GITEA__security__SECRET_KEY=your-secret-key-here
6GITEA__security__INTERNAL_TOKEN=your-internal-token-here
7
8# Email notifications (optional)
9GITEA__mailer__ENABLED=true
10GITEA__mailer__PROTOCOL=smtps
11GITEA__mailer__SMTP_ADDR=smtp.example.com
12GITEA__mailer__SMTP_PORT=465
13GITEA__mailer__USER=gitea@example.com
14GITEA__mailer__PASSWD=email-password
15GITEA__mailer__FROM=gitea@example.com

04Built-In CI/CD with Gitea Actions

Gitea Actions is compatible with GitHub Actions workflows. You write the same YAML, use the same syntax, and many GitHub Actions work directly. The main difference is that you run your own runners — Gitea doesn't provide hosted runners. Setting up a runner is straightforward. Gitea provides a Docker-based runner that you add to your Compose stack. The runner picks up jobs from your Gitea instance and executes them in isolated Docker containers.
[.gitea/workflows/ci.yml]
1# .gitea/workflows/ci.yml — works like GitHub Actions
2name: CI
3on:
4 push:
5 branches: [main]
6 pull_request:
7 branches: [main]
8
9jobs:
10 test:
11 runs-on: ubuntu-latest
12 steps:
13 - uses: actions/checkout@v4
14 - uses: actions/setup-node@v4
15 with:
16 node-version: '22'
17 - run: npm ci
18 - run: npm test
19 - run: npm run build

The Gitea Actions runner consumes significant resources during builds. On a resource-constrained server, set concurrency limits (labels and capacity settings in the runner config) to prevent builds from overwhelming your system. I limit mine to 2 concurrent jobs on a 4-core VPS.

05Migrating from GitHub or GitLab

Gitea has a built-in migration tool that imports repositories from GitHub, GitLab, Gitea, Gogs, and other platforms. It doesn't just clone the Git history — it also imports issues, pull requests/merge requests, labels, milestones, releases, and wiki content. To migrate: go to New Repository → New Migration in Gitea's web UI, paste the source repository URL, provide an access token if it's a private repo, and select what to import. For GitHub, create a personal access token with repo scope. I migrated 40+ repositories from GitHub and the process took about 20 minutes total. Every issue, PR, and release was imported correctly, with original authors and dates preserved (shown as "migrated" comments). For ongoing mirroring, Gitea supports mirror repositories that automatically pull from an upstream source on a schedule. I use this to keep a local copy of critical dependencies and tools — if GitHub is down during a deploy, I can still pull images and tools from my local mirror.

06Day-to-Day Experience

After 18 months of daily use, here's my honest assessment: What works great: Git operations are fast — push, pull, and clone are as snappy as GitHub for repositories under 1GB. The web interface is clean and functional. Pull request reviews work well, with inline comments and approval workflows. The API is extensive and well-documented. RSS feeds for repository activity are a nice touch. What's good enough: The code search is decent but not as good as GitHub's. The project boards are basic compared to GitHub Projects. Package registry support exists but the documentation is sparse. What's missing: There's no equivalent to GitHub Copilot integration, no Dependabot-style automated security updates (though you can use Renovate), and the mobile experience is functional but not polished. The Actions ecosystem is smaller — many GitHub Actions work, but some need modifications for Gitea compatibility. Forgejo vs Gitea: Forgejo is a hard fork of Gitea created over governance concerns. Functionally, they're nearly identical today. Forgejo tends to be slightly more conservative with changes and has stronger community governance. Either choice is fine — I use Gitea, but Forgejo is an equally valid option (just change the Docker image to codeberg/forgejo).

About the Author

Frank Pegasus

DevOps engineer and self-hosting enthusiast with over a decade of experience running containerized workloads in production. Creator of docker.recipes.