Setting up Forgejo on Alpine with Nginx and Fail2ban

March 30, 2025 :: 13 min read

Introduction

Forgejo [1] is an awesome code hosting platform. It is a community-driven hard fork of Gitea, that is being developed over at Codeberg [2]. Self-hosting Forgejo is not that difficult, and the documentation explains everything we need to know in order to do so.

However, as some of you may know, I am not really a fan of systemd. In fact, I tend to avoid it as much as possible: I use Artix Linux [3] (a systemd-free version of Arch Linux) on my personal computer, and Alpine Linux [4] on my VPS. Both run without systemd and use OpenRC [5] instead (in reality, Artix supports many alternatives to systemd, but my heart is set on OpenRC). If you’ve never used OpenRC, you should really give it a try.

In this article, I will provide a complete guide for setting up a self-hosted Forgejo instance on an Alpine Linux system, behind an nginx reverse proxy with TLS. Additionally, I will explain how to secure the instance against brute-force login attempts, using Fail2ban.

Note that the commands are all prefixed with doas when requiring root access in this tutorial. This is because Alpine uses doas as default. If you use another privilege elevation tool (such as sudo), you should prefix the commands with the correct executable instead.

Disclaimer

Please note that although this blog post contains many relevant tips and tricks, it is not official documentation of any kind. Please consult the Forgejo documentation [6] if you want to set up your own Forgejo instance. While the information in this post may be correct at the time it was written, it may not be up-to-date with future releases of the software.

Installing the dependencies

Before getting into anything technical, we need to install the packages that will be used in this installation. To do so, we have to use the Alpine Package Keeper, apk:

doas apk add git git-lfs nginx fail2ban certbot certbot-nginx postgresql

Installing Forgejo

Since Alpine Linux 3.21, Forgejo is available in the community repository. However, for prior versions of Alpine, this is not the case. In this post, the installation will be performed using the Forgejo binary that is available on Codeberg. Instructions for downloading the binary and verifying its authenticity (this is important!) are available on the Forgejo website [7].

Once the binary is downloaded and verified, we can install it into /usr/local/bin. To do that, let’s run:

doas install -Dm755 forgejo-x.y.z-linux-amd64 /usr/local/bin/forgejo

If you’re not familiar with the install command, the instruction written above is more or less a shorter way to do the following:

# create the /usr/local/bin directory if not present
doas mkdir -p /usr/local/bin
# copy the executable into /usr/local/bin
doas cp forgejo-x.y.z-linux-amd64 /usr/local/bin/forgejo
# make the forgejo executable by all users and writable by root only
doas chmod 755 /usr/local/bin/forgejo

Setting up the system

Next, we need to add a new system user for running Forgejo on the system. In terms of security, it is a good practice to run each service running on a host with a different user (and when possible, not root itself!). Here, we want the new user to be called git, because this is the user that Forgejo will be using for SSH access. To create this new user, we need to run:

doas useradd --home-dir /home/git --create-home \
  --system --shell /bin/sh \
  --gid git git

Next, we need to create the directories used by Forgejo: i.e., the data directory, as well as the configuration directory. First, let’s create the data directory:

doas mkdir -p /var/lib/forgejo

Next, we need to change the owner of this directory. Currently, the owner is root, but we want the system user git to be the owner. To change the owner, we use:

doas chown git:git /var/lib/forgejo

Now that the owner is changed, we want to change the permissions on this directory. We want the user git to have full (read+write) access, any member of the git group to have read-only access, and other users to have no access to the directory at all. That means we need to change the directory to have mode 750. Let’s do that:

doas chmod 750 /var/lib/forgejo

Next, we need to create the directory for Forgejo’s configuration file. We do that by running:

doas mkdir /etc/forgejo

Like we did before, we need to change the owner and permissions of this directory. This time, however, we will only change the group owner, but will leave root as the owner of the directory. We will then give read and write access to root and any member of the git group.

doas mkdir /etc/forgejo
doas chown root:git /etc/forgejo
doas chmod 770 /etc/forgejo
As explained in the Forgejo documentation [8], the git group needs write access to the configuration only during the initial setup. Afterwards, we can change the permission to read-only for the git group.

Creating the PostgreSQL database

In this article, we will use PostgreSQL as the database backend for Forgejo. Instructions for other databases, as well as PostgreSQL, are all available in the Forgejo documentation [9].

First of all, if it isn’t already the case, we need to add the postgresql system service to the default runlevel:

doas rc-update add postgresql

Once this is done, we can then start the service using:

doas rc-service postgresql start

Next, we want to create a database for Forgejo. To do that, let’s execute the command psql as the user postgres:

doas -u postgres psql

