Forgejo with Docker on the Raspberry Pi 3

You know the classic advice about not putting all your eggs in the same basket? The same, but applied to code repositories.

It makes me a bit nervous that we somehow have centralised a lot of services in GitHub. Plus, you can reduce latency considerably by working closer to where you are, and publishing only at the end.

So I have been wanting to try running Forgejo locally for a while. Since I resuscitated a Raspberry Pi 3 on which I installed Nextcloud, I wondered if it can take this one too.

Then I saw the instructions strongly point people towards using Docker! Would an RPi3 take Nextcloud AND Docker?

There was only one way to find the answer: trying it out!

Update system

sudo apt update
sudo apt upgrade

Install dependencies

sudo apt install ca-certificates curl gnupg lsb-release

Although I didn't really need to install them, they were already installed in my system.

Set up to use Docker's APT repository

Some tutorials ask you to pipe the contents of a script in Docker's website to your bash. This makes me a bit queasy, and I normally always want to use APT repos or similar to keep systems up to date.

So the solution is to use Docker's APT repository.

First we create a directory for storing GPG keys:

sudo mkdir -p /etc/apt/keyrings

Then we get Docker's GPG key to that keyrings dir:

curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

And we add Docker's APT repository:

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Running that in my Raspberry Pi had the end result of generating a /etc/apt/sources.list.d/docker.list with these contents:

