--- title: "How to Install Forgejo with Docker on Ubuntu 24.04" description: "Self-host Git with Forgejo on Ubuntu 24.04 using Docker Compose and PostgreSQL, serve it over HTTPS with Nginx, enable SSH cloning and add an Actions runner." category: "Development Tools" language: en source: https://cubepath.com/docs/development-tools/forgejo-git-hosting-installation updated: 2026-09-25 --- # How to Install Forgejo with Docker on Ubuntu 24.04 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 `sudo` privileges. - Docker Engine and the Docker Compose plugin installed from Docker's official repository. - A domain name such as `git.your_domain` with a DNS `A` record 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: ```bash 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: ```bash 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: ```bash sudo cat /opt/forgejo/.env ``` ```output 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: ```bash sudo nano /opt/forgejo/compose.yaml ``` Paste the following content: ```yaml 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.1` only. Nginx will be the only public entry point for HTTP traffic. - Port `2222` on 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=2222` makes Forgejo show the right clone URL. - `codeberg.org/forgejo/forgejo:15` tracks the latest release of major version 15. Check the [Forgejo releases page](https://forgejo.org/releases/) and use the current stable or LTS major version. ## Step 3 - Starting Forgejo Start both containers in the background: ```bash cd /opt/forgejo sudo docker compose up -d ``` Check that both containers are running: ```bash sudo docker compose ps ``` ```output 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: ```bash curl -sI http://127.0.0.1:3000 | head -n 1 ``` ```output 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: ```bash sudo apt update sudo apt install nginx certbot python3-certbot-nginx ``` Create a server block for Forgejo: ```bash sudo nano /etc/nginx/sites-available/forgejo ``` ```nginx 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: ```bash 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: ```bash 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: ```bash sudo certbot --nginx -d git.your_domain ``` ```output Successfully deployed certificate for git.your_domain to /etc/nginx/sites-enabled/forgejo Congratulations! You have successfully enabled HTTPS on https://git.your_domain ``` > Note: Docker publishes ports by writing its own iptables rules, which bypass UFW. That is why port 3000 is bound to `127.0.0.1` in 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. 1. Leave the database section as is (PostgreSQL, host `db:5432`). 2. Under **Server and third-party service settings**, check **Disable self-registration** unless you want anyone on the internet to create an account. 3. Under **Administrator account settings**, create your admin user with a strong password. 4. 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: ```bash ssh -T -p 2222 git@git.your_domain ``` ```output 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: ```bash git clone ssh://git@git.your_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: ```bash sudo useradd --system --create-home --home-dir /var/lib/forgejo-runner --shell /usr/sbin/nologin forgejo-runner sudo usermod -aG docker forgejo-runner ``` > Warning: membership in the `docker` group 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](https://code.forgejo.org/forgejo/runner/releases) and put it in the `RUNNER_VERSION` variable, without the leading `v`: ```bash 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: ```bash 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: ```bash sudo nano /etc/systemd/system/forgejo-runner.service ``` ```ini [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: ```bash 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`: ```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`.