Forgejo is a lightweight, community-governed Git forge (a hard fork of Gitea) that gives you repositories, issues, pull requests, packages and a built-in CI system called Forgejo Actions. In this tutorial you will run Forgejo and PostgreSQL with Docker Compose on Ubuntu 24.04, publish the web interface over HTTPS through Nginx, enable Git over SSH, and register a runner so your repositories can run CI jobs.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS with at least 1 GB of RAM (2 GB if you plan to run CI jobs on the same machine), for example a CubePath VPS.
- A non-root user with
sudoprivileges. - Docker Engine and the Docker Compose plugin installed from Docker's official repository.
- A domain name such as
git.your_domainwith a DNSArecord pointing to your server's public IP address. - UFW enabled with OpenSSH allowed.
Throughout the guide, replace git.your_domain with your real hostname.
Step 1 - Creating the project directory and secrets
Keep everything Forgejo needs (compose file, data and database files) in one directory so it is easy to back up:
sudo mkdir -p /opt/forgejo
cd /opt/forgejo
Generate a random password for the PostgreSQL user and store it in a .env file that Docker Compose reads automatically:
echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)" | sudo tee /opt/forgejo/.env > /dev/null
sudo chmod 600 /opt/forgejo/.env
Check that the file contains a single line with a long random value:
sudo cat /opt/forgejo/.env
POSTGRES_PASSWORD=3f9c1b0e7a54d2c8e6f1a9b3d7c0e2f4a6b8c1d3e5f7a9b0
Step 2 - Writing the Docker Compose file
The compose file defines two services: forgejo and db. Forgejo's container reads any environment variable in the form FORGEJO__section__KEY and writes it into its app.ini, so you can preconfigure the database, the public URL and the SSH port without editing files inside the container.
Open a new compose file:
sudo nano /opt/forgejo/compose.yaml
Paste the following content:
services:
forgejo:
image: codeberg.org/forgejo/forgejo:15
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=${POSTGRES_PASSWORD}
- FORGEJO__server__DOMAIN=git.your_domain
- FORGEJO__server__ROOT_URL=https://git.your_domain/
- FORGEJO__server__SSH_DOMAIN=git.your_domain
- FORGEJO__server__SSH_PORT=2222
volumes:
- ./data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "127.0.0.1:3000:3000"
- "2222:22"
depends_on:
- db
restart: unless-stopped
db:
image: postgres:17
container_name: forgejo-db
environment:
- POSTGRES_DB=forgejo
- POSTGRES_USER=forgejo
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- ./postgres:/var/lib/postgresql/data
restart: unless-stopped
A few details worth knowing:
- The web port is bound to
127.0.0.1only. Nginx will be the only public entry point for HTTP traffic. - Port
2222on the host maps to the SSH server inside the container, so it does not collide with the server's own SSH daemon on port 22.SSH_PORT=2222makes Forgejo show the right clone URL. codeberg.org/forgejo/forgejo:15tracks the latest release of major version 15. Check the Forgejo releases page and use the current stable or LTS major version.
Step 3 - Starting Forgejo
Start both containers in the background:
cd /opt/forgejo
sudo docker compose up -d
Check that both containers are running:
sudo docker compose ps
NAME IMAGE SERVICE STATUS PORTS
forgejo codeberg.org/forgejo/forgejo:15 forgejo Up 20 seconds 127.0.0.1:3000->3000/tcp, 0.0.0.0:2222->22/tcp
forgejo-db postgres:17 db Up 21 seconds 5432/tcp
Confirm that Forgejo answers locally:
curl -sI http://127.0.0.1:3000 | head -n 1
HTTP/1.1 200 OK
If the status is not 200, read the logs with sudo docker compose logs forgejo. The most common cause is a database authentication error because the .env file was changed after PostgreSQL had already been initialized.
Step 4 - Configuring Nginx and HTTPS
Install Nginx and Certbot with its Nginx plugin:
sudo apt update
sudo apt install nginx certbot python3-certbot-nginx
Create a server block for Forgejo:
sudo nano /etc/nginx/sites-available/forgejo
server {
listen 80;
listen [::]:80;
server_name git.your_domain;
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;
}
}
client_max_body_size raises Nginx's default 1 MB limit, which would otherwise break large pushes over HTTPS and package uploads.
Enable the site, test the configuration and reload Nginx:
sudo ln -s /etc/nginx/sites-available/forgejo /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Open the firewall for HTTP, HTTPS and the Forgejo SSH port:
sudo ufw allow 'Nginx Full'
sudo ufw allow 2222/tcp
Request a Let's Encrypt certificate. Certbot edits the server block to add TLS and a redirect from HTTP to HTTPS:
sudo certbot --nginx -d git.your_domain
Successfully deployed certificate for git.your_domain to /etc/nginx/sites-enabled/forgejo
Congratulations! You have successfully enabled HTTPS on https://git.your_domain
NoteDocker publishes ports by writing its own iptables rules, which bypass UFW. That is why port 3000 is bound to
127.0.0.1in the compose file: never publish it on all interfaces.
Step 5 - Completing the web installer
Visit https://git.your_domain. Forgejo shows its initial configuration page with the database settings already filled in from the environment variables.
- Leave the database section as is (PostgreSQL, host
db:5432). - Under Server and third-party service settings, check Disable self-registration unless you want anyone on the internet to create an account.
- Under Administrator account settings, create your admin user with a strong password.
- Click Install Forgejo.
After a few seconds you are logged in as the administrator. Forgejo stores the resulting configuration in /opt/forgejo/data/gitea/conf/app.ini on the host (the path keeps the gitea name for compatibility).
Step 6 - Testing HTTPS and SSH access
Create a repository called hello from the + menu in the top right corner. Then, on your local machine, add your public SSH key under Settings > SSH / GPG keys in Forgejo and test the SSH connection:
ssh -T -p 2222 [email protected]_domain
Hi there, your_user! You've successfully authenticated with the key named laptop, but Forgejo does not provide shell access.
Clone the repository, commit a file and push it:
git clone ssh://[email protected]_domain:2222/your_user/hello.git
cd hello
echo "# hello" > README.md
git add README.md
git commit -m "Add README"
git push origin main
Refresh the repository page in the browser and the README is rendered. Cloning over HTTPS works too with git clone https://git.your_domain/your_user/hello.git.
Step 7 - Adding a Forgejo Actions runner
Forgejo Actions runs workflows written in a syntax compatible with GitHub Actions, but jobs are executed by a separate program, forgejo-runner, that you register against your instance. Here you will run it on the same server and let it start jobs in Docker containers.
Create a system user for the runner and add it to the docker group so it can start containers:
sudo useradd --system --create-home --home-dir /var/lib/forgejo-runner --shell /usr/sbin/nologin forgejo-runner
sudo usermod -aG docker forgejo-runner
Warningmembership in the
dockergroup is equivalent to root access on the host. Only run jobs from repositories you trust, or put the runner on a separate server.
Download the runner binary. Look up the latest version number on the runner releases page and put it in the RUNNER_VERSION variable, without the leading v:
RUNNER_VERSION=your_runner_version
sudo curl -fL -o /usr/local/bin/forgejo-runner \
"https://code.forgejo.org/forgejo/runner/releases/download/v${RUNNER_VERSION}/forgejo-runner-${RUNNER_VERSION}-linux-amd64"
sudo chmod +x /usr/local/bin/forgejo-runner
forgejo-runner --version
In the Forgejo web interface go to Site administration > Actions > Runners, click Create new runner and copy the registration token. Register the runner from its home directory, where it will save its credentials in a .runner file:
cd /var/lib/forgejo-runner
sudo -u forgejo-runner forgejo-runner register --no-interactive \
--instance https://git.your_domain \
--token your_registration_token \
--name runner-1 \
--labels docker:docker://node:20-bookworm
The label docker is the name workflows will use in runs-on, and docker://node:20-bookworm is the image each job runs in.
Create a systemd unit so the runner starts at boot:
sudo nano /etc/systemd/system/forgejo-runner.service
[Unit]
Description=Forgejo Actions runner
After=docker.service network-online.target
Wants=network-online.target
Requires=docker.service
[Service]
User=forgejo-runner
WorkingDirectory=/var/lib/forgejo-runner
ExecStart=/usr/local/bin/forgejo-runner daemon
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
Enable and start it:
sudo systemctl daemon-reload
sudo systemctl enable --now forgejo-runner
sudo systemctl status forgejo-runner
The runner now appears as Idle in Site administration > Actions > Runners.
To test it, add a workflow to the hello repository at .forgejo/workflows/ci.yaml:
on:
push:
branches: [main]
jobs:
test:
runs-on: docker
steps:
- uses: actions/checkout@v4
- run: ls -la && node --version
Commit and push the file. Open the Actions tab of the repository and you will see the run complete with the file listing and the Node.js version in the log. If the Actions tab is missing, enable it under the repository's Settings > Units.
Troubleshooting
Clone URLs show localhost:3000 or the wrong port. The FORGEJO__server__* variables were not applied. Check them in compose.yaml, then recreate the container with sudo docker compose up -d --force-recreate forgejo.
ssh: connect to host git.your_domain port 2222: Connection refused. Confirm the port is published with sudo docker compose ps and that any provider-level firewall allows TCP 2222.
Pushes over HTTPS fail with 413 Request Entity Too Large. Raise client_max_body_size in the Nginx server block and reload Nginx.
The runner stays offline. Read its log with sudo journalctl -u forgejo-runner -e. A permission denied on /var/run/docker.sock means the user is not in the docker group, and an authentication error means the .runner file must be regenerated with a new token.
Conclusion
You now have Forgejo running with PostgreSQL behind Nginx with HTTPS, accepting Git over SSH on port 2222, and a runner executing Forgejo Actions workflows in Docker containers. As next steps, schedule backups of /opt/forgejo (stop the stack or dump PostgreSQL with pg_dump first), configure outgoing email in app.ini so users receive notifications, and upgrade by changing the image tag and running sudo docker compose pull && sudo docker compose up -d.
