Forgejo is a self-hosted platform for managing Git repositories, similar to GitHub or GitLab, but running fully on our own server. This article covers installing Forgejo via Docker from scratch: setting up Docker Engine, running containers, enabling HTTPS, up to creating your first repository. All commands here were tested against Ubuntu Server 26.04 LTS and Forgejo version 16.0.3.
What Is Forgejo?
Forgejo is an open-source Git server software created as a soft-fork of Gitea in late 2022. Unlike Gitea, Forgejo is managed by an independent community under Codeberg, without the involvement of a single commercial entity controlling development direction.
We can use Forgejo to replace GitHub or GitLab when teams want full control over source code data, without limits on private repositories or reliance on third-party services. Compared to GitLab Community Edition which is heavy for small servers, Forgejo is much lighter because it is written in Go and only needs a single binary to run.
Sysadmins managing internal infrastructure usually choose Forgejo to host code belonging to Developer teams, while ensuring that source code never leaves the company network.
Pre-Installation Requirements
Official Forgejo documentation does not list exact figures for minimum CPU and RAM requirements. As a practical overview, a small instance with a SQLite database and a few users can run comfortably on 1 vCPU and 1 GB RAM. For teams with higher traffic and PostgreSQL database, allocate a minimum of 2 vCPU and 2-4 GB RAM. Note carefully: this is a practical estimation, not an official specification from the Forgejo team, so keep monitoring resource usage after the instance is running.
What must be prepared before starting:
- A server with Ubuntu Server 26.04 LTS (or another Linux distribution supporting Docker)
- Docker Engine and Docker Compose plugin
- A domain or subdomain already pointed to the server IP, if enabling HTTPS
- Ports 80, 443, and a custom SSH port (we use 222 in this article) not used by other services
- Minimum disk space of 10 GB, growing according to the number and size of repositories
For databases, Forgejo supports SQLite (no extra installation needed), PostgreSQL version 14 and above, MySQL version 8.4 and above, and MariaDB version 10.6 and above. SQLite is suitable for small instances or evaluation, while PostgreSQL is a more mature choice for production because it is robust in handling concurrent queries from many users.
Installing Docker Engine on Ubuntu
Follow these four sequential steps to install Docker Engine from the official repository.
Remove old Docker packages that might be installed from non-official repositories. Packages like
docker.io,docker-compose, orpodman-dockeroften conflict with official Docker Engine.sudo apt remove docker.io docker-compose podman-dockerAdd GPG key and official Docker repository to the system.
sudo apt update sudo apt install ca-certificates curl sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc sudo tee /etc/apt/sources.list.d/docker.sources <<EOF Types: deb URIs: https://download.docker.com/linux/ubuntu Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") Components: stable Architectures: $(dpkg --print-architecture) Signed-By: /etc/apt/keyrings/docker.asc EOF sudo apt updateInstall Docker Engine along with Buildx and Compose plugins.
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginVerify installation by running the official Docker test image. If the message "Hello from Docker!" appears, installation is successful and ready to run Forgejo.
sudo docker run hello-world
Preparing Folder Structure and Compose File
Create a working directory to store configuration files and Forgejo data. Separate the data folder so it is easy to backup independently from the compose file.
mkdir -p ~/forgejo/{forgejo,postgres}
cd ~/forgejoNext, create a compose.yaml file in that directory. This file defines the Forgejo service, database, network, and volumes shared together.
Two important environment variables here are USER_UID and USER_GID. Both determine file ownership inside the data volume. The volume folder on the host must be owned by the same UID/GID values; otherwise, the container may fail to start due to permission issues.
Choosing a Database: SQLite or PostgreSQL
SQLite is the simplest choice because it does not require additional services, suitable for small instances or initial evaluations. Its drawback is that SQLite is less optimal when many processes write to the database simultaneously, something common in instances with many active Developers.
For production needs, we use PostgreSQL. Below is the complete compose.yaml content with PostgreSQL as the database backend:
services:
forgejo:
image: codeberg.org/forgejo/forgejo:16.0.3
container_name: forgejo
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=replace-with-strong-password
restart: always
networks:
- forgejo
volumes:
- ./forgejo:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "222:22"
depends_on:
- db
db:
image: postgres:16-alpine
container_name: forgejo-db
restart: always
environment:
- POSTGRES_USER=forgejo
- POSTGRES_PASSWORD=replace-with-strong-password
- POSTGRES_DB=forgejo
networks:
- forgejo
volumes:
- ./postgres:/var/lib/postgresql/data
networks:
forgejo:
external: falseImportant: replace replace-with-strong-password in both places with the same strong password. Do not leave default credentials saved on a production server.
The naming format for environment variables like FORGEJO__database__DB_TYPE follows the FORGEJO__[SECTION]__[KEY] pattern, mapping directly to Forgejo app.ini configuration parameters. This pattern applies to almost all Forgejo configuration options, allowing us to configure many aspects via environment variables without entering the container.
Running the Forgejo Container
Follow these three sequential steps to run Forgejo and ensure the container is stable.
Align folder ownership with the UID/GID defined in the compose file.
sudo chown -R 1000:1000 ~/forgejo/forgejoRun the entire stack using Docker Compose.
sudo docker compose up -dCheck container status to ensure both run without a restart loop.
sudo docker compose ps
If Forgejo status continuously changes from Up to Restarting, check logs to find root causes, usually related to database connections or folder permissions:
sudo docker compose logs -f forgejoInitial Configuration via Web Installer
Once the container is stable, access http://server-address:3000 in your browser. Because database credentials were set via environment variables, the installation wizard usually displays basic configuration pages directly, such as application name, server URL, and initial admin user creation.
Fill out the Administrator Account Settings section with admin username, email, and password. This account has full access to the entire instance, so use a strong and unique password.
Alternatively, we can create an admin account via the command line interface without exposing the web installer publicly, which is safer for production servers:
sudo docker exec forgejo forgejo admin user create \
--username admin \
--password replace-admin-password \
--email [email protected] \
--adminThis CLI approach is useful if we want to block port 3000 access externally from the start and only open access through a reverse proxy after the first admin is created.
Reverse Proxy with Nginx
Running Forgejo directly on port 3000 without a reverse proxy is not recommended for production. Using Nginx as a reverse proxy makes it easy to configure HTTPS, domains, and security headers in one place.
Install Nginx via package manager.
sudo apt install nginxCreate a new configuration file at
/etc/nginx/conf.d/forgejo.conf.server { listen 80; listen [::]:80; server_name git.example.com; merge_slashes off; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Connection $http_connection; proxy_set_header Upgrade $http_upgrade; 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; client_max_body_size 512M; } }The
X-Real-IPandX-Forwarded-Forheaders are important so Forgejo logs original IP addresses from Visitors or Developers rather than Nginx internal IP. Settingmerge_slashes offprevents Nginx from merging consecutive slashes in URLs, which could interfere with certain Git endpoints. Settingclient_max_body_size 512Mincreases upload limits, essential for pushing large repositories.Test configuration and reload Nginx.
sudo nginx -t sudo systemctl reload nginx
Enabling HTTPS with Let's Encrypt
Once the domain points to the server and Nginx runs normally on port 80, it is time to enable HTTPS. Ensure ports 80 and 443 are allowed in the firewall before proceeding.
Install Certbot along with its Nginx plugin.
sudo apt install certbot python3-certbot-nginxRun Certbot for the target domain. Certbot automatically injects SSL configurations into the existing Nginx file and redirects HTTP traffic to HTTPS.
sudo certbot --nginx -d git.example.com
Let's Encrypt certificates are valid for 90 days, but the certbot package in Ubuntu includes a systemd timer handling automatic renewals. Check its schedule with:
sudo systemctl list-timers | grep certbotConfiguring DOMAIN, SSH_DOMAIN, and ROOT_URL in app.ini
After HTTPS is active and the domain resolves to the server via Nginx, it is time to adjust core Forgejo options determining how URLs are constructed for web links, webhook payloads, and repository clone URLs displayed on repository pages.
The main Forgejo configuration file is located at ~/forgejo/forgejo/gitea/conf/app.ini on the host, mapped from the ./forgejo:/data volume created initially (path inside container: /data/gitea/conf/app.ini). Open this file with a text editor, find the [server] section, and adjust these three options:
[server]
DOMAIN = git.example.com
ROOT_URL = https://git.example.com/
SSH_DOMAIN = git.example.com
SSH_PORT = 222DOMAINis used by Forgejo to construct internal URLs and domain cookies. Its value must match the public domain accessed via Nginx; if different, features like login redirects or webhooks may target wrong addresses.ROOT_URLis the complete URL appearing in web links, webhook payloads, and HTTPS clone URLs on repository pages. It must use thehttps://scheme because SSL terminates at Nginx rather than Forgejo itself.SSH_DOMAINandSSH_PORTdefine the domain and port displayed under the SSH tab on the Clone button. AlignSSH_PORTwith the mapped port 222 incompose.yamlso displayed SSH clone URLs are correct without requiring manual edits by Developers.
Save changes and restart the container so new configurations take effect:
sudo docker compose restart forgejoSSH Setup for Git Operations
Forgejo SSH port was mapped to 222 on the host via compose.yaml earlier, avoiding conflicts with the main server SSH daemon running on port 22. Because the port is non-standard and each Developer has dedicated keys for Forgejo, the most practical approach is storing connection details in individual user SSH client configs so users do not need to retype port and key paths for every push or pull. Developers follow these five sequential steps.
Generate a dedicated SSH key pair for Forgejo on client machines. Using a separate filename (instead of default
id_ed25519) makes management easier when users have other keys for different purposes.ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/id_ed25519_forgejoAdd the following entry to the user's
~/.ssh/configfile (create the file if it does not exist).Host git.example.com HostName git.example.com Port 222 User git IdentityFile ~/.ssh/id_ed25519_forgejo IdentitiesOnly yesWith this configuration, the SSH client automatically uses port 222 and the correct key whenever the host matches
git.example.com, without needing-p 222or-iflags added manually in commands.Copy public key contents (
~/.ssh/id_ed25519_forgejo.pub) and paste them into Settings > SSH / GPG Keys under respective Forgejo user accounts.Test connection from the client side. Thanks to
~/.ssh/config, the correct port and key are automatically used. If a welcome message from Forgejo showing the username appears, SSH connectivity works properly.ssh -T [email protected]Clone repository via SSH. Since the port is defined in
~/.ssh/config, the URL format remains as simple as standard SSH cloning without embedding port numbers.git clone [email protected]:username/repo-name.git
Creating Your First Repository
Follow these three sequential steps to test end-to-end Git workflows on the newly installed instance.
Log in to the Forgejo dashboard at
https://git.example.com, then click New Repository. Fill in repository name, brief description, and choose visibility (public or private) based on team needs.Clone the newly created repository locally via HTTPS or SSH.
git clone https://git.example.com/username/repo-name.git cd repo-namePush an initial commit to ensure authentication flow through server storage works normally.
echo "# Repo Name" > README.md git add README.md git commit -m "Initial commit" git push origin main
If the push succeeds without errors, the entire chain from containers, database, reverse proxy, HTTPS, to SSH is confirmed working.
Backup and Updating Forgejo
Forgejo provides a built-in forgejo dump command that compresses databases, repositories, logs, and other supporting data into a single archive file. Run it inside the container:
sudo docker exec forgejo forgejo dump -f /data/forgejo-backup.zipSince /data inside the container links to ~/forgejo/forgejo on the host via volume mapping, backup files automatically appear in that directory and can be copied to separate storage. Schedule this command via cron for regular backups, for example every night.
Updating to the latest Forgejo version follows three sequential steps.
- Open
compose.yamland change theforgejoimage tag to the target version. Pull the new image.
sudo docker compose pull forgejoRestart the container using the new image.
sudo docker compose up -d forgejo
Check official changelogs before upgrading to major releases. Some major releases bring database schema changes requiring automatic migrations on container startup, so ensure fresh backups are available before running updates.
Essential Basic Security Measures
The following steps are recommended baseline standards before using the instance in production:
- Restrict firewall access to ports 80, 443, and custom SSH port (222). Do not expose port 3000 directly to the internet, as traffic should route through Nginx on localhost.
- Disable public registration using
FORGEJO__service__DISABLE_REGISTRATION=truein the compose file, unless public registration is explicitly intended. - Enable two-factor authentication for admin accounts, so attackers obtaining passwords still cannot log in without a second factor.
- Monitor SSH and HTTP access logs regularly to detect brute-force attempts early.
Troubleshooting Common Issues
Common issues encountered during setup:
Container keeps restarting after docker compose up. Most commonly caused by volume folders lacking proper UID/GID ownership. Re-run sudo chown -R 1000:1000 ~/forgejo/forgejo, then restart the container.
Nginx shows 502 Bad Gateway error. Usually means the Forgejo container is not running or proxy_pass points to an incorrect port. Verify using docker compose ps and ensure port 3000 in proxy_pass matches mapped ports in the compose file.
SSH connection refused. Check whether port 222 is open on the server firewall and not conflicting with other services. Run sudo ss -tulpn | grep 222 to verify the container is actively listening on the port.
Push or pull rejected with permission denied message. Ensure your public key is attached to the correct account under SSH Keys menu, and that the user has write permissions to the destination repository.
Conclusion
Forgejo on Docker offers a lightweight, fast-to-deploy self-hosted Git server that is easy to maintain using a single compose.yaml file. Resource-wise, this instance is much lighter than GitLab without losing essential features required daily by Developer teams: pull requests, issue trackers, and Actions for CI/CD automation.




