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
AorAAAArecord 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
222unless the host is designed around Forgejo owning port22. - 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
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'
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:
- Log in as the administrator and create a normal user account for daily work.
- Add your SSH public key under the user settings.
- Create a private test repository and clone it through SSH.
- Push a commit, open an issue, create a branch, and submit a pull request.
- Confirm email delivery if SMTP is enabled.
- 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.
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, andFORGEJO__server__SSH_PORTmatch the public hostname and SSH port. - SSH clone fails: Check
sudo ufw status, verify the Compose port mapping, and test withssh -F /dev/null -T -p 222 git@git.example.com. - Forgejo cannot write to
/data: Check ownership of/opt/forgejo/forgejoand make sureUSER_UIDandUSER_GIDmatch the directory owner. - Database connection fails: Run
docker compose ps, inspectdocker compose logs db, and confirm the password values in.envmatch the Compose environment. - Uploads fail with 413 errors: Increase
client_max_body_sizein the Nginx site and reload Nginx. - Registration policy is wrong: Review
[service] DISABLE_REGISTRATIONinapp.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