Back to projects
Running Forgejo on a VPS
Mar 15, 2026
9 min read

Forgejo on a VPS

Setting up a VPS and running Forgejo on it

In this project, I will be deploying Forgejo on a Virtual Private Server (VPS). Forgejo is a self-hosted Git server that allows you to manage your own Git repositories. It is a great alternative to GitHub and GitLab.

You can poke around at git.jackwaterloo.com to see what it looks like before you start.

What you need to follow along

  • A Virtual Private Server (VPS)
  • A domain name
  • A way to connect to the VPS

What I used to set up the server

Make sure you have all the above items before you start.

Pretty much all of my directions will be for AlmaLinux 9 or RHEL based distributions. If you are using a different distribution, you will need to adjust the commands accordingly.

DNS Setup

I like to set up DNS first so we can access the server by domain name instead of by IP. You can do this later, but doing it early avoids using the IP to connect. After purchasing a domain, add the DNS records at your registrar or a DNS provider (I use Cloudflare). You will also need your VPS’s IP from your provider.

Add a DNS A record for your domain pointing to the VPS’s IP address.

Cloudflare DNS setup
Screenshot of the Cloudflare DNS setup

Securing Server

I received most of this information from this blog post that I found from Dreams of Code Youtube channel.

You should have received a default root password when you first signed up with your VPS provider.

Change the root password first to reduce brute-force risk, since default passwords are often well known. Run:

Terminal window
passwd root

Enter the new password when prompted and confirm it.

To add another layer of security, create a dedicated user with sudo for daily use. That way you avoid using root for normal tasks, and sudo will require a password. I use the username jack.

Terminal window
sudo useradd -m -s /bin/bash jack # create new user
sudo usermod -aG wheel jack # add to wheel group for sudo
# wheel is a group that has sudo privileges in RHEL distributions.

Set the password for the new user:

Terminal window
passwd jack

We will use this user for SSH, so create an SSH key on your local machine:

Terminal window
ssh-keygen -t ed25519 -C "jack@vps" # -C is the comment for the key. Name it something descriptive.

This creates an SSH key pair in ~/.ssh.

Copy the public key to the server:

Terminal window
ssh-copy-id -i ~/.ssh/id_ed25519.pub jack@git.example.com

Log back into the server:

Terminal window
ssh jack@git.example.com

With the public key on the server, restrict SSH to key-based auth, disable root login, and disable Kerberos to speed up logins. On RHEL, use a drop-in under /etc/ssh/sshd_config.d/. For example: nano /etc/ssh/sshd_config.d/01-custom-security.conf

/etc/ssh/sshd_config.d/01-custom-security.conf
# 1. Force SSH Key usage and disable passwords
# This overrides any 'yes' settings in later files
PasswordAuthentication no
AuthenticationMethods publickey
# 2. Disable root login for security
PermitRootLogin no
# 3. Performance: Disable Kerberos lookups to speed up login
GSSAPIAuthentication no

The 01 prefix makes this file take priority over higher-numbered files in the same directory.

Confirm your public key is on the server:

Terminal window
cat ~/.ssh/authorized_keys

Test the SSH config syntax (the output will show if settings are applied):

Terminal window
sudo sshd -t

Restart sshd to apply the changes:

Terminal window
sudo systemctl restart sshd

SSH is locked down; next, add a firewall. AlmaLinux 9 does not install one by default. IONOS provides a gateway firewall for the server, but I prefer to manage one on the host. Firewalld is the default on RHEL. Install it with:

Terminal window
sudo dnf install -y firewalld

Open SSH, HTTP, and HTTPS so the server can accept that traffic:

Terminal window
# Open SSH, HTTP, HTTPS (persistent)
sudo firewall-offline-cmd --add-service={ssh,http,https}

Start and enable firewalld:

Terminal window
sudo systemctl start firewalld && sudo systemctl enable firewalld

Now that the server is secure, we can start the setup for Forgejo.

Forgejo Setup

We will deploy Forgejo with Docker. Install Docker using the official docs; for AlmaLinux, use the RHEL instructions.

Forgejo will run as a dedicated user inside the container; that same user is used for cloning and pushing. To avoid using root or a sudo-capable user, create a git user (a common convention for Git servers). Create it with:

Terminal window
# Dedicated 'git' system user for Forgejo (or similar git servers):
# - --system: system user (non-login, lower UID)
# - --create-home: create home dir if missing
# - --home-dir /home/git/: home directory path
# - --shell /bin/bash: login shell
# - --comment "Git Version Control": user description
# - --user-group: group 'git' with this user as sole member
# - --no-log-init: no skel files in home (minimal env)
# - --password *: disables password login for this user
sudo useradd \
--system \
--create-home \
--home-dir /home/git/ \
--shell /bin/bash \
--comment "Git Version Control" \
--user-group \
--no-log-init \
--password "*" \
git
sudo usermod -aG docker git # add git to the docker group

