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 →