Why Forgejo

GitHub is an excellent service right up until it isn't: a flagged account, a deleted repo, a DMCA strike on a fork you depended on, or just the creeping feeling that your code lives on someone else's computer. Git itself is decentralized — the forge around it doesn't have to be a corporate platform.

Forgejo is a self-hosted git forge: issues, pull requests, wikis, CI webhooks, a package and container registry, even Actions-compatible runners. It's the community fork of Gitea, and it runs comfortably on a single 1 vCPU / 1 GB RAM VPS. This guide walks through a production-ready install: a hardened binary setup, automatic HTTPS, backups, and the upgrade path that won't bite you later.

What you'll need

  • A VPS with Debian 12+ or Ubuntu 24.04+ (1 vCPU, 1 GB RAM is plenty for a small team)
  • A domain pointed at the server (e.g. git.example.com)
  • Caddy for HTTPS (or nginx if you prefer — Caddy's automatic certs save you a renewal cron)

SQLite is the default database and honestly fine for anything under a few dozen users. Forgejo enables WAL mode automatically, so the "SQLite can't handle concurrency" worry mostly doesn't apply here.

Install the binary the honest way

Skip the distro package — it's usually stale. Grab the release binary directly from Codeberg (Forgejo's own forge):

sudo adduser --system --group --home /home/git \
  --disabled-password --shell /bin/bash git

sudo mkdir -p /var/lib/forgejo /etc/forgejo
sudo chown git:git /var/lib/forgejo

VER="12.0.1"  # check codeberg.org/forgejo/forgejo/releases for the latest
sudo curl -L "https://codeberg.org/forgejo/forgejo/releases/download/v${VER}/forgejo-${VER}-linux-amd64" \
  -o /usr/local/bin/forgejo
sudo chmod +x /usr/local/bin/forgejo
sudo chown git:git /usr/local/bin/forgejo

The git user is the entire security model: Forgejo never needs root, and SSH-based git pushes run inside this account.

systemd unit

# /etc/systemd/system/forgejo.service
[Unit]
Description=Forgejo
After=network-online.target

[Service]
User=git
Group=git
WorkingDirectory=/var/lib/forgejo
Environment="HOME=/home/git"
ExecStart=/usr/local/bin/forgejo web --config /etc/forgejo/app.ini
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now forgejo

app.ini: the config that matters

On first launch Forgejo writes /etc/forgejo/app.ini. Visit http://your-server-ip:3000 and fill the web installer — or drop these essentials in directly:

[server]
PROTOCOL         = http
DOMAIN           = git.example.com
ROOT_URL         = https://git.example.com/
HTTP_ADDR        = 127.0.0.1
HTTP_PORT        = 3000

[service]
DISABLE_REGISTRATION = true
SHOW_REGISTRATION_BUTTON = false

[database]
DB_TYPE = sqlite3
PATH    = /var/lib/forgejo/forgejo.db

[repository]
ROOT = /var/lib/forgejo/repositories

Bind to 127.0.0.1 and let the reverse proxy be the only way in — a forge listening on the public internet with default credentials is how abandoned instances get mined. Note DISABLE_REGISTRATION: on a private forge, accounts should be created by the admin, not the internet. Create your admin user in the web UI, then flip this off in config and restart.

Automatic HTTPS with Caddy

# /etc/caddy/Caddyfile
git.example.com {
    reverse_proxy 127.0.0.1:3000
}

Reload Caddy and you have TLS with automatic renewal — no certbot, no renewal cron, no "your cert expired on a Sunday" pager.

Backups: the three things that actually matter

A Forgejo instance is three things: the config, the database, and the data directories. Back up those and you can rebuild anywhere in ten minutes:

# database (safe while running, thanks to WAL)
sqlite3 /var/lib/forgejo/forgejo.db ".backup '/root/backups/forgejo.db'"

# data: repos, LFS objects, attachments, avatars, logs
tar -czf /root/backups/forgejo-data.tar.gz \
  /var/lib/forgejo/repositories /var/lib/forgejo/data \
  /var/lib/forgejo/log /etc/forgejo/app.ini

Ship those archives off the box with rsync or rclone. A cron job nightly is all it takes — this is the step everyone plans to do after the incident, so just do it now.

Upgrades without drama

The upgrade path is refreshingly boring: stop the service, swap the binary, start it, let it run its own migrations.

sudo systemctl stop forgejo
# download the new binary to /usr/local/bin/forgejo
sudo systemctl start forgejo
sudo journalctl -u forgejo --since "5 minutes ago" | grep -iE "error|migrat"

Never skip major versions in one jump, and read the release notes' "breaking changes" section before you swap — five minutes of reading beats an hour of rollback.

Gotchas from real deployments

  • SSH pushes: Forgejo can serve SSH on port 2222 behind your regular sshd. Point SSH_PORT and SSH_LISTEN_PORT at it in [server], and make sure the git user's ~/.ssh/authorized_keys is managed by Forgejo (it rewrites it — don't hand-edit it).
  • LFS: off by default. Enable [server] LFS_START_SERVER = true only if you actually store large files; otherwise it's an attack surface you don't need.
  • Email: without an SMTP config, invite and notification emails silently go nowhere. A cheap transactional SMTP (or even your VPS provider's relay) fixes it; the mailer config lives in [mailer].
  • CI: Forgejo supports Gitea Actions-compatible runners. Don't install the runner on the same VPS — give it its own box or run it locally, so a malicious workflow can't read your forge's database.
  • Postgres: switch DB_TYPE to postgres only when you outgrow SQLite's comfort zone (dozens of active users, heavy CI). The migration is a dump-and-restore; the docs cover it.

Why this beats "just another VPS service"

Compared to GitLab CE, Forgejo uses roughly a tenth of the RAM at idle — GitLab wants 4 GB minimum and will happily eat 8. Compared to a bare git-shell setup, you get code review, issue tracking, and a web UI your non-terminal coworkers will actually use. And unlike GitHub, the uptime, the data, and the export path are yours: git clone --mirror from your own forge is the entire lock-in strategy, and there isn't one.

Sources