Now that we’re inside PostgeSQL’s shell, we can first create a new PostgreSQL user, that will be the owner of the database used by Forgejo. Similarly to system users, it is also recommended to have separate owners for different databases. To create a new user, we execute:

CREATE USER forgejo WITH ENCRYPTED PASSWORD 'SomeSecurePassword';

Remember to change the password to a strong one, instead of executing exactly what is written above. Then, we create a new database and set the owner to be the forgejo user that we just created:

CREATE DATABASE forgejo WITH OWNER forgejo;

We can now exit the shell using CTRL-D or \q.

Configuring the nginx reverse proxy

Now that the database is ready, we need to configure the reverse proxy, using nginx. Using a reverse proxy has many advantages. Most importantly, it makes it possible to serve multiple services on a single server without having to use non-standard ports. It also makes it way easier to set up TLS.

Fortunately, the Forgejo documentation [10], once again, has detailed instructions for this.

Before beginning, if not already done, we have to add the nginx service to the default runlevel and start it:

doas rc-update add nginx
doas rc-service nginx start

Now, let’s start a text editor (in restricted mode) in order to write our nginx configuration file:

doas rvim /etc/nginx/http.d/forgejo.conf

Inside the file, we have to write the following content (that comes directly from the corresponding Forgejo documentation [10]):

# Source of this file: https://forgejo.org/docs/latest/admin/reverse-proxy/
server {
    listen 80; # Listen on IPv4 port 80
    listen [::]:80; # Listen on IPv6 port 80

    server_name git.example.com; # Change this to the server domain name.

    location / {
        proxy_pass http://127.0.0.1:3000; # Port 3000 is the default Forgejo port

        proxy_set_header Connection $http_connection;
        proxy_set_header Upgrade $http_upgrade;
        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 512M;
    }
}

Now that our configuration is written, we can exit and save the file (for people who are not used to vim, we do that by typing :wq).

Next, we can reload nginx so that it reads our new configuration:

doas nginx -s reload

Now, if we try to visit our domain name using a web browser, we’ll notice two things:



This is perfectly normal! In fact, we haven’t setup TLS yet, which is why our connection isn’t secure. The 502 error is caused by the fact that Forgejo isn’t yet running behind the reverse proxy.

Adjusting the firewall

If we are unable to connect, this could be caused by the fact the we forgot to allow TCP connections to ports 80 and 443 on our server. Since this is required for HTTP(S) traffic, we should open these ports before going any further. Instructions to do so vary, depending on the firewall in use by the system. For the ufw firewall, the commands for opening ports 80 and 443 are as follow:

doas ufw allow 80/tcp
doas ufw allow 443/tcp

Generating certificates and configuring access over HTTPS

To be able to access our Forgejo instance securely over HTTPS, we can use the certbot utility that we installed earlier. It will create, sign and install Let’s Encrypt certificates, and then update our nginx configuration in order to use these certificates.

doas certbot --nginx -d git.example.com -v

Once this command succeeds, we should be able to access our server over HTTPS, using the domain name specified in the nginx configuration and the certbot command.

Writing a Forgejo system service for use with OpenRC

Now that our reverse-proxy configuration is done, we still have to configure Forgejo. To do that, we need to start Forgejo for the first time. However, since Alpine Linux uses OpenRC instead of systemd to manage system services, we can’t use the recommended systemd service to run Forgejo. Instead, we need to use an OpenRC init script (we have used that many times before, when using the commands rc-update and rc-service).

Fortunately, I wrote my own [11], that is based on the Artix Linux init script for Gitea. We can start a text editor to edit the following file:

doas rvim /etc/init.d/forgejo

In the file, we need to write the following content:

#!/sbin/openrc-run
# This file is licensed under GPLv2

supervisor=supervise-daemon
name=forgejo
command="/usr/local/bin/forgejo"  # change this accordingly to the location of your executable
command_user="${FORGEJO_USER:-git}"
command_args="web --config '${FORGEJO_CONF:-/etc/forgejo/app.ini}'"
supervise_daemon_args="--env GITEA_WORK_DIR='${FORGEJO_WORK_DIR:-/var/lib/forgejo}' --chdir '${FORGEJO_WORK_DIR:-/var/lib/forgejo}' --stdout '${FORGEJO_LOG_FILE:-/var/log/forgejo/http.log}' --stderr '${FORGEJO_LOG_FILE:-/var/log/forgejo/http.log}'"
pidfile="/run/forgejo.pid"

depend() {
  use logger dns
  need net
  after firewall postgresql mysql
}

We then need to write the configuration for the init script. We can open again a text editor:

doas rvim /etc/conf.d/forgejo

And write the following content into it:

