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.
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 wrongWORK_PATHor missing directory permissions. Runsudo -u forgejo forgejo web --config /etc/forgejo/app.iniinteractively to see the real error. - 502 Bad Gateway from Nginx: Forgejo is not running or is still starting. Also confirm
proxy_passpoints to127.0.0.1:3000, notlocalhost:3000(IPv6 resolution differences can cause this). - SSH clone fails: If Forgejo's built-in SSH runs on a non-standard port, set
SSH_PORTinapp.iniand update your clone URLs. Verify theforgejouser's~/.ssh/authorized_keysis writable by the service user. - Runners stuck in "waiting": Confirm Docker is running, the
forgejouser is in thedockergroup (log out and back in, or restart the unit), and that[actions] ENABLED = trueis present inapp.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.
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
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.