GuidesDevelopers and hostingGit server with Forgejo

Run your own Git server with Forgejo and rootless Podman

Forgejo on Debian 13, a Git server with a web interface, issues, pull requests and CI, in rootless Podman under a user of its own, with a separate user for CI jobs and a first push for Git newcomers.

Tested on Forgejo 15 LTS and Forgejo Runner 13.2 on Debian 13 (trixie) on a Melonslab server Updated September 26, 2026

Recommended server for this guide

VC-S Micro · 2 vCPU · 8 GB Memory · 250 GB Storage

Month to month, no lock-in 7-day money-back guarantee

€7.99/mo

Deploy now
On this page

What you will set up

Forgejo is a Git server with a web interface, much like GitHub: repositories, issues, pull requests, and Forgejo Actions, which runs your tests and builds on every push, with workflows much like GitHub's.

Here it runs under two users of their own:

  • forgejo runs Forgejo itself, in one container, with its database inside. The web interface sits behind Caddy from the Podman guide, and Git connects over SSH on port 2222.
  • runner runs the Forgejo Runner, which starts a fresh rootless container for each CI job. Jobs run code from your repositories, so they get a user of their own: a job cannot touch Forgejo's files or any other app on the server.

New to Git? Step 9 shows how to put code on the server, and what to do when Git refuses.

Every step below was run on a Melonslab VC-P Alloy (2 vCPU, 8 GB) with Debian 13:

  • From a laptop, a repository was cloned over SSH, changed and pushed, and an existing folder was pushed as a new repository.
  • A push that Git refused, because of a change from another computer, went through after git pull, also when both had changed the same line.
  • A workflow checked out the code and ran in the runner's containers, and a cache it saved was restored on the next run.
  • Forgejo logged the real address behind a failed login, and CI jobs could not reach apps that listen only on the server's loopback address.
  • Everything came back by itself after a reboot.

Forgejo used about 120 MB of memory, and the runner 35 MB between jobs.

Before you start

You need:

  • a server set up as in the Podman guide, with Caddy running;
  • ufw switched on as in the security guide. The runner opens ports on the server for its jobs' cache, which the firewall keeps closed to the internet;
  • an A record and an AAAA record for git.example.com pointing at your server.

The examples use git.example.com, anna as your user name and 203.0.113.10 for your server's IPv4 address. Replace them with your own throughout.

1. Open the SSH port for Git

Port 22 is the server's own SSH, so Forgejo's SSH server listens on 2222. As root:

ufw allow 2222/tcp

2. Create the user

useradd -m -s /bin/bash forgejo
loginctl enable-linger forgejo
machinectl shell forgejo@

3. Create the secret key

Forgejo encrypts some of what it stores with a secret key, such as the secrets behind two-factor logins:

openssl rand -hex 32 | tr -d '\n' | podman secret create forgejo-secret-key -

4. Describe the container

mkdir -p ~/.config/containers/systemd

Create ~/.config/containers/systemd/forgejo.container:

[Unit]
Description=Forgejo, a Git server

[Container]
ContainerName=forgejo
Image=codeberg.org/forgejo/forgejo:15-rootless
Volume=forgejo-data:/var/lib/gitea
# SSH for Git, open to the internet
PublishPort=2222:2222
# Web interface, reached through Caddy only
PublishPort=127.0.0.1:8089:3000
Secret=forgejo-secret-key,type=mount,target=/run/secrets/forgejo-secret-key,uid=1000,mode=0400
Environment=FORGEJO__security__SECRET_KEY__FILE=/run/secrets/forgejo-secret-key
Environment=FORGEJO__security__INSTALL_LOCK=true
Environment=FORGEJO__security__REVERSE_PROXY_TRUSTED_PROXIES=127.0.0.0/8,::1/128,203.0.113.10/32
Environment=FORGEJO__server__DOMAIN=git.example.com
Environment=FORGEJO__server__ROOT_URL=https://git.example.com/
Environment=FORGEJO__server__SSH_DOMAIN=git.example.com
Environment=FORGEJO__server__SSH_PORT=2222
Environment=FORGEJO__service__DISABLE_REGISTRATION=true
AutoUpdate=registry

[Service]
Restart=always

[Install]
WantedBy=default.target

Each FORGEJO__ line is a Forgejo setting:

  • INSTALL_LOCK skips the setup page, which anyone could otherwise use to take over a fresh Forgejo before you do.
  • Visitors reach Forgejo through Caddy, and those connections arrive in the container from the server's own IPv4 address. REVERSE_PROXY_TRUSTED_PROXIES includes it, so Forgejo logs each visitor's real address, which Caddy passes on.
  • DISABLE_REGISTRATION means nobody can sign up by themselves: you create the accounts.