deb [arch=arm64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian bookworm stable

Then we update the repositories again so that Docker's is included in the list of things we can install or upload:

sudo apt update

Output should be something similar to this:

Hit:1 http://archive.raspberrypi.com/debian bookworm InRelease
Hit:2 http://deb.debian.org/debian bookworm InRelease                          
Hit:3 http://deb.debian.org/debian-security bookworm-security InRelease        
Hit:4 http://deb.debian.org/debian bookworm-updates InRelease                  
Get:5 https://download.docker.com/linux/debian bookworm InRelease [47.0 kB]    
Get:6 https://download.docker.com/linux/debian bookworm/stable arm64 Packages [38.1 kB]
Fetched 85.1 kB in 3s (33.2 kB/s)   
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
All packages are up to date.

Installing Docker packages

Now we can move on to actually installing Docker:

sudo apt install docker-ce docker-ce-cli containerd.io
sudo apt install docker-compose-plugin

Add our user to the docker group

Otherwise we won't have access to properly running Docker (root could).

sudo usermod -aG docker $USER

That adds the currently logged user to the docker group.

Bonus tricks:

  • who am i or whoami to check the current user
  • groups $USER to list the groups the current user belongs to
  • newgrp docker immediately logs the current user into the new group without logging in and out

Test docker setup

docker run hello-world

Should pull the hello-world image from the container registry and build and run it.

System config

We want Docker to start on boot:

sudo systemctl enable docker

To start it now:

sudo systemctl start docker

Check its status:

sudo systemctl status docker

Creating directories to store config and data files

mkdir ~/forgejo

Create compose.yaml to run Forgejo image with postgresql as the database backend

cd ~/forgejo
vim compose.yaml

Paste:

services:
  server:
    image: codeberg.org/forgejo/forgejo:10
    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=<DBPASSWORD>
    restart: always
    networks:
      - forgejo
    volumes:
      - <FORGEJODATAPATH>:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "3000:3000"
      - "222:22"
    depends_on:
      - db

  db:
    image: postgres:14
    restart: always
    environment:
      - POSTGRES_USER=forgejo
      - POSTGRES_PASSWORD=<DBPASSWORD>
      - POSTGRES_DB=forgejo
    networks:
      - forgejo
    volumes:
      - <POSTGRESDATAPATH>:/var/lib/postgresql/data

networks:
  forgejo:
    external: false

But replace:

  • <DBPASSWORD> with a random value (in both places in the file)
  • <FORGEJODATAPATH>: where in the Raspberry Pi file system you want the data files from forgejo, e.g. ./data/repos
  • <POSTGRESDATAPATH>: where in the Raspberry Pi file system you want the data files from the postgresql instance used for forgejo, e.g. ./data/database.

The data path values point to volumes that will persist i.e. they're outside the container.

For the volumes, the syntax is VOLUME:CONTAINER_PATH (ref).

Also: "the relative path is resolved from the Compose file’s parent directory". Meaning you don't need to specify the full path in the file system, only relative to the compose.yaml file.

Save and quit.

Starting up our compos-ition:

From the forgejo dir:

docker compose up -d

will start downloading packages and images and things ("pulling")

cloud@cloudberry:~/forgejo $ docker compose up -d
[+] Running 24/24
 ✔ server Pulled                                                         107.8s 
 ✔ db Pulled                                                             183.7s 
                                                                                
[+] Running 3/3
 ✔ Network forgejo_forgejo  Created                                        0.3s 
 ✔ Container forgejo-db-1   Started                                        5.0s 
 ✔ Container forgejo        Started  

Then we access the new forge by going to the browser. In my case its hostname is cloudberry, so I go to port 3000: http://cloudberry:3000.

This will show the initial set up screen, if everything went according to plan!

Note the port number is defined in the compose file, so you can change it if you don't like the default.

If you look in the data directories we created, there are already files that forgejo created inside the container:

~/forgejo/data/repositories $ ls -l
total 12
drwxr-xr-x 3 cloud cloud 4096 Apr 16 13:02 git
drwxr-xr-x 4 cloud cloud 4096 Apr 16 13:02 gitea
drwx------ 2 root  root  4096 Apr 16 13:02 ssh

Now, enter data and complete the initial set up.

The settings are also saved to the data dir, in ./data/gitea/conf/app.ini.

So you can edit them after the fact. The documentation's config cheat sheet might be handy.

Then you'll be shown the login page at http://cloudberry:3000/user/login, but you can't login because you don't have a user yet. So click the last link under the login form to create an account now.

Once you create an account, you can disable creating new accounts:

Change DISABLE_REGISTRATION = false to true in app.ini

Then... restart:

docker compose restart (it takes a while)

The registration link won't be shown.

The rest...

And now you can start creating repositories and setting up SSH keys and etc!

The responsiveness on the RPi3 so far is fairly good. Although admittedly I haven't used heavy repos yet, but I'm impressed.

How to make git+ssh connections work?

The container is listening in 22 and so it doesn't render the port number in the UI as it's the standard SSH port, but the setup uses 222 to forward external connections to the internal (container) SSH service.

I didn't want to include the port number in the URLs for the repos, so I created a new SSH key for connecting to the local forge (following my own instructions). Then added it via the settings in the forge.

Then in my computer I edited the .ssh/config file and added an entry for the local forge:

Host cloudberry
    IdentityFile ~/.ssh/id_ed25519_cloudforge_2025-04-16
    User git
    Port 222

Now if I try to connect, and after allowing the connection to proceed the first time, it works:

$ ssh cloudberry

The authenticity of host '[cloudberry]:222 ([192.168.123.456]:222)' can't be established.
ED25519 key fingerprint is SHA256:pKNyk/.....
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[cloudberry]:222' (ED25519) to the list of known hosts.

And confirmation that it works:

ssh cloudberry
PTY allocation request failed on channel 0
Hi there, sole! You've successfully authenticated with the key named local forge, but Forgejo does not provide shell access.
If this is unexpected, please log in with password and setup Forgejo under another user.
Connection to cloudberry closed.

I can now clone with git, without any port number in sight:

git clone git@cloudberry:sole/hello.git

Upgrading the forgejo version

I realised I had installed an old version (7) instead of the 10 I wanted to install. The dangers of copy paste!

The official documentation strongly suggests stopping everything and making a backup before upgrading, but this was installed not even a day ago, so I decided to embrace the yolo spirit, and:

  1. edited the compose.yaml file to change the version of the forgejo image, from 7 to 10. Saved the file.
  2. stopped the container: docker compose stop.
  3. PULLED the new image: docker compose pull.
  4. Rebuilt: docker compose build --no-cache.
  5. And finally start: docker compose start.

The compose pull bit was what was missing, otherwise (and it makes a lot of sense, but it is counterintuitive that it the changes are not picked up automatically) it just starts the container again but does not pull in and rebuild anything, so it feels as if nothing is really happening.

The forge seems to be working well... and if it isn't I will nuke everything and start again!

UPDATE: I realised it was actually not running an updated version as the reported version in the website was the old one (7). I fixed this by stopping the containers and rebuilding but with an added --no-cache. The instructions above are updated already, this note is just in case you already tried it and it didn't work for you.

Pending questions

  • How to use SSL (custom certificate?) for the web front? I have found this doc page but it looks a bit old, so I'm not sure if it's still the recommended way (I could not find any other mention to https or ssl in the other doc pages). I also do not know yet how to run gitea when running forgejo in a container, but I will figure this out eventually, as it's more of a Docker question.
  • Or... how to disable http based git cloning?
  • Also in general, how to control this docker-composed software. A review of the documentation and a few searches are in order!

Interesting things to explore

  • Setting up a runner so things can happen when actions such as code pushes take place on the repository. It's like GitHub actions but... well, it's not GitHub. It would be nice to do things like automating building static websites and deploying automatically, which I do manually now. It's not a great deal of effort because those things are scripted anyway, but it would be nice. However the instructions are scaring me away from running the runner on the same machine, so I'll leave this for later.
  • Auto mirroring repos: I could in theory work locally and have forgejo periodically sync to/from other repositories! This sounds like a great way of improving resilience and increasing federation (whichever order you want to put them on).

Have fun forging!

Sources

https://phoenixnap.com/kb/docker-on-raspberry-pi https://pimylifeup.com/raspberry-pi-forgejo/