Self-Hosting Forgejo: A Complete Guide to Running an Open-Source Git Forge

Self-Hosting Forgejo: A Complete Guide to Running an Open-Source Git Forge

Forgejo is a community-led open-source Git forge for repositories, pull requests, issues, packages, releases, and CI-style workflows. This guide walks through a practical Docker Compose deployment on Ubuntu 24.04 LTS with PostgreSQL, SSH clone access, Nginx, HTTPS, backups, upgrades, and safe runner planning.

 Forgejo self-hosted Git forge overview

Caption: Forgejo gives teams a self-hosted place for Git repositories, issues, pull requests, releases, packages, webhooks, and optional Actions runners.

Introduction

Forgejo is a community-led open-source Git forge for people who want Git hosting without handing every repository, deploy key, issue, release, and automation log to a third-party SaaS. It is a hard fork of Gitea with a focus on free software governance, federation-minded development, lightweight operations, and a familiar collaboration workflow. If you have used GitHub, GitLab, Gitea, Codeberg, or Bitbucket, the core ideas will feel natural: users create repositories, push over SSH or HTTPS, open pull requests, review code, track issues, publish releases, and manage organization access from a browser.

Self-hosting Forgejo makes sense when your source code is part of your infrastructure. Homelab administrators can version Docker Compose files, Terraform, Ansible playbooks, dotfiles, and server notes. Small agencies can keep client repositories and deployment manifests under their own backup policy. Product teams can create a private forge for internal packages, release artifacts, mirrors, webhooks, and code review. You also control account registration, repository visibility, runner placement, retention rules, and the exact upgrade window.

This guide installs Forgejo on Ubuntu 24.04 LTS with Docker Compose, PostgreSQL, persistent host directories, Nginx, Let's Encrypt certificates, and a dedicated SSH clone port. The example publishes the site at https://git.example.com, maps container SSH to host port 222, stores configuration under /opt/forgejo, and keeps Forgejo reachable only through Nginx for web traffic. Replace the example domain, email address, passwords, and firewall policy before using these commands in production.

Why Choose Forgejo?

  • Community-led Git hosting: Forgejo is built as a free software forge with transparent development and practical self-hosting defaults.
  • Familiar collaboration flow: Repositories, pull requests, issues, labels, milestones, projects, releases, packages, webhooks, and organizations are built in.
  • Small operational footprint: A single Docker Compose stack can serve a personal forge or small team without the overhead of a larger DevOps suite.
  • PostgreSQL support: Relational state lives in a dedicated database while Git repositories, attachments, avatars, packages, and logs stay on persistent storage.
  • Control over access: Registration, anonymous browsing, default repository privacy, SSH ports, and organization membership are under your policy.
  • Optional Actions runners: Forgejo Actions can run workflows through separately installed runners when you are ready to isolate and operate them safely.
  • Good migration target: Mirrors, Git remotes, webhooks, releases, and package features make Forgejo useful for consolidating scattered code hosting.

Forgejo can hold sensitive source code, production deployment manifests, customer project history, release assets, package files, webhook URLs, and runner logs. Treat it like core infrastructure: harden HTTPS, restrict registration, test backups, and keep CI runners outside the trust boundary of the main application.

Prerequisites

Hardware Recommendations:

  • 1 vCPU and 1 GB RAM for a personal forge with a small number of repositories
  • 2 vCPU and 2-4 GB RAM for a small team, package registry use, or frequent repository indexing
  • SSD storage for Git object databases, attachments, avatars, packages, logs, indexes, and PostgreSQL data
  • Off-server backup storage such as another VPS, NAS, encrypted object storage bucket, or backup appliance
  • A separate runner host if you plan to execute Forgejo Actions jobs from untrusted repositories

Software and Accounts:

  • Ubuntu 24.04 LTS server with sudo access
  • A domain such as git.example.com
  • DNS A or AAAA record pointing the hostname to the server
  • Docker Engine with the Docker Compose v2 plugin
  • Nginx, Certbot, OpenSSL, curl, ufw, jq, git, rsync, and PostgreSQL client tools
  • A password manager for the site administrator account, database password, SMTP password, backup keys, and recovery notes