Everything Forgejo stores, including its SQLite database, lives in the forgejo-data volume. SQLite is plenty for a person, a family or a small team. 15-rootless is Forgejo's long-term support release, with fixes until July 2027.

Start it:

systemctl --user daemon-reload
systemctl --user start forgejo
podman logs forgejo

The log ends with Starting new Web server: tcp:0.0.0.0:3000.

5. Put Caddy in front

Go back to root with exit, switch to machinectl shell caddy@, and add this block to the end of ~/Caddyfile:

git.example.com {
    reverse_proxy 127.0.0.1:8089
}

Restart Caddy with systemctl --user restart caddy.

6. Create your account

Go back to root with exit, then to machinectl shell forgejo@, and run:

podman exec forgejo forgejo admin user create --admin --username anna --email anna@example.com --random-password

It prints generated random password is '...'. Log in at https://git.example.com with it, and Forgejo asks you to choose your own. Create accounts for other people the same way, without --admin, or from the administration pages in the web interface.

7. Set up the runner for Forgejo Actions

Skip steps 7 and 8 if you do not need CI. As root, install Git, which the runner needs, and download the runner. The runner is one program, and Forgejo signs each release, so check the signature before you install it:

apt install -y git gpg curl
v=13.2.0
url=https://code.forgejo.org/forgejo/runner/releases/download/v$v/forgejo-runner-$v-linux-amd64
curl -fL -o forgejo-runner $url
curl -fL -o forgejo-runner.asc $url.asc
gpg --keyserver hkps://keys.openpgp.org --recv EB114F5E6C0DC2BCDD183550A4B61A2DC5923710
gpg --verify forgejo-runner.asc forgejo-runner

The last command must print Good signature from "Forgejo <contact@forgejo.org>". A warning that the key is not certified is expected. 13.2.0 was the newest version when we wrote this; code.forgejo.org/forgejo/runner/releases lists newer ones. Install it, and create the user:

install -m 755 forgejo-runner /usr/local/bin/forgejo-runner
rm forgejo-runner forgejo-runner.asc
useradd -m -s /bin/bash runner
loginctl enable-linger runner
systemctl --user -M runner@ enable --now podman.socket
machinectl shell runner@

The runner starts each job's container through this user's Podman, which podman.socket gives it access to.

Jobs usually start by fetching your code from https://git.example.com, which is this server. Rootless containers normally share the server's own address, and so cannot reach it. Create ~/.config/containers/containers.conf, which gives this user's containers addresses of their own:

mkdir -p ~/.config/containers
# Give job containers addresses of their own, so they can reach this
# server's public address, and Forgejo on it.
[network]
pasta_options = ["-a", "10.0.2.0", "-n", "24", "-g", "10.0.2.2", "--dns-forward", "10.0.2.3"]

Then generate the runner's configuration:

forgejo-runner generate-config > ~/runner-config.yml
chmod 600 ~/runner-config.yml

Open ~/runner-config.yml in an editor, such as nano ~/runner-config.yml, and replace the line labels: [] with:

  labels:
    - docker:docker://data.forgejo.org/oci/node:24-trixie
    - ubuntu-latest:docker://data.forgejo.org/oci/node:24-trixie

Keep the two spaces at the start of the first line. A workflow picks its runner with runs-on:, and both names run the job in Debian 13 with Node.js, Git and Python.

8. Connect the runner

In Forgejo, open https://git.example.com/admin/actions/runners and choose Create new runner. Name it, for example server-runner, and choose Create. Forgejo shows a block that starts with server:, with the runner's token, once. In ~/runner-config.yml, delete everything from the line server: to the end of the file, and paste the block in its place.

Create ~/.config/systemd/user/forgejo-runner.service:

mkdir -p ~/.config/systemd/user
[Unit]
Description=Forgejo Runner, which runs Forgejo Actions jobs in rootless Podman
After=podman.socket
Wants=podman.socket

[Service]
ExecStart=/usr/local/bin/forgejo-runner daemon -c %h/runner-config.yml
ExecReload=/bin/kill -s HUP $MAINPID
WorkingDirectory=%h
Restart=on-failure
RestartSec=10
TimeoutStopSec=infinity

[Install]
WantedBy=default.target

Start it:

systemctl --user daemon-reload
systemctl --user enable --now forgejo-runner
journalctl --user -u forgejo-runner

The log says declared successfully, and the runner page in Forgejo shows it as Idle.

9. Push your first code

This part is on your own computer. Git keeps the whole history of a project on your computer, and git push sends it to Forgejo.

Install Git. On Windows, install Git for Windows from git-scm.com. On macOS, run git --version in Terminal and accept the offer to install it. On Linux, install the git package, for example with sudo apt install git.

