$linuxjunkies
>

Self-Host Forgejo (Gitea Fork)

Install Forgejo from a single binary on Linux, harden it with systemd, put it behind Nginx with TLS, add CI runners, and understand the ForgeFed federation roadmap.

IntermediateUbuntuDebianFedoraArch12 min readUpdated June 7, 2026

Before you start

  • A server with a public IP and a domain A/AAAA record pointing to it
  • Nginx installed with a valid TLS certificate (e.g., from Let's Encrypt via Certbot)
  • Docker installed if you plan to run Forgejo Actions runners with container isolation
  • Root or passwordless sudo access on the server

Forgejo is a community-driven, self-hosted Git service forked from Gitea in late 2022. It is lightweight, ships as a single binary, and runs comfortably on a $5 VPS. This guide walks through a production-grade binary install behind an Nginx reverse proxy, systemd service hardening, optional Forgejo Actions runners for CI/CD, and what to expect from the federation roadmap.

Prerequisites and Assumptions

  • A server running a recent LTS — examples use Ubuntu 24.04, Debian 12, or Fedora 40/RHEL 9.
  • A domain name pointed at the server (A/AAAA record).
  • Nginx installed and a TLS certificate via Certbot or equivalent.
  • Root or passwordless sudo access.

Step 1 — Create a Dedicated System User

Never run Forgejo as root. A locked-down system account keeps the blast radius small if the process is ever compromised.

sudo useradd \
  --system \
  --shell /bin/bash \
  --comment 'Forgejo Git Service' \
  --create-home \
  --home-dir /var/lib/forgejo \
  forgejo

Step 2 — Download the Binary

Forgejo releases are hosted at codeberg.org/forgejo/forgejo/releases. Always grab the latest stable release and verify the signature. Replace 9.0.3 with the current release number.

FORGEJO_VERSION="9.0.3"
ARCH="amd64"   # arm64 also available

wget -O forgejo \
  "https://codeberg.org/forgejo/forgejo/releases/download/v${FORGEJO_VERSION}/forgejo-${FORGEJO_VERSION}-linux-${ARCH}"

wget -O forgejo.asc \
  "https://codeberg.org/forgejo/forgejo/releases/download/v${FORGEJO_VERSION}/forgejo-${FORGEJO_VERSION}-linux-${ARCH}.asc"

Verify the Signature

Forgejo signs releases with a project GPG key. Import it once, then verify every download.

gpg --keyserver keys.openpgp.org \
    --recv-keys EB114F5E6C0DC2BCDD183550A4B61A2DC5923710

gpg --verify forgejo.asc forgejo

You should see Good signature from "Forgejo Releases". A BAD signature means the file is corrupt or tampered — delete it and re-download.

Install the Binary

sudo install -m 0755 forgejo /usr/local/bin/forgejo

Step 3 — Directory Layout and Configuration

sudo mkdir -p /etc/forgejo
sudo mkdir -p /var/lib/forgejo/{data,log,repos,custom}
sudo chown -R forgejo:forgejo /var/lib/forgejo
sudo chown root:forgejo /etc/forgejo
sudo chmod 750 /etc/forgejo

Forgejo reads its main config from /etc/forgejo/app.ini. Create a minimal starting config — the web installer will write the rest on first run.

sudo -u forgejo tee /etc/forgejo/app.ini <<'EOF'
APP_NAME = My Forgejo
RUN_USER = forgejo
RUN_MODE = prod
WORK_PATH = /var/lib/forgejo

[server]
HTTP_PORT = 3000
ROOT_URL   = https://git.example.com/
DOMAIN     = git.example.com
DISABLE_SSH = false
SSH_PORT    = 22

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

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

[log]
ROOT_PATH = /var/lib/forgejo/log
MODE      = file
LEVEL     = Warn

[security]
INSTALL_LOCK = false
EOF

For production with more than a handful of users, replace SQLite with PostgreSQL: set DB_TYPE = postgres and supply HOST, NAME, USER, PASSWD entries. SQLite is fine for personal or small-team use.

Step 4 — systemd Service

Create a hardened unit file. The PrivateTmp, ProtectSystem, and capability restrictions keep the service from wandering outside its own directories.

sudo tee /etc/systemd/system/forgejo.service <<'EOF'
[Unit]
Description=Forgejo self-hosted Git service
Documentation=https://forgejo.org/docs
After=network.target

[Service]
Type=simple
User=forgejo
Group=forgejo
WorkingDirectory=/var/lib/forgejo
ExecStart=/usr/local/bin/forgejo web --config /etc/forgejo/app.ini
Restart=on-failure
RestartSec=5s

# Hardening
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/lib/forgejo /etc/forgejo
NoNewPrivileges=true
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now forgejo

CAP_NET_BIND_SERVICE is only needed if Forgejo binds directly to port 22 or 80. Behind Nginx on port 3000 you can drop those two Capability lines entirely.

Step 5 — Nginx Reverse Proxy with TLS

Forgejo listens on localhost:3000. Nginx terminates TLS and proxies through. Assuming you already have a certificate at the standard Certbot path:

sudo tee /etc/nginx/sites-available/forgejo <<'EOF'
server {
    listen 80;
    server_name git.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name git.example.com;

    ssl_certificate     /etc/letsencrypt/live/git.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/git.example.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;

    client_max_body_size 512m;

    location / {
        proxy_pass         http://127.0.0.1:3000;
        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;
    }
}
EOF

sudo ln -s /etc/nginx/sites-available/forgejo /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

On Fedora/RHEL the config goes in /etc/nginx/conf.d/forgejo.conf — there is no sites-enabled convention by default.

Step 6 — Web Installer and First Admin Account

Browse to https://git.example.com. Forgejo presents a one-time installation form. Review every field — database path, site URL, SSH settings — then scroll to Administrator Account Settings and create your admin user before clicking Install. Once INSTALL_LOCK is set to true in app.ini (Forgejo does this automatically), the installer never appears again.

Step 7 — Forgejo Actions (CI Runners)

Forgejo Actions uses the same YAML syntax as GitHub Actions. Enable it in app.ini first:

sudo -u forgejo tee -a /etc/forgejo/app.ini <<'EOF'

[actions]
ENABLED = true
DEFAULT_ACTIONS_URL = https://code.forgejo.org
EOF

sudo systemctl restart forgejo

Install the act_runner

The official runner binary is act_runner, also distributed as a single binary from Codeberg.

RUNNER_VERSION="0.2.11"
wget -O act_runner \
  "https://code.forgejo.org/forgejo/act_runner/releases/download/v${RUNNER_VERSION}/act_runner-${RUNNER_VERSION}-linux-amd64"
sudo install -m 0755 act_runner /usr/local/bin/act_runner

Register and Run the Runner

Generate a runner registration token in Forgejo: Site Administration → Runners → Create Runner Token. Then register:

act_runner register \
  --instance https://git.example.com \
  --token <YOUR_TOKEN> \
  --name myserver-runner \
  --labels ubuntu-latest:docker://node:20-bookworm

Wrap it in a systemd service for persistence. A minimal unit:

sudo tee /etc/systemd/system/act_runner.service <<'EOF'
[Unit]
Description=Forgejo act_runner
After=docker.service forgejo.service

[Service]
Type=simple
User=forgejo
WorkingDirectory=/var/lib/forgejo
ExecStart=/usr/local/bin/act_runner daemon
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now act_runner

Runners require Docker (or Podman with the Docker-compatible socket) on the host. Add the forgejo user to the docker group: sudo usermod -aG docker forgejo.

Firewall

Open only what is needed. Forgejo needs 22 (SSH), 80, and 443. Examples for the three main firewall tools:

# ufw (Ubuntu/Debian default)
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# firewalld (Fedora/RHEL)
sudo firewall-cmd --permanent --add-service={ssh,http,https}
sudo firewall-cmd --reload

# nftables (Arch / manual setup)
# Add to your nft ruleset:
# tcp dport { 22, 80, 443 } accept

Verification

sudo systemctl status forgejo
sudo journalctl -u forgejo -n 50
curl -I https://git.example.com

The curl should return HTTP/2 200. Check the journal for startup errors, especially database migration messages on upgrades.

Federation Roadmap

Forgejo is actively implementing ForgeFed, an ActivityPub-based federation protocol that will let instances follow repositories, fork across instances, and exchange issues without a central host. As of the 9.x series, federation is in opt-in experimental status — you can enable it in app.ini under [federation] ENABLED = true for testing, but it is not yet production-ready. The Forgejo project publishes development progress at forgejo.org. Expect stable federated stars and follows in a future minor release, with cross-instance pull requests further out.

Troubleshooting

  • Port 3000 refused: Check systemctl status forgejo. A common cause is a wrong WORK_PATH or missing directory permissions. Run sudo -u forgejo forgejo web --config /etc/forgejo/app.ini interactively to see the real error.
  • 502 Bad Gateway from Nginx: Forgejo is not running or is still starting. Also confirm proxy_pass points to 127.0.0.1:3000, not localhost:3000 (IPv6 resolution differences can cause this).
  • SSH clone fails: If Forgejo's built-in SSH runs on a non-standard port, set SSH_PORT in app.ini and update your clone URLs. Verify the forgejo user's ~/.ssh/authorized_keys is writable by the service user.
  • Runners stuck in "waiting": Confirm Docker is running, the forgejo user is in the docker group (log out and back in, or restart the unit), and that [actions] ENABLED = true is present in app.ini.
  • Database migration errors on upgrade: Always back up /var/lib/forgejo/data/forgejo.db (SQLite) or dump your PostgreSQL database before dropping in a new binary.
tested on:Ubuntu 24.04Debian 12Fedora 40Rocky 9

Frequently asked questions

What is the difference between Forgejo and Gitea?
Forgejo is a hard fork of Gitea created in 2022 after governance concerns in the Gitea project. Forgejo is run by a nonprofit, develops in the open on Codeberg, and is adding ForgeFed federation that Gitea does not plan to implement.
Can I migrate my existing Gitea instance to Forgejo?
Yes. Forgejo is a drop-in replacement at the binary level for close Gitea versions. Stop Gitea, swap the binary for the matching Forgejo release, and start the service. Always back up your database and repos first.
Does Forgejo support PostgreSQL or MySQL instead of SQLite?
Yes, all three are supported. Set DB_TYPE to postgres or mysql in app.ini and supply connection details. PostgreSQL is recommended for instances with multiple active users or heavy CI workloads.
What is ForgeFed and when will it be stable?
ForgeFed is an ActivityPub extension for code forges that allows cross-instance repository following, forking, and issue tracking. It is experimentally available in Forgejo 9.x but is not yet production-ready; the project targets stable federated interactions in a future release.
Can I run Forgejo Actions runners without Docker?
Yes. act_runner supports a 'host' execution mode that runs jobs directly on the host without containers, useful for minimal setups or ARM boards. Specify it with the label suffix :host instead of a Docker image URL when registering.

Related guides