Security Notes:

  • Put the web interface behind HTTPS before creating users or importing repositories.
  • Keep /opt/forgejo/.env, database dumps, repository backups, and SSH host keys private.
  • Disable open registration unless you intentionally operate a public community forge.
  • Use a dedicated SSH clone port such as 222 unless the host is designed around Forgejo owning port 22.
  • Do not run untrusted Actions jobs on the same host as Forgejo, PostgreSQL, or backup credentials.
  • Back up both the database and Forgejo data directory before upgrades.

Start with a patched host and a tight firewall:

sudo apt update
sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg openssl ufw nginx certbot python3-certbot-nginx jq git rsync postgresql-client

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 222/tcp
sudo ufw enable
sudo ufw status

Installation Guide

This deployment uses the official Forgejo container image from Codeberg with PostgreSQL. Forgejo stores application files, Git repositories, attachments, avatars, packages, logs, templates, and generated configuration under /data inside the container. PostgreSQL stores relational metadata such as users, organizations, repository settings, issues, pull requests, permissions, releases, webhooks, and package records.

1. Install Docker Engine

Install Docker and confirm that Compose v2 is available:

curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER"
newgrp docker

docker --version
docker compose version

These examples use the modern docker compose plugin syntax. If docker compose version fails, fix the Docker installation before continuing.

2. Create the Project Directory

Create a private working directory for Compose files, persistent data, PostgreSQL, and backups:

sudo mkdir -p /opt/forgejo/{forgejo,postgres,backups}
sudo chown -R "$USER":"$USER" /opt/forgejo
chmod 700 /opt/forgejo
cd /opt/forgejo

The Forgejo Docker documentation notes that the mounted data volume must be writable by the UID and GID used inside the container. This guide uses USER_UID=1000 and USER_GID=1000; adjust those values if your service user has a different numeric ID.

3. Generate Environment Values

Create a .env file for Compose interpolation. Generate a strong database password and set the public hostname:

cd /opt/forgejo

FORGEJO_DB_PASSWORD="$(openssl rand -base64 36 | tr -d '\n')"
cat > .env <<EOF
FORGEJO_DOMAIN=git.example.com
FORGEJO_ROOT_URL=https://git.example.com/
FORGEJO_SSH_PORT=222
FORGEJO_DB_PASSWORD=${FORGEJO_DB_PASSWORD}
POSTGRES_PASSWORD=${FORGEJO_DB_PASSWORD}
EOF

chmod 600 .env

Store the generated password in your password manager. The example reuses one strong password for the application database user and the PostgreSQL container variable to keep the walkthrough compact. Larger environments should use dedicated secret handling and separate rotation procedures.

4. Write Docker Compose Configuration

Create docker-compose.yml with Forgejo, PostgreSQL, a private Compose network, persistent host mounts, and localhost-only web binding:

networks:
  forgejo:
    external: false

services:
  server:
    image: codeberg.org/forgejo/forgejo:16
    container_name: forgejo
    restart: always
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - 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__server__DOMAIN=${FORGEJO_DOMAIN}
      - FORGEJO__server__SSH_DOMAIN=${FORGEJO_DOMAIN}
      - FORGEJO__server__SSH_PORT=${FORGEJO_SSH_PORT}
      - FORGEJO__server__ROOT_URL=${FORGEJO_ROOT_URL}
      - FORGEJO__service__DISABLE_REGISTRATION=true
      - FORGEJO__service__REQUIRE_SIGNIN_VIEW=false
      - FORGEJO__repository__DEFAULT_PRIVATE=last
      - FORGEJO__session__COOKIE_SECURE=true
    networks:
      - forgejo
    volumes:
      - ./forgejo:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "127.0.0.1:3000:3000"
      - "222:22"
    depends_on:
      db:
        condition: service_healthy

  db:
    image: docker.io/library/postgres:16
    container_name: forgejo-db
    restart: always
    environment:
      - POSTGRES_USER=forgejo
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=forgejo
    networks:
      - forgejo
    volumes:
      - ./postgres:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U forgejo -d forgejo"]
      interval: 10s
      timeout: 5s
      retries: 5

 Forgejo Docker Compose stack

Caption: A small Forgejo deployment separates public HTTPS, local web traffic, SSH clone access, PostgreSQL metadata, persistent Forgejo data, and backup storage.

