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:
- The connection is not secure (it uses HTTP and not HTTPS)
- We will get a 502 Bad Gateway error
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
- Database type: PostgreSQL
- Database host: 127.0.0.1:5432
- Database username: forgejo
- Database password: SomeSecurePassword (change this to your actual password)
- Database name: forgejo
- Database SSL: Disable (because it is a local database)
General settings
- Repository root path: /var/lib/forgejo/data/forgejo-repositories
- Git LFS root path: /var/lib/forgejo/data/lfs
- User to run as: git
- Server domain: git.example.com (change this to your actual domain, it must be the same as the one specified in the nginx configuration)
- SSH server port: 22
- HTTP listen port: 3000 (we need to keep this because our reverse proxy is the one listening to ports 80 and 443)
- Base URL: https://git.example.com/ (change this to your actual domain, it must be the same as the one specified in the nginx configuration)
- Log path: /var/lib/forgejo/log (we will use that later with fail2ban)
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
[8] Installation from binary - Forgejo documentation
[9] Database preparation - Forgejo documentation
[10] Reverse proxy - Forgejo documentation
[11] forgejo-openrc on the AUR
Source