Gitea: Self-Hosted Git Server for Your Homelab
Why Run Gitea: The Self-Hosted Git Server Your Homelab Actually Needs
You're managing multiple repositories across your homelab infrastructure, and GitHub's free tier isn't cutting it anymore—or you want your git server running on your own hardware without cloud vendor lock-in. Gitea is a lightweight, self-hosted Git service that gives you the entire GitHub-like experience (pull requests, issues, Actions CI/CD) with a footprint small enough to run on a Raspberry Pi or alongside existing services on your VM.
This post covers a production-grade Gitea setup: Docker deployment on Ubuntu 24.04 LTS, SSH access configuration, GitHub Actions compatibility, automatic mirroring to GitHub as a backup, and reverse proxy integration with Nginx. I'm writing this from hands-on experience running Gitea 1.22.3 on my T5810 homelab box alongside Nextcloud and Immich.
Prerequisites: Software Versions and System Requirements
Before you start, confirm your environment meets these specifications:
- OS: Ubuntu 24.04.1 LTS (or Debian 12+)
- Docker: 26.1.3 or later with Docker Compose V2
- RAM: Minimum 1GB, recommended 2GB+ for CI/CD pipelines
- Storage: 20GB+ SSD (depending on repository size)
- Network: Static IP, port 22 (SSH) and 3000 (HTTP) available or configurable
- Gitea version: 1.22.3 (latest as of this writing)
Verify Docker installation:
docker --version
docker compose versionDocker Deployment: Container-First Setup
Use Docker Compose to manage Gitea alongside a PostgreSQL database. This is more robust than SQLite for multiple concurrent users and CI/CD runners.
Create a deployment directory:
mkdir -p /opt/gitea/{data,config}
cd /opt/giteaWrite your docker-compose.yml:
version: '3.8'
services:
postgres:
image: postgres:16-alpine
container_name: gitea-postgres
environment:
POSTGRES_USER: gitea
POSTGRES_PASSWORD: your_secure_password_here
POSTGRES_DB: gitea
volumes:
- ./data/postgres:/var/lib/postgresql/data
restart: unless-stopped
networks:
- gitea-network
gitea:
image: gitea/gitea:1.22.3
container_name: gitea
depends_on:
- postgres
environment:
- ROOT_URL=https://gitea.yourdomain.com
- SSH_DOMAIN=gitea.yourdomain.com
- SSH_PORT=2222
- DB_TYPE=postgres
- DB_HOST=postgres:5432
- DB_NAME=gitea
- DB_USER=gitea
- DB_PASSWD=your_secure_password_here
volumes:
- ./data/gitea:/data
- ./config/app.ini:/data/gitea/conf/app.ini
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "2222:22"
restart: unless-stopped
networks:
- gitea-network
networks:
gitea-network:
driver: bridgeCritical gotcha #1: The SSH port mapping uses 2222 on the host because port 22 is likely already occupied by your main SSH daemon. Update SSH_PORT in the environment and tell users to clone with ssh://[email protected]:2222/user/repo.git.
Start the stack:
docker compose up -dCheck logs for initialization:
docker compose logs -f giteaGitea will run database migrations on first startup—wait 30 seconds before accessing the web UI at http://localhost:3000.
SSH Key Management and Git Operations
Gitea's SSH access depends on proper key handling. The container exposes SSH on port 2222; the Gitea process inside handles authentication against its user database.
Add your SSH key through the web UI: login → Settings → SSH Keys → Add Key. Paste your public key:
cat ~/.ssh/id_rsa.pubConfigure your local ~/.ssh/config for convenience:
Host gitea.yourdomain.com
HostName gitea.yourdomain.com
Port 2222
User git
IdentityFile ~/.ssh/id_rsaTest SSH connectivity:
ssh -T [email protected]You should see: "Hi there! You've successfully authenticated, but Gitea does not provide shell access."
Now clone a repository (create one first via the web UI):
git clone ssh://[email protected]:2222/username/reponame.gitGotcha #2: If SSH fails with "Permission denied (publickey)", check that your SSH key is added to Gitea's user account, not the admin account. Also verify the Gitea container has the right permissions on /data—it runs as UID 1000 by default. Fix with:
sudo chown -R 1000:1000 /opt/gitea/dataNginx Reverse Proxy Configuration
Expose Gitea behind Nginx with HTTPS (Let's Encrypt) so you're not relying on plaintext HTTP internally or exposing the container port directly.
Create /etc/nginx/sites-available/gitea:
upstream gitea {
server localhost:3000;
}
server {
listen 80;
server_name gitea.yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name gitea.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/gitea.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/gitea.yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
client_max_body_size 50M;
location / {
proxy_pass http://gitea;
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_redirect off;
}
}
Enable the site and test Nginx:
sudo ln -s /etc/nginx/sites-available/gitea /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxUse Certbot to issue an HTTPS certificate:
sudo certbot certonly --webroot -w /var/www/letsencrypt -d gitea.yourdomain.comUpdate ROOT_URL in your docker-compose.yml to https://gitea.yourdomain.com and restart:
docker compose restart giteaGitea Actions: CI/CD Pipeline Support
Gitea 1.22+ includes Actions, a GitHub Actions-compatible CI/CD system. Enable it in your app configuration.
Edit or create /opt/gitea/config/app.ini:
[actions]
ENABLED = true
DEFAULT_ACTIONS_URL = https://github.comRegister a runner to execute jobs. Install the Gitea runner binary on your homelab box (or a separate machine):
wget https://dl.gitea.io/act_runner/0.2.10/act_runner-0.2.10-linux-amd64.tar.gz
tar xzf act_runner-0.2.10-linux-amd64.tar.gz
sudo mv act_runner /usr/local/bin/
chmod +x /usr/local/bin/act_runnerCreate a runner registration token in Gitea: Admin → Actions → Runners → Create Registration Token. Then register:
act_runner register --no-interactive --instance https://gitea.yourdomain.com --token your_registration_token --name homelab-runnerRun the runner in the background (or as a systemd service). For testing, run in foreground:
act_runner daemonNow create a workflow file in your repository: .gitea/workflows/test.yml
name: Test
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: |
echo "Running tests..."
bash ./test.sh
Push and watch your pipeline execute via the Actions tab in the repository.
GitHub Mirroring for Backup and Redundancy
Mirror your Gitea repositories to GitHub as a disaster recovery strategy. Gitea can push to GitHub automatically on every commit.
On GitHub, create a personal access token (Settings → Developer Settings → Personal Access Tokens → Tokens (classic)) with repo and delete_repo scopes.
In Gitea, go to repository Settings → Mirror Settings. Enable the mirror and configure:
- Push URL:
https://your_github_username:[email protected]/your_github_username/reponame.git - Mirror Interval: 1 hour (default is reasonable)
Verify the first push succeeds: it'll show in the Mirror Settings panel once synced. You can also trigger manually via the repository web UI.
This gives you automatic backups and lets your collaborators clone from GitHub if Gitea ever goes down.
Common Issues and Troubleshooting
SSH Connection Timeout
If ssh -T [email protected] hangs or times out, verify the container's SSH port is exposed correctly:
docker compose ps
# Should show: gitea ... 0.0.0.0:2222->22/tcpAlso check your firewall or ISP isn't blocking port 2222:
sudo ufw allow 2222/tcpHTTP 502 Bad Gateway from Nginx
Gitea takes 30+ seconds to initialize on first startup. Wait and try again. If it persists, check the Gitea container logs:
docker compose logs gitea | tail -20Also verify the upstream in your Nginx config points to the correct Docker host IP. If using host networking, change localhost:3000 to 127.0.0.1:3000.
Actions Runner Not Connecting
The runner must be able to reach your Gitea instance by the URL you registered it with. If you registered with https://gitea.yourdomain.com, the runner machine needs DNS resolution and network access to that domain. Test from the runner host:
curl -I https://gitea.yourdomain.comAlso ensure the runner has Docker available for executing job containers (act_runner uses Docker by default).
What You Now Have
You're running a production-grade, self-hosted Git server with SSH access, GitHub Actions-compatible CI/CD, automatic GitHub mirroring for backups, and HTTPS termination via Nginx. Your repositories are fully under your control, accessible from your homelab network, and backed up to GitHub as a secondary location.
Next steps: set up organizational teams for access control, configure webhooks to trigger external services (home automation, alerting), or integrate Gitea with your reverse proxy's authentication system using oauth2-proxy for SSO.
Related reading: SSH key rotation strategies for homelab services, setting up a private container registry alongside Gitea for GitOps workflows, and integrating Gitea with Woodpecker CI if you prefer a more lightweight alternative to Actions.
Disclosure: This post contains affiliate links. If you purchase through these links, we may earn a small commission at no extra cost to you. We only recommend services we've tested and trust.