Create the directory (sudo mkdir -p /home/jack/forgejo/) and add a Docker Compose file (I got it from the Forgejo site). Get the correct UID and GID for the git user by running: id git.

/home/jack/forgejo/docker-compose.yml
networks:
forgejo:
external: false
services:
server:
image: codeberg.org/forgejo/forgejo:14
container_name: forgejo
environment:
- USER_UID=1000 #UID of the git user
- USER_GID=1000 #GID of the git user
restart: always
networks:
- forgejo
volumes:
- ./forgejo:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "222:22"

So both the git user and your account can access the Forgejo files under /home/jack/forgejo, set permissions like this:

Terminal window
# Add jack to group git (use group name or GID 993)
sudo usermod -aG git jack
cd /home/jack/forgejo
# Recursive: owner jack, group git (993)
sudo chown -R jack:git .
# Group can read/write/execute; others none
sudo chmod -R g+rwX,o= .
# So new files created inside get group git
sudo find . -type d -exec chmod g+s {} \;

Do not start the container yet. Set up Nginx first to proxy traffic to Forgejo.

Nginx Setup

The Forgejo container will not be exposed directly; we will put Nginx in front as a reverse proxy. Install Nginx using your distro’s method; for RHEL, see Nginx’s RHEL packages.

Install Certbot to obtain and auto-renew an SSL certificate. With Cloudflare for DNS, use the certbot-dns-cloudflare plugin. On RHEL, enable EPEL first, then install Certbot and the plugin:

Terminal window
sudo dnf install epel-release
sudo dnf install certbot python3-certbot-nginx python3-certbot-dns-cloudflare

Forgejo’s docs describe setting upNginx as a reverse proxy. Create a basic Nginx server block over HTTP first (we’ll add HTTPS next). Replace the URL with your Forgejo domain.

/etc/nginx/conf.d/forgejo.conf
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;
}
}

By default, SELinux prevents Nginx from making network connections to other services. Run this to allow it:

Terminal window
sudo setsebool -P httpd_can_network_connect 1

Start Nginx and Forgejo

Start both Nginx and Forgejo so you can complete the initial admin setup over HTTP. We’ll enable HTTPS after that.

Terminal window
sudo systemctl start nginx
sudo systemctl enable nginx
cd /home/jack/forgejo # Change path if your docker-compose.yml is elsewhere
sudo docker compose up -d

Open a browser and go to your Forgejo URL right away (use HTTP for now, e.g. http://git.example.com). The initial admin setup is publicly accessible, so complete it quickly.

Then enable HTTPS with Certbot. Ensure the site loads at your domain before continuing.

Terminal window
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
--dns-cloudflare-propagation-seconds 60 \
--installer nginx \
-d git.example.com

Update the Forgejo app.ini so ROOT_URL (and related base URL settings) uses https://. You may need to bring the container down, edit the file, then start it again for changes to apply.

Terminal window
cd /home/jack/forgejo
sudo docker compose down
sudo nano /home/jack/forgejo/forgejo/gitea/conf/app.ini

/home/jack/forgejo/forgejo/gitea/conf/app.ini
[server]
SSH_DOMAIN = git.example.com # Change to your domain name
DOMAIN = git.example.com # Change to your domain name
HTTP_PORT = 3000
ROOT_URL = https://git.example.com/ # Change to https:// if you have set up SSL/Certbot

Restart the Forgejo container so the updated app.ini is applied:

Terminal window
sudo docker compose -f /home/jack/forgejo/docker-compose.yml up -d

HTTPS should now be working!

Git clone over SSH

The VPS already runs SSH on port 22. Proxying SSH to the Forgejo container is possible but adds complexity I chose to avoid. We can instead expose Forgejo’s built-in SSH server. In our docker-compose.yml file, we have port 222 exposed for the SSH server.

Allow that port through the firewall:

Terminal window
sudo firewall-cmd --permanent --add-port=222/tcp

Reload firewalld:

Terminal window
sudo firewall-cmd --reload

Set the SSH port in app.ini so the clone URL shown in the Forgejo UI is correct. See the config cheat sheet.

Terminal window
cd /home/jack/forgejo
sudo docker compose down
sudo nano /home/jack/forgejo/forgejo/gitea/conf/app.ini

/home/jack/forgejo/forgejo/gitea/conf/app.ini
[server]
SSH_PORT = 222

Start the Forgejo container again to apply the change:

Terminal window
sudo docker compose -f /home/jack/forgejo/docker-compose.yml up -d

Overview of the setup

  • Nginx listens on ports 80 and 443 and proxies to the Forgejo container on port 3000.
  • Forgejo’s SSH server is exposed on port 222.
  • Certbot provides the SSL certificate and automatic renewal.