Gitea: Self-Hosted Git Server for Your Homelab

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 version

Docker 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/gitea

Write 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: bridge

Critical 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 -d

Check logs for initialization:

docker compose logs -f gitea

Gitea 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.pub

Configure your local ~/.ssh/config for convenience:

Host gitea.yourdomain.com
    HostName gitea.yourdomain.com
    Port 2222
    User git
    IdentityFile ~/.ssh/id_rsa

Test 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.git

Gotcha #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/data

Nginx 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 nginx

Use Certbot to issue an HTTPS certificate:

sudo certbot certonly --webroot -w /var/www/letsencrypt -d gitea.yourdomain.com

Update ROOT_URL in your docker-compose.yml to https://gitea.yourdomain.com and restart:

docker compose restart gitea

Gitea 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.com

Register 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_runner

Create 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-runner

Run the runner in the background (or as a systemd service). For testing, run in foreground:

act_runner daemon

Now 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/tcp

Also check your firewall or ISP isn't blocking port 2222:

sudo ufw allow 2222/tcp

HTTP 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 -20

Also 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.com

Also 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.

Read more