Jake Groszewski

Blog

Self-Host Your Own GitHub Alternative: Weekend Guide to Running Gitea or Forgejo on a $5 VPS

GitHub isn't going anywhere, but neither is the nagging feeling that you're handing over your private repos, experimental side projects, and internal tooling to a platform whose pricing and terms can change overnight. If you've been putting off standing up your own Git server because it sounds like a weekend-eating nightmare, the current state of tools like Gitea and Forgejo makes it genuinely approachable in an afternoon.

This guide walks through the whole setup: picking a VPS, choosing between Gitea and Forgejo, wiring up Docker Compose, adding HTTPS, configuring backups, and hooking in a basic CI runner. If you work primarily in Python or Django, there are a few specific notes along the way.

Gitea or Forgejo: Which One?

Forgejo is a community-maintained fork of Gitea that split off in late 2022 over governance concerns. Both projects ship pull requests, issues, wikis, labels, milestones, org management, and a full web UI. They're close enough in features that the choice mostly comes down to philosophy.

Gitea is backed by a commercial entity (Gitea Ltd.) and tends to ship features faster. Forgejo is developed entirely in the open under a non-profit structure, which appeals to developers who care about that kind of thing. For a personal or small-team setup, either works. I lean toward Forgejo for private infrastructure precisely because it's not tied to a company that could change direction.

Both are lightweight enough to run on a 1 GB RAM VPS without breaking a sweat.

VPS Setup

A $5-6/month instance from Hetzner, DigitalOcean, or Vultr is all you need. Ubuntu 22.04 LTS is a reasonable base. Once you've SSH'd in:

apt update && apt upgrade -y
apt install -y docker.io docker-compose-plugin ufw
ufw allow OpenSSH
ufw allow 80
ufw allow 443
ufw enable

Point a subdomain at your server's IP before you go any further. Something like git.yourdomain.com. Caddy (used below) will handle cert provisioning automatically, but it needs DNS to resolve first.

Docker Compose Configuration

Create a working directory and drop in a docker-compose.yml. The setup below uses Forgejo, but swapping in Gitea is one line change (replace the image reference).

version: "3.8"

services:
  forgejo:
    image: codeberg.org/forgejo/forgejo:7
    container_name: forgejo
    restart: unless-stopped
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - FORGEJO__database__DB_TYPE=sqlite3
      - FORGEJO__server__DOMAIN=git.yourdomain.com
      - FORGEJO__server__ROOT_URL=https://git.yourdomain.com/
      - FORGEJO__server__HTTP_PORT=3000
      - FORGEJO__security__SECRET_KEY=${SECRET_KEY}
      - FORGEJO__security__INTERNAL_TOKEN=${INTERNAL_TOKEN}
    volumes:
      - forgejo_data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "127.0.0.1:3000:3000"

  caddy:
    image: caddy:2-alpine
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config

volumes:
  forgejo_data:
  caddy_data:
  caddy_config:

Create a .env file next to it with secrets generated from your shell, not typed by hand:

echo "SECRET_KEY=$(openssl rand -hex 32)" >> .env
echo "INTERNAL_TOKEN=$(openssl rand -hex 32)" >> .env

This matches the Docker-first best practice of using environment-based secrets rather than hard-coding credentials anywhere in your config files. Especially relevant when you're exposing this on the public internet.

Add a minimal Caddyfile for automatic HTTPS:

git.yourdomain.com {
    reverse_proxy forgejo:3000
}

Spin it up:

docker compose up -d

Visit https://git.yourdomain.com and you'll land on the initial setup wizard. The defaults are fine for SQLite on a personal instance. Create your admin account, and you're up.

SSH Git Access

HTTPS cloning works out of the box, but SSH is cleaner for daily use. You'll need to expose port 22 or a custom SSH port from the container. The simplest approach on a VPS where you're already using port 22 for server SSH is to move your server SSH to a different port and reclaim 22 for Forgejo.

Alternatively, run Forgejo SSH on 2222 and add Port 2222 to your local ~/.ssh/config for that host. Either works.

Backups

SQLite makes backups trivially easy. A cron job that copies the data volume to object storage (Backblaze B2, R2, whatever you prefer) is enough:

#!/bin/bash
DATETIME=$(date +%Y%m%d_%H%M%S)
docker exec forgejo forgejo admin dump -c /data/gitea/conf/app.ini --file /tmp/forgejo-backup.zip
docker cp forgejo:/tmp/forgejo-backup.zip /backups/forgejo-$DATETIME.zip
# then push to your object storage of choice

Schedule this with cron daily. A week's worth of backups at typical repo sizes is well under 1 GB.

CI Runners

Both Gitea and Forgejo ship an Actions-compatible runner that understands a subset of GitHub Actions syntax. This means you can run Python tests, lint your Django projects, or build artifacts using workflows that are structurally identical to what you'd write for GitHub Actions.

Add a runner to your compose file:

  runner:
    image: code.forgejo.org/forgejo/runner:3
    container_name: forgejo_runner
    restart: unless-stopped
    depends_on:
      - forgejo
    volumes:
      - runner_data:/data
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - FORGEJO_INSTANCE_URL=http://forgejo:3000
      - FORGEJO_RUNNER_TOKEN=${RUNNER_TOKEN}

Generate a runner token from the admin panel under Site Administration > Actions, add it to your .env, and restart. A basic Python workflow looks like this:

name: Test

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v4
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - run: pytest

This works for Django apps, data pipeline scripts, Dash apps, or any Python project without modification.

Building a Broader Self-Hosted Stack

Once your Git server is running, it's natural to start thinking about what else you can attach to it. One pattern that comes up repeatedly in self-hosting communities is building a cohesive toolchain where your Git server, documentation, artifact storage, and other internal tools all live on the same infra, completely independent of third-party SaaS. You can run Gitea/Forgejo alongside a private file storage endpoint, an internal docs wiki, or even a package index for Python packages.

For sensitive projects, putting your Git service behind a VPN like WireGuard or a zero-trust access proxy (Cloudflare Access, Tailscale Funnel) limits your attack surface significantly. Not every project needs to be publicly reachable.

A Few Honest Caveats

Forgejo and Gitea cover maybe 80% of what most developers actually use on GitHub. Advanced features like integrated Dependabot-style security scanning or deeply integrated package registries for every ecosystem are thinner. If your team depends heavily on the GitHub marketplace for third-party integrations, you'll feel some friction.

For personal repos, Django project hosting, data science experiments, or small team workflows, the tradeoff is completely reasonable. You own the data, the uptime is yours to manage, and the monthly cost is effectively a rounding error.

The self-hosting wave isn't slowing down, and running your own Git server is one of the more practical on-ramps to owning your own developer infrastructure.