# This file is licensed under GPLv2
FORGEJO_USER=git
FORGEJO_CONF=/etc/forgejo/app.ini
FORGEJO_WORK_DIR=/var/lib/forgejo
FORGEJO_LOG_FILE=/var/log/forgejo/http.log

Finally, we have to make the init script executable. For that, we will give it mode 755:

doas chmod 755 /etc/init.d/forgejo

Once this is done, we can add the Forgejo service to the default runlevel, and then start the service.

doas rc-update add forgejo
doas rc-service forgejo start

Forgejo should now be running. If we visit our server again using a web browser, we should have working HTTPS, and no longer see a 502 error, but the initial configuration page of Forgejo instead.

Initial Forgejo configuration

Now that we see the initial Forgejo configuration page, we need to enter the correct values everywhere. Here are the values we need to use with our current setup:

Database settings







General settings









It could also be just the right time to create an administrator account.

Tweaking Forgejo logging

Now that Forgejo is running, we need to tweak its configuration a little bit, in order to configure fail2ban. What we need to do is to log failed authentication attempts. To do that, we need to write the following lines into Forgejo’s configuration file:

[log]
MODE = console, file
LEVEL = info
logger.access.MODE = access-file
ENABLE_SSH_LOG = true
ROOT_PATH = /var/lib/forgejo/log

[log.file]
MODE = file
LEVEL = warn
FILE_NAME = gitea.log

[log.access-file]
MODE = file
LEVEL = info
FILE_NAME = access.log

What this will do is that it will keep an access log under /var/lib/forgejo/log/access.log. It will also log all warnings into /var/lib/forgejo/log/gitea.log. This second file is the one that we will use with fail2ban, since failed authentication attempts are logged with a warning severity.

For the settings to apply, we need to restart Forgejo, which we can do using OpenRC:

doas rc-service forgejo restart

Configuring Fail2ban

Fail2ban is a powerful software, that detects and prevents intrusion. It works by reading log files of various services and identifying failed authentication attempts in them. When too many attempts are made from a specific IP address, Fail2ban temporarily blocks this IP address for the whole system.

In order for Fail2ban to work with our Forgejo installation, a few more steps are required. First, we need to define a regex rule that will match the failed authentication attempts in the Forgejo logs. To do that, we can start a text editor:

doas rvim /etc/fail2ban/filter.d/forgejo.conf

In it, we need to write the following content:

# forgejo.local
[Definition]
failregex =  .*(Failed authentication attempt|invalid credentials|Attempted access of unknown user).* from <HOST>
ignoreregex =

Now that this is done, we need to configure the Fail2ban jail (i.e. the duration of the ban, the number of failed authentications that trigger the ban, etc.). Again, let’s start a text editor:

doas rvim /etc/fail2ban/jail.d/forgejo.local

And write the following content:

[forgejo]
enabled = true
filter = forgejo
logpath = /var/lib/forgejo/log/gitea.log
maxretry = 10
findtime = 10m
bantime = 1h

This configuration will block for one hour any IP address that makes 10 failed authentication attempts in a period of 10 minutes. Depending on your threat model and on whether you are running a single-user instance or not, these values can be changed to more aggressive values.

Now that Fail2ban is correctly configured, we still have to add its service to the default runlevel and start it:

doas rc-update add fail2ban
doas rc-service fail2ban restart

We can now check whether our Fail2ban configuration was correctly read by Fail2ban, by executing this command, which gives us an overview of the configured jails, as well as the different IP addresses that are blocked by them:

doas fail2ban-client banned

The output should look like this (but maybe there’s already IP addresses in it!):

[{'sshd': []}, {'forgejo': []}]

Troubleshooting

Depending on your ssh and system configurations, it is possible that accessing repositories over ssh doesn’t work, even though everything is configured correctly. If that’s the case, the problem is probably that the system user git is considered locked because it doesn’t have a password. Setting a password for the git user usually fixes this problem:

doas passwd git

Conclusion

As I’ve said before, this is not official documentation, so please do not copy-paste the code written here to execute it directly on your server. Some commands or configurations may change with time, and this article will at some point have some outdated instructions (by the way, if that is the case, do not hesitate to contact me to let me know).

I’d also like to thank the Forgejo developers and anyone else involved in the project, because not only is Forgejo a truly essential piece of software, it also is an actually good software that is easy to use, to install, and that has a very clear documentation as well (which unfortunately is quite rare).

Footnotes

[1] Forgejo website

[2] Forgejo on Codeberg.org

[3] Artix Linux website

[4] Alpine Linux website

[5] OpenRC on Gentoo Wiki

[6] Forgejo documentation

[7] Forgejo installation page

[8] Installation from binary - Forgejo documentation

[9] Database preparation - Forgejo documentation

[10] Reverse proxy - Forgejo documentation

[11] forgejo-openrc on the AUR


Source