In a previous article, I described my first steps with Forgejo-aneksajo, and deploying it on my Raspberry Pi. This got me so excited that I started looking into deploying it on more machines. However, I quickly realized that the Docker-based approach I had used originally was not going to yield a setup that I would see myself wanting to maintain for an extended period of time.
I knew I wanted something that I could completely apt install
from
Debian, that is maintained there as a whole, and not
pieced together from only loosely connected components. With that in mind,
I spent a few days reading and learning about my options.
I will put the conclusions upfront: Podman is an amazingly flexible solution that is neatly integrated with systemd, and just the two together provide everything needed.
Put simply, systemd is the tool that is used to manage pretty much anything there is to manage on a modern Linux system. Whatever machine Forgejo will need to run on for me, systemd is already a given.
Podman is in many ways like Docker, but a Docker that lets containers feel like normal processes, although not as much as with Singularity. It is daemonless, and fully supports running containers as a normal user (“rootless”). Podman knows directly how to handle images, containers, and groups of related containers (pods). There is a lot to say about podman, for which there is no space here. A good place to start reading is redhat.com – they are a driving force behind the podman development. I do want to note that podman can even work with SIF-formatted containers, a technology that is widely used for scientific and HPC computing.
Deployment target
In the following, I will describe an approach to deploying Forgejo that
- runs the service entirely in users space
- uses systemd to start, stop, and monitor the service
- exposes the service via a reverse proxy configuration (like
https://git.example.com
) - uses “SSH-passthrough” to avoid running a Forgejo-internal SSH server and having to expose it in addition to an SSH server that is already run on the host
- builds the Forgejo-aneksajo container image from source, but otherwise only uses tools that are shipped with Debian 12+, or can be installed immediately from Debian’s official package repository
- works on Pi OS, and off-the-shelf Debian 12 images offered by cloud/VPS providers.
Debian 12 provides podman version 4.3. However, there have been substantial improvements of the systemd integration from version 4.4 onwards. In anticipation of a future upgrade (Debian 13 is likely to ship with podman v5), we will also be taking a brief look at these new features.
Software dependencies
For the setup described here, we need the following packages (all available via
apt install
from Debian) in addition to systemd – which is assumed to be
available already.
podman
to build and run containerscaddy
to serve as a reverse proxy for Forgejo. Any other solution, say nginx, should work too, but is not described here.
User setup
As a first step, we will create a dedicated git
user on the target system.
This user will not only run the Forgejo service, but it will also be means to
provide SSH-based access to it, through the host system.
For clarity, we will perform all setup steps from a normal user account on the
target machine, and use sudo
whenever elevated permissions are needed.
We are creating a regular (non-system) git
user, planing to put all
Forgejo data under /home/git
, so we can use adduser
as simple as:
sudo adduser git --disabled-password
If we would leave out --disabled-password
, we could set a password, and
enable pushing an existing Forgejo container from another machine directly into
this user account via something like (replace example.com
with the target
host address):
podman image scp localhost/forgejo-aneksajo:7-latest-rootless git@example.com::
But for this article, we will be building the container directly on the target machine.
At this point it is worth checking, if the git
user is properly configured for
user namespace mapping. This is essential for running rootless containers,
where processes inside the container need to operate under different
users and groups. Both /etc/subuid
and /etc/subgid
must contain a mapping
definition for the git
user at this point (actual values are system-dependent).
❯ grep 'git' /etc/subuid
git:265536:65536
❯ grep 'git' /etc/subgid
git:265536:65536
This article has some more background on user namespaces.
We plan to run the container as the git
user, via systemd. This requires that
systemd is running a controlling process for this user, even when the user is not
logged in. This is achieved via systemd’s user lingering:
sudo loginctl enable-linger git
This will also cause a working directory for the git
user to be available
at /run/user/$UID
.
This directory must be “announced” via the XDG_RUNTIME_DIR
environment
variable for systemd to work properly for a user. To have that happen
automatically, put the following snippet into the git
user’s shell setup.
if [ -z "${XDG_RUNTIME_DIR}" ]; then
XDG_RUNTIME_DIR=/run/user/$(id -u)
export XDG_RUNTIME_DIR
fi
Depending on the shell that is being used (e.g., after running sudo -u git -s
from a different user account), it would need to go into something like
/home/git/.zshenv
for zsh, or /home/git/.bashrc
for
bash. Importantly, do not
put it in any of the “profile” files. Those are for login shells, and we won’t be
using those for this purpose.
In this article, I will nevertheless show export
commands for XDG_RUNTIME_DIR
whenever they are needed. A reader that did the above setup can safely ignore them.
Service configuration
I want all configuration for the Forgejo setup to be underneath /etc
, such
that it is tracked together with all the other configuration of that machine by
etckeeper. I will not talk about this tool,
but it is indispensable for me.
All configuration will be placed under /etc/forgejo
sudo mkdir -p /etc/forgejo
SSH-passthrough
We want SSH-based access to Forgejo to go through the git
user and the SSH
server running on the host machine. This allows for disabling Forgejo’s own
SSH server, and avoids having to expose a dedicated port for it. Any user will
use the generic git@<host>/...
to access Forgejo, and only Forgejo will
determine whether a particular repository may be accessed, based on the used
SSH-key. The gitea
documentation
has some more information on this.
To make this work, we will create a small script that will serve as a
restricted login shell for the git
user. We place this under /etc
too,
because its purpose is to relay commands to the Forgejo service container,
identified by its name forgejo
, which needs to be kept in sync with the rest of the
setup.
sudo mkdir /etc/forgejo/bin
# create the "login shell"
cat << EOT | sudo tee /etc/forgejo/bin/forgejo-shell
#!/bin/sh
#
podman exec -i --env SSH_ORIGINAL_COMMAND="\$SSH_ORIGINAL_COMMAND" forgejo sh "\$@"
EOT
# make executable to function as a login shell
sudo chmod +x /etc/forgejo/bin/forgejo-shell
# set as login shell for the `git` user
sudo usermod --shell /etc/forgejo/bin/forgejo-shell git
The second key aspect of the SSH-passthrough setup is to relay access
authorization for the git
user to Forgejo in the container. For that, we
place a configuration drop-in into the host’s sshd
configuration, calling
gitea keys inside
the container to act as an
AuthorizedKeysCommand.
cat << EOT | sudo tee /etc/ssh/sshd_config.d/forgejo-ssh-passthrough.conf
Match User git
AuthorizedKeysCommandUser git
AuthorizedKeysCommand /usr/bin/podman exec -i forgejo /usr/local/bin/gitea keys -c /etc/gitea/app.ini -e git -u %u -t %t -k %k
EOT
The SSH server on the host needs to be restarted to load the new configuration.
sudo systemctl restart sshd
Reverse-proxy setup
Eventually, the Forgejo container will run and offer http access via port 3000
.
We will tell podman to expose this port also on the host’s port 3000
. This makes
Forgejo accessible via http://localhost:3000
, or whatever hostname identifies the
machine.
However, we want to have proper HTTPS and use something like
https://git.example.com
, where example.com
is replaced with whatever your
domain is. This is (most) easily achieved with Caddy,
including automated certificate generation for https
.
First point your domain DNS configuration for host git
to the server’s (IP)
address. Afterwards, sudo apt install caddy
, add the following to
/etc/caddy/Caddyfile
and sudo systemctl reload caddy
afterwards (again
replacing example.com
with your domain).
# forgejo
git.example.com {
reverse_proxy localhost:3000
}
Caddy with provising the necessary certificate automatically.
Obtain a Forgejo-aneksajo container image
For the moment, images for a recent version are available from Docker
Hub (for architectures
amd64
and arm64
). Get one as the git
user via, for example
podman pull docker.io/mihanke/forgejo-aneksajo:7-latest-rootless
However, if you do not have a want an account on Docker Hub, or a good enough
version is not available (yet), it is straightforward to build one from source.
The following script does exactly that. Importantly, we will build the image directly
with the git
user.
# become the `git` user
sudo -u git -s
# clone the Forgejo-aneksajo sources into a `src/` directory
mkdir ~/src
git clone https://codeberg.org/matrss/forgejo-aneksajo.git ~/src/forgejo-aneksajo
# check out the release we want to build
git -C ~/src/forgejo-aneksajo checkout v7.0.5-git-annex1
# set up and check that systemd is functional for the `git` user
export XDG_RUNTIME_DIR="/run/user/$UID"
systemctl --user status
# build the image
podman build \
--tag forgejo-aneksajo:7.0.5-rootless \
--tag forgejo-aneksajo:7-latest-rootless \
-f ~/src/forgejo-aneksajo/Dockerfile.rootless
The podman build
call points to Dockerfile.rootless
. This is important.
The “rootless” container setup works substantially different compared to the
regular one. Only the “rootless” setup is covered here!
Initial Forgejo launch
For the initial start of Forgejo, we will make the configuration directory
writable for the git
user, because Forgejo will place a default configuration
at /etc/forgejo/app.ini
, and it will behave differently when this file
already exists, and fail when the directory is not writable. Later, we will
remove these permissions again.
sudo chown git: /etc/forgejo
We could also immediately deposit a complete configuration at
/etc/forgejo/app.ini
. However, the initial configuration wizard will pick up
some environment properties (this is the reason why we configured caddy
already), and yield a functional setup that needs minimal adjustment. It will
also produce a configuration suitable for the deployed Forgejo version, even
some time in the future for a different Forgejo release.
The following script will ready the environment for Forgejo,
create a container, generate a systemd service unit for it,
and start the container (via systemd) for the first time.
All of this is done with the git
user.
A couple of things are important:
- we create a
gitea/
and agit/
directory that will contain Forgejo instance related information separate from the Git repositories host by that instance. The latter will just be a standard collection of bare Git repositories (with an annex). - we bind the host’s configuration at
/etc/forgejo
to the location Forgejo expects in the container - the name of the container is set to
forgejo
, while the corresponding systemd service is calledcontainer-forgejo
. - the
forgejo
container will only expose port3000
for http(s) access and the web UI. - we map the
git
user on the host system to thegit
user inside the container (it hasuid=1000
), such that any files created by it within the container, are owned by the use running the container on the host. In other words, all files created will be owned by thegit
user on the host. - many more aspects can be configured here, for example, a network setup.
Adding an option like
--network=slirp4netns:allow_host_loopback=true
topodman container create
would allow the forgejo container to access the host’s loopback network interface. This would enable the built-in email functionality to use a host’s SMTP server to send email via an institutional relay server.
# the following code runs as the `git` user
sudo -u git -s
# create the directory to host the Forgejo instance data
# and the repositories
mkdir ~/gitea ~/git
# `systemctl` needs `XDG_RUNTIME_DIR`
export XDG_RUNTIME_DIR="/run/user/$UID"
# create the container
podman container create \
--name forgejo \
-p 3000:3000 \
-v "$HOME/gitea:/var/lib/gitea:Z" \
-v "$HOME/git:/var/lib/gitea/git:Z" \
-v '/etc/forgejo:/etc/gitea:Z' \
--userns 'keep-id:uid=1000,gid=1000' \
localhost/forgejo-aneksajo:7-latest-rootless
# generate a systemd service unit from the container
mkdir -p ~/.config/systemd/user
( cd ~/.config/systemd/user \
&& podman generate systemd \
--restart-policy=always --new --files --name forgejo )
# we only needed the container to create the service unit.
# otherwise, we create a new one each time, no need to keep this one
# around
podman rm forgejo
# start for the first time
systemctl --user start container-forgejo
If this looks a bit clunky to you, you are not alone with this opinion, and you’ll be glad that starting with podman version 4.4+ this can be done much nicer.
For podman v4.3, we create a container, similar to what would need to be done
with Docker, but only to generate a systemd service unit from the setup.
Afterwards, we can immediately remove the container again, because the service
unit will create a new one, each time it starts (--new
). Take a look at the
.service
file generated in .config/systemd/user/
. In case something does
not work when launching the container, it can help to comment out the
Restart=
line for better error reporting.
Once the container is running, open a webbrowser and point it to the web UI.
With the reverse-proxy setup done as described above, go to
https://git.example.com
(replacing example.com
with your domain). This
will bring up the setup wizard. All values can be kept at their default to
configure a Forgejo instance that uses SQLite as a database. A setup with a
dedicated database server/container is out of scope for this article. That being said,
a podman “pod” would be a suitable vehicle to implement this.
Now push the button to initialize the Forgejo instance, which will populate the
gitea/
directory in the git
user’s home directory.
Go ahead and register a first user via the web UI now. This user will automatically get administrator status. More users can be elevated to this role later on.
At this point, we can stop the container again:
systemctl --user stop container-forgejo
(executed as the git
user).
Finalizing
Initializing the Forgejo instance has created a default configuration at
/etc/forgejo/app.ini
, which is already sensible, but can be tuned further. A
discussion of all options is out of scope here. Use the Forgejo
documentation as a
starting point to learn about the possibilities.
That being said, here are a few configuration snippets that are noteworthy, and can be added/adjusted as desired.
[annex]
ENABLED = true
This enables git-annex support. Without it, Forgejo-aneksajo behaves just like a vanilla Forgejo.
[server]
DISABLE_SSH = false
; we use ssh-passthrough, no need for the internal ssh server
START_SSH_SERVER = false
; remove custom port from displayed URLs
SSH_PORT = 22
With the SSH-passthrough configuration targeted here, we can disabled Forgejo’s
internal SSH server entirely. However, we do not want to disable its SSH
features entirely! Setting the SSH_PORT
to the standard 22
causes Forgejo
to stop putting a custom port into the SSH URLs it displays. This is
important, because SSH-access is happening via the SSH server running on the
host, on that port 22
.
[service]
DISABLE_REGISTRATION = true
Disabling the registration feature can be a good idea. Accounts can still be created by an admin user via the web UI. This is likely sufficient for a small user base, and avoids having to deal with undesired sign-ups by random agents.
Now it is a good idea to restrict the access to the configuration again.
sudo chown root:git /etc/forgejo/app.ini
sudo chmod 640 /etc/forgejo/app.ini
We make the configuration file writeable by root
only, and readable by the
git
group (the main group of the git
user) only. This prevents modification
of the configuration by the user that runs the Forgejo service, and also makes
adding secrets to the configuration file slightly more secure. Adding a secret
may be necessary for setting up email SMTP server access.
At this point, and as the git
user, we can tell systemd to launch the
container at boot, and restart it now.
systemctl --user enable container-forgejo
systemctl --user start container-forgejo
The service should now be fully functional. It is a good idea to test the essentials:
- reboot the machine and make sure the container is started automatically by
and for the
git
user - add an SSH-key for a Forgejo user via the web UI, and test cloning and push from another machine
Setup with podman v4.4 and later
Podman v4.4 introduced Quadlets, which address some of the clunkier parts of the setup procedure shown above.
With quadlets it is no longer necessary to generate a systemd service unit from an existing podman container, and then place it into a location recognized by systemd, to be able to enabled the service.
Instead, podman ships a systemd generator that auto-generates transient service units from quadlet files. These files are similar to service units in syntax and semantics. Quadlets can define containers, volumes, networks, and more. They more-or-less replace “compose” files, but in a way that is tightly integrated with systemd.
A proper discussion of this feature is well beyond the scope of this article. However, here is a small example that hopefully illustrates how nice it is.
Quadlets for user-space containers can be placed into a number of locations
(see the
documentation),
including underneath /etc
, such that they are also within etckeeper
’s
reach.
sudo mkdir -p /etc/containers/systemd/users/$(id -u git)
Now we can place a container definition into this directory. We name the
file container-forgejo.container
. Only the .container
extension makes it
a container specification. We only add the container-
prefix to keep
the name of the result service unit the same as with the podman v4.3 setup.
cat << EOT | sudo tee /etc/containers/systemd/users/$(id -u git)/container-forgejo.container
[Unit]
Description=Forgejo-aneksajo container
Wants=network-online.target
After=network-online.target
[Container]
ContainerName=container-forgejo
# image to use for the container
# if the image is not available locally already, it is pulled, so
# something like the following is also possible
#Image=docker.io/mihanke/forgejo-aneksajo:7-latest-rootless
Image=localhost/forgejo-aneksajo:7-latest-rootless
PublishPort=3000:3000
Volume=%h/gitea:/var/lib/gitea:Z
Volume=%h/git:/var/lib/gitea/git:Z
Volume=/etc/forgejo:/etc/gitea:Z
# the user running 'git' forgejo inside the container is 1000:1000, we
# map it to the user running the forgejo container on the host
UserNS=keep-id:uid=1000,gid=1000
Network=host
[Service]
Restart=always
TimeoutStartSec=900
WorkingDirectory=%t
[Install]
# Start by default on boot
WantedBy=default.target
EOT
The content shown above roughly matches the container setup we used before. It looks
like a service unit specification and can use all of systemd’s features (templating, placeholders, etc.). Only the [Container]
is specific to this quadlet.
This quadlet makes any direct use of podman commands shown above fully
obsolete. With this file in place (and the old service unit removed), the git
user can systemctl --user start|stop container-forgejo
immediately. No
static .service
file will be generated anymore. It is also not necessary to
explicitly enable
the container service. This is handled automatically by
systemd’s dependency management.
Conclusions
The setup describe here is a lot nicer than the one I created
originally. Not only are all software dependencies
outside the container coming from Debian directly, but also the integration
with the host system is much better. The containerized Forgejo service
is managed by systemd in the same way all other services are managed.
Forgejo runs under a non-privileged host account, and the SSH-access via that
git
user feels just like one is used to from GitHub/Lab and friends.
I will likely revisit the Forgejo setup again when it is time to deploy a runner for its actions. But for now I am rather happy how this turned out.
One could argue that it is no less difficult to understand enough of systemd to be able to do this, than would be for Docker. However, I think investing the learning into systemd has benefits well beyond the world of containers. It was well worth the time for me.
Thanks to Adina Wagner, and Matthias Riße for providing feedback on a draft of this post.