The web port is bound to 127.0.0.1:3000 so only Nginx can reach it locally. SSH is published on host port 222, so users clone with URLs such as ssh://git@git.example.com:222/owner/repo.git. If you want standard SSH on port 22, plan the host OpenSSH service carefully before changing the mapping.

Validate the Compose file and start the stack:

docker compose config
docker compose pull
docker compose up -d
docker compose ps
docker compose logs -f --tail=100 server

Wait until Forgejo starts without database errors. Do not complete the browser onboarding over plain HTTP from a public network; configure Nginx and HTTPS first.

5. Configure Nginx and HTTPS

Create an Nginx reverse proxy for the Forgejo web interface:

sudo tee /etc/nginx/sites-available/forgejo >/dev/null <<'EOF'
server {
    listen 80;
    server_name git.example.com;

    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/forgejo
sudo nginx -t
sudo systemctl reload nginx

sudo certbot --nginx -d git.example.com

After Certbot finishes, confirm the HTTPS site responds:

curl -I https://git.example.com/

If your team uploads large release assets, packages, or Git LFS objects, increase client_max_body_size according to your policy. For normal source repositories, 512 MB is a reasonable starting point.

6. Complete First-Run Onboarding

Open https://git.example.com/ and review the onboarding page. Most database and URL settings are already provided through FORGEJO__... environment variables:

  • Database type: PostgreSQL
  • Host: db:5432
  • Database name: forgejo
  • Username: forgejo
  • Password: value from /opt/forgejo/.env
  • Site title: your team, organization, or personal forge name
  • Server domain: git.example.com
  • Forgejo base URL: https://git.example.com/
  • SSH server port: 222

Create the initial administrator account during onboarding. Because open registration is disabled in Compose, this administrator account becomes the path for inviting or creating additional users.

Configuration

Forgejo stores its main configuration in app.ini. In the Docker image, environment variables named like FORGEJO__section__KEY are translated into the matching app.ini section and key. Review the generated values after the first start:

docker compose exec server bash -lc 'grep -E "ROOT_URL|SSH_DOMAIN|SSH_PORT|DISABLE_REGISTRATION|COOKIE_SECURE" /data/gitea/conf/app.ini'

 Forgejo security configuration map

Caption: A production Forgejo configuration should align public URLs, SSH clone settings, registration policy, cookie security, SMTP delivery, repository privacy, and runner boundaries.

For a private team forge, consider these settings:

[service]
DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = true
ENABLE_NOTIFY_MAIL = true

[repository]
DEFAULT_PRIVATE = private

[server]
ROOT_URL = https://git.example.com/
SSH_DOMAIN = git.example.com
SSH_PORT = 222

[session]
COOKIE_SECURE = true

Configure SMTP before inviting users so password resets, mentions, issue updates, and review notifications can be delivered:

services:
  server:
    environment:
      - FORGEJO__mailer__ENABLED=true
      - FORGEJO__mailer__PROTOCOL=smtps
      - FORGEJO__mailer__SMTP_ADDR=smtp.example.com
      - FORGEJO__mailer__SMTP_PORT=465
      - FORGEJO__mailer__FROM=git@example.com
      - FORGEJO__mailer__USER=git@example.com
      - FORGEJO__mailer__PASSWD=${FORGEJO_SMTP_PASSWORD}

Restart after configuration changes:

docker compose up -d
docker compose logs --tail=100 server

If you enable Forgejo Actions, install Forgejo Runner separately. A runner executes code from repositories, so do not place an untrusted runner on the same host that stores Forgejo data, PostgreSQL, SSH keys, or backups.

Usage

Start with a small operational checklist before moving important projects:

  1. Log in as the administrator and create a normal user account for daily work.
  2. Add your SSH public key under the user settings.
  3. Create a private test repository and clone it through SSH.
  4. Push a commit, open an issue, create a branch, and submit a pull request.
  5. Confirm email delivery if SMTP is enabled.
  6. Confirm anonymous users can or cannot browse repositories according to your policy.

Test SSH access from your workstation:

ssh -F /dev/null -T -p 222 git@git.example.com

git clone ssh://git@git.example.com:222/your-user/test-repo.git
cd test-repo
printf '# Test Repository\n' > README.md
git add README.md
git commit -m "docs: add readme"
git push origin main

For an existing project, add Forgejo as another remote and push the branches and tags you want to host:

git remote add forgejo ssh://git@git.example.com:222/your-user/project.git
git push forgejo main
git push forgejo --tags

For repository mirrors, package publishing, or release uploads, create a dedicated test repository first. Validate permissions, retention, webhooks, and backup size before moving production projects.

Screenshots and Visuals

The visuals in this guide are original diagrams rather than copied product screenshots. They show the deployment model you are operating: Nginx terminates HTTPS, Forgejo handles web and SSH workflows, PostgreSQL stores relational state, persistent volumes hold repository data, and backups leave the host.

 Forgejo backup and restore workflow

Caption: Useful Forgejo backups capture PostgreSQL, Forgejo data, repositories, attachments, packages, configuration, verification logs, and off-server copies.

Troubleshooting

  • Web UI shows wrong clone URLs: Confirm FORGEJO__server__ROOT_URL, FORGEJO__server__SSH_DOMAIN, and FORGEJO__server__SSH_PORT match the public hostname and SSH port.
  • SSH clone fails: Check sudo ufw status, verify the Compose port mapping, and test with ssh -F /dev/null -T -p 222 git@git.example.com.
  • Forgejo cannot write to /data: Check ownership of /opt/forgejo/forgejo and make sure USER_UID and USER_GID match the directory owner.
  • Database connection fails: Run docker compose ps, inspect docker compose logs db, and confirm the password values in .env match the Compose environment.
  • Uploads fail with 413 errors: Increase client_max_body_size in the Nginx site and reload Nginx.
  • Registration policy is wrong: Review [service] DISABLE_REGISTRATION in app.ini, then recreate the Forgejo container after Compose changes.
  • Emails do not send: Check SMTP protocol, host, port, sender, username, password, and docker compose logs server.
  • Actions jobs do not run: Confirm a Forgejo Runner is installed, registered, online, and assigned labels that match the workflow jobs.
  • Backups are incomplete: Restore into a temporary server and confirm users, repositories, issues, pull requests, attachments, releases, packages, and settings are present.

Scaling, Securing, and Next Steps

Backups must include both PostgreSQL and the Forgejo data directory. The Forgejo upgrade guide recommends a full backup before upgrades and notes that forgejo dump should be paired with a separate PostgreSQL dump for database-backed installs. A simple maintenance script can capture both:

sudo tee /opt/forgejo/backup.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

cd /opt/forgejo
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p backups

docker compose exec -T db pg_dump -U forgejo -d forgejo | gzip > "backups/forgejo-db-${stamp}.sql.gz"
docker compose exec -T server forgejo dump --type zip --file - > "backups/forgejo-dump-${stamp}.zip"
tar --one-file-system -czf "backups/forgejo-data-${stamp}.tar.gz" forgejo

find backups -type f -mtime +14 -delete
EOF

sudo chmod 700 /opt/forgejo/backup.sh
/opt/forgejo/backup.sh
rsync -avz /opt/forgejo/backups/ backup-user@backup.example.net:/srv/backups/forgejo/

Test restores on a separate machine. A backup is only useful if the database dump, data directory, .env, SSH settings, and Forgejo configuration can be restored together.

For upgrades, read the Forgejo release notes, take a fresh backup, pull the next intended image, restart, and verify normal workflows:

cd /opt/forgejo
/opt/forgejo/backup.sh
docker compose pull
docker compose down
docker compose up -d
docker compose logs --tail=120 server

Pinning codeberg.org/forgejo/forgejo:16 keeps upgrades deliberate within the current stable series. When a future major tag is released, read the upgrade guide before changing the Compose file, because some major upgrades require manual verification or cleanup.

The outcome of this guide is a private, HTTPS-secured Forgejo instance with PostgreSQL durability, SSH clone support, disabled open registration, persistent storage, and a tested backup path. From here, migrate one repository at a time, document restore steps, monitor disk usage, review runner isolation, and keep an upgrade log so the forge remains simple enough to rebuild under pressure.

Need this done on your server?

I deploy and harden Laravel, CodeCanyon, and open-source apps on cPanel or VPS, and offer monthly Server Watch retainers. Hire for deploy · Care plan

References

Share:

Get new posts in your inbox

No spam. One short email per new article — practical PHP, Laravel, devops, and AI-assisted workflows.

Comments

Powered by GitHub Discussions via Giscus. A free GitHub account is required.