Matt Novak logo Matt Novak

Blog

Why I'm Moving from GitLab to Forgejo (and Not Looking Back)

2025-08-05 • 5 min
gitlab forgejo gitea self-hosting devops ci/cd

Why I'm Moving from GitLab to Forgejo (and Not Looking Back)🔗

TL;DR: GitLab is fantastic—but it's become massive. I want a forge that's small, fast, and easy to operate without eating a whole node's worth of RAM just to idle. Forgejo gives me the features I actually use, with a fraction of the footprint and fuss.


The problem: GitLab got heavy🔗

I've run self-hosted GitLab on and off for years. It does everything… which is also the problem. On a personal or small-team server, you end up running a constellation of services (Rails app, Sidekiq, Gitaly, Workhorse, Redis, PostgreSQL, Prometheus exporters, etc.). That's a lot to keep patched, backed up, and happy.

  • Memory footprint: too big for what I need.
  • Operational complexity: upgrades feel like little projects.
  • Blast radius: one component flaking can make the whole experience feel brittle.
  • "All-in-one" gravity: it nudges you to move everything into GitLab's way of doing things.

I don't want that anymore. I want a lean forge for repos, issues, wikis, and CI hooks—without a kitchen sink strapped to a jet engine.


Enter Forgejo🔗

Forgejo is a community-driven fork of Gitea focused on being open, small, and fast. That bias toward simplicity is exactly what I want:

  • Low footprint: single lightweight server plus a database (SQLite or Postgres) and optional Redis.
  • Batteries included: repos, issues, PRs, code search, releases, a Packages registry (including container images), Git LFS, OAuth, webhooks.
  • Modern CI story: "Forgejo Actions" (GitHub Actions–compatible) + runners, or plug in Woodpecker/Drone if you prefer.
  • Straightforward ops: easy Docker Compose; quick backups; fast restore.

This isn't about chasing novelty—it's about choosing a tool proportionate to the job.


What I actually need from a forge🔗

My day-to-day looks like this:

  • Private and public repos
  • Pull requests with code review
  • Issue tracking + labels + milestones
  • Release artifacts
  • A small, sane CI (build/tests/lint for Rust, Go, Python)
  • Container image hosting (nice to have)
  • Webhooks to my own services

Forgejo checks these boxes without making my server feel like it's running a mini cloud provider.


My deployment (Docker Compose)🔗

I'm going with Postgres for durability and future growth, but you can happily run SQLite for ultra-minimal setups.

# docker-compose.yml
services:
  forgejo:
    image: codeberg.org/forgejo/forgejo:latest
    container_name: forgejo
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - FORGEJO__server__DOMAIN=git.mydomain.tld
      - FORGEJO__server__ROOT_URL=https://git.mydomain.tld/
      - FORGEJO__database__DB_TYPE=postgres
      - FORGEJO__database__HOST=db:5432
      - FORGEJO__database__NAME=forgejo
      - FORGEJO__database__USER=forgejo
      - FORGEJO__database__PASSWD=${FORGEJO_DB_PASSWORD}
      - FORGEJO__cache__ENABLED=true
      - FORGEJO__service__DISABLE_REGISTRATION=true
      - FORGEJO__server__SSH_DOMAIN=git.mydomain.tld
      - FORGEJO__server__START_SSH_SERVER=true
      - FORGEJO__server__BUILTIN_SSH_SERVER_USER=git
      - FORGEJO__actions__ENABLED=true
    volumes:
      - ./data:/data
    ports:
      - "3000:3000"  # HTTP (behind reverse proxy)
      - "222:22"     # SSH
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: postgres:16
    environment:
      - POSTGRES_DB=forgejo
      - POSTGRES_USER=forgejo
      - POSTGRES_PASSWORD=${FORGEJO_DB_PASSWORD}
    volumes:
      - ./pgdata:/var/lib/postgresql/data
    restart: unless-stopped

Reverse-proxy with Caddy/Traefik/Nginx, add TLS, and you're done.


CI without the bloat: Forgejo Actions🔗