Tell Git who you are, once, in a terminal (on Windows, in PowerShell or Git Bash):

git config --global user.name "Anna Andersson"
git config --global user.email anna@example.com
git config --global init.defaultBranch main
git config --global pull.rebase true

Your name and address go on every change you save. The third line names your first branch main, as Forgejo does. The last one is explained under "When Git refuses" below.

Add your SSH key to Forgejo. Use the key from step 1 of the security guide, or make one with ssh-keygen -t ed25519. Show the public key:

cat ~/.ssh/id_ed25519.pub

In Forgejo, open your avatar's menu at the top right, choose Settings, then SSH / GPG keys and Add key. Paste the whole line, starting with ssh-ed25519, and choose Add key.

Create a repository. Choose + at the top right, then New repository. Name it hello, tick Initialize repository, and choose Create repository.

Copy it to your computer:

git clone ssh://git@git.example.com:2222/anna/hello.git
cd hello

The repository's page shows the same address when you choose SSH. The first time, SSH asks whether you trust the server: type yes.

Change something and send it. Open README.md in any editor, add a line and save it. Then:

git add README.md
git commit -m "Say hello"
git push

git add picks the changes to save, git commit saves them as one step in the history, on your computer, and git push sends your new steps to Forgejo. Reload the repository's page to see the change. git status shows what you have changed and not yet saved.

Put a folder you already have on Forgejo. Create a repository as above, but leave Initialize repository unticked, so it is empty. In the folder:

git init
git add .
git commit -m "First commit"
git remote add origin ssh://git@git.example.com:2222/anna/myproject.git
git push -u origin main

After the first push, git push is enough. To get changes made elsewhere, such as on another computer, run git pull.

When Git refuses

! [rejected] main -> main (fetch first). Forgejo has changes you do not have yet, for example from your other computer. Run git pull, then git push again. With pull.rebase set as above, git pull puts your new steps after the ones from Forgejo.

CONFLICT after git pull. You and the other change edited the same lines. Git marks them in the file, between <<<<<<< and >>>>>>>. Edit the file to keep what you want and remove the marker lines, then run git add with the file's name and git rebase --continue. If Git opens an editor with the step's message, save and close it. Then git push. git rebase --abort undoes the whole git pull instead.

error: src refspec main does not match any. There is nothing to push yet. Run git add and git commit first.

Permission denied (publickey). Forgejo did not accept your key. Check it is added under SSH / GPG keys, and that the address starts with ssh://git@: every account uses git, and Forgejo tells them apart by their key.

10. Run a workflow

In a repository, create .forgejo/workflows/test.yml:

on: [push]
jobs:
  test:
    runs-on: docker
    steps:
      - uses: actions/checkout@v5
      - run: ls

Add, commit and push it. The repository's Actions tab shows the run, and each step's output. Many workflows written for GitHub run as they are, with runs-on: ubuntu-latest. That runs on this runner too, in the same Debian image, which has less software installed than GitHub's own machines.

11. Keep it up to date

As forgejo, switch on Podman's daily updates:

systemctl --user enable --now podman-auto-update.timer

The 15-rootless tag gets every fix within Forgejo 15. Moving to the next long-term release is a change to the tag you make yourself, after reading Forgejo's release notes. For a new runner version, repeat the download in step 7 with its number, then run systemctl --user restart forgejo-runner as runner. The runner downloads the job image once and then keeps it; as runner, podman pull data.forgejo.org/oci/node:24-trixie gets the newest.

12. Back up

As forgejo:

mkdir -p ~/backup
systemctl --user stop forgejo
podman volume export forgejo-data --output ~/backup/forgejo-data.tar
systemctl --user start forgejo

That saves every repository, the database and Forgejo's settings. Copy ~/backup to another machine afterwards, and keep it private.

Troubleshooting

Git cannot connect on port 2222. Check the ufw rule from step 1, and that systemctl --user status forgejo shows Forgejo running as forgejo.

A job waits and never starts. No runner has the name in its runs-on: line. Check it is docker or ubuntu-latest, and that the runner shows as Idle at /admin/actions/runners.

The checkout step fails with Failed to connect to git.example.com port 443. The job container shares the server's address. Check ~/.config/containers/containers.conf from step 7, as runner.

Run it on your own server

VC-S Micro

€7.99/mo

vCPU
2
Memory
8 GB
Storage
250 GB
Transfer
10 TB
Standard
HDD · RAID 10
  • Full root access
  • Native /64 IPv6
  • RAID-protected storage
  • Malmö, Sweden
  • Month to month, no lock-in
  • 7-day money-back guarantee
All guides