Forgejo supports an Actions runner that's compatible with a big chunk of the GitHub Actions ecosystem. That means I can keep simple YAML workflows per repo—no need to wire up a giant CI system.

Example: Rust workflow

# .forgejo/workflows/rust.yml
name: rust-ci

on:
  push:
  pull_request:

jobs:
  test:
    runs-on: docker
    steps:
      - uses: actions/checkout@v4
      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable
      - name: Cache cargo
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            target
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
      - name: Build
        run: cargo build --locked --all-targets
      - name: Test
        run: cargo test --locked --all -- --nocapture

Example: Go workflow

# .forgejo/workflows/go.yml
name: go-ci

on:
  push:
  pull_request:

jobs:
  test:
    runs-on: docker
    steps:
      - uses: actions/checkout@v4
      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.22'
      - name: Cache modules
        uses: actions/cache@v4
        with:
          path: |
            ~/go/pkg/mod
            ~/.cache/go-build
          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
      - name: Test
        run: go test ./... -v

Example: Python workflow

# .forgejo/workflows/python.yml
name: python-ci

on:
  push:
  pull_request:

jobs:
  test:
    runs-on: docker
    steps:
      - uses: actions/checkout@v4
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - name: Install deps
        run: |
          python -m pip install --upgrade pip wheel
          pip install -r requirements.txt
      - name: Lint & Test
        run: |
          python -m pip install ruff pytest
          ruff check .
          pytest -q

If I ever outgrow this, I can hang Woodpecker or Drone off webhooks—but I doubt I will.


Migration plan🔗

I want the move to be boring and reversible. My steps:

  1. Inventory List all groups, repos, CI pipelines, container images, and LFS usage on GitLab. Decide what stays/goes.

  2. Export projects

    • Git: git clone --mirror for each repo (preserves refs)
    • Issues/PRs: GitLab exports per project; some details may require a script or intermediary (there are community tools to convert issues—good enough for small projects).
    • Packages/Container images: pull/push to the new registry, or rebuild and republish.
  3. Provision Forgejo Stand up the Docker Compose stack, configure users, orgs, and teams, set SSH keys and OAuth providers.

  4. Import repos Push mirrored repos to Forgejo. Re-create branches and protections, labels, and milestones (I keep a small script for labels).

  5. Set up CI Add .forgejo/workflows/*.yml to each project. Install a runner on my build node(s).

  6. Cutover Freeze GitLab writes, update remotes (git remote set-url origin), flip webhook endpoints, and enable the registry. Announce the change to collaborators.

  7. Backup & retire Snapshot the Forgejo data volume and Postgres. Keep GitLab read-only for a short grace period, then shut it down.


Backups (simple and solid)🔗

  • Postgres: nightly pg_dump + weekly base backup (or just snapshot the volume if your storage supports it).
  • Forgejo data dir: tar/rsync the /data volume (repos, attachments, avatars, LFS, actions logs).
  • Off-site: encrypted push to object storage.
  • Restore drill: occasionally restore to a throwaway VM and verify logins, repos, and issues.

Small tools are easier to back up—and easier to actually restore when it matters.


Trade-offs I accept🔗

  • GitLab's integrated everything is hard to beat if you truly use it all. I don't.
  • Some "enterprise-y" features may need third-party tools or simple scripts in Forgejo. That's fine by me.
  • Actions compat isn't 1:1 with GitHub-land, but it's good enough for my pipelines.

In return, I get a forge that starts fast, stays fast, and doesn't demand a dedicated ops sprint every time I upgrade.


Closing🔗

I still respect GitLab. It's a powerhouse. But for my work, Forgejo is the right size: small, friendly, and focused. I spend more time coding and less time babysitting infra—and that's the whole point.

If you're running a personal forge or a small team and your GitLab instance feels like a space heater, try Forgejo. Your CPU—and your brain—will cool off.

AI-assisted writing

I draft and edit all articles myself, and I use AI as an assistant for outlining, phrasing, and cleanup. Curious how I use it—and where I draw the lines?