Take Control of Your Code: Replace GitHub by Self-Hosting Gitea with Traefik as a Reverse Proxy
Published by Weisser Zwerg Blog on
How to set up your own private Git repositories and reclaim your digital independence and privacy.
Rationale
This post is part of the Digital Civil Rights and Privacy series. In a world where privacy and control over your data are increasingly important, this guide will show you how to set up your own private substitute for GitHub using Gitea, paired with Traefik as a reverse proxy. By self-hosting your repositories, you ensure your code stays private, secure, and entirely under your control.
Prerequisites: Networking and Network Topology Overview
In this guide, we’ll use Traefik as a Reverse Proxy on a netcup Virtual Private Server (VPS) that we’ve already set up earlier and made accessible over the internet. To connect this VPS to your home server, where Gitea will be running, we’ll use a WireGuard Hub-and-Spoke (Star) topology. This setup allows us to securely access services like Gitea on your home server from anywhere via the public internet while keeping your data private and under your control.
Here’s a simple visual overview of the setup:
At the top of this configuration at the top of this Ʌ (upside-down V) is your VPS server reachable via the internet, which acts as the “hub” (“star-center”) and hosts the Traefik reverse proxy. Connected to this hub is your home server, where Gitea and other services will run. Both are linked securely using WireGuard in a Virtualized Mesh Network, creating a private and encrypted connection between them.
For a step-by-step guide on setting up a WireGuard virtualized mesh network, check out the WireGuard section in the ODROID-M1: Dockerized Home Assistant page.
Getting my Hub and Spoke setup to work took some time, mainly because I overlooked the following lines in the wg0.conf
at my star center at my netcup VPS:
# Allow routing between clients
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT
As detailed in Wireguard Netzwerk mit Routing einrichten.
In the Traefik docker-compose.yml from my previous post, I still use
traefik:v2.8.0
. If you want to utilize a more recent version, check out Christian Lempa’s boilerplates and specifically his docker-compose/traefik file. Currently, this usestraefik:v3.3.3
.
For this guide, I’ll assume you’re working with a VPS, have Traefik set up as a reverse proxy on this VPS, and that the VPS is accessible via the internet at your-domain.tld
.
Getting Started
The process for setting up a docker compose
instance is well-documented in the Installation with Docker Gitea documentation page, which is an excellent resource to have handy.
A significant portion of the setup below is inspired by Vladimir Mikhalev’ gitea-traefik-letsencrypt-docker-compose repository on GitHub.
I simplified it by removing the integrated backup mechanisms, as I prefer using tools like Borg or Duplicati for backups.
If you’re curious about the full setup, Vladimir’s complete guide is available on his website.
For your convenience, I’ll walk you through the steps here, adding a few extra comments to guide you smoothly along the way.
On your Home Server
Let’s start by setting up the necessary file system structure on your home server to run Gitea.
Make sure to perform these steps as the root
user for proper permissions.
mkdir -p /opt/gitea/config/{postgres/data,gitea/data} && chmod a+w /opt/gitea/config/gitea/data
Next, copy the docker-compose.yaml
and .env
files into the /opt/gitea/
directory.
These files are essential for configuring your Gitea setup:
- The
docker-compose.yaml
file defines the services needed to run Gitea. - The
.env
file stores environment variables for your configuration.
docker-compose.yaml
########################### EXTENSION FIELDS
# Helps eliminate repetition of sections
# Keys common to some of the core services that we always to automatically restart on failure
x-common-keys-core: &common-keys-core
restart: unless-stopped
# docker compose up -d
# docker compose config
name: gitea
services:
postgres:
image: ${GITEA_POSTGRES_IMAGE_TAG}
<<: *common-keys-core
volumes:
- ./config/postgres/data:/var/lib/postgresql/data
environment:
POSTGRES_DB: ${GITEA_DB_NAME}
POSTGRES_USER: ${GITEA_DB_USER}
POSTGRES_PASSWORD: ${GITEA_DB_PASSWORD}
healthcheck:
test: [ "CMD", "pg_isready", "-q", "-d", "${GITEA_DB_NAME}", "-U", "${GITEA_DB_USER}" ]
interval: 10s
timeout: 5s
retries: 3
start_period: 60s
restart: unless-stopped
gitea:
image: ${GITEA_IMAGE_TAG}
<<: *common-keys-core
ports:
- 3000:3000
- 2222:22
volumes:
- ./config/gitea/data:/${DATA_PATH}
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
environment:
GITEA_DATABASE_HOST: postgres
GITEA_DATABASE_NAME: ${GITEA_DB_NAME}
GITEA_DATABASE_USERNAME: ${GITEA_DB_USER}
GITEA_DATABASE_PASSWORD: ${GITEA_DB_PASSWORD}
GITEA_ADMIN_USER: ${GITEA_ADMIN_USERNAME}
GITEA_ADMIN_PASSWORD: ${GITEA_ADMIN_PASSWORD}
GITEA_ADMIN_EMAIL: ${GITEA_ADMIN_EMAIL}
GITEA_RUN_MODE: prod
GITEA_DOMAIN: ${GITEA_HOSTNAME}
GITEA_SSH_DOMAIN: ${GITEA_HOSTNAME}
GITEA_ROOT_URL: ${GITEA_URL}
GITEA_HTTP_PORT: 3000
GITEA_SSH_PORT: ${GITEA_SHELL_SSH_PORT}
GITEA_SSH_LISTEN_PORT: 22
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/"]
interval: 10s
timeout: 5s
retries: 3
start_period: 90s
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
.env
:
GITEA_POSTGRES_IMAGE_TAG=postgres:17
GITEA_IMAGE_TAG=bitnami/gitea:1.23.1
GITEA_DB_NAME=giteadb
GITEA_DB_USER=giteadbuser
GITEA_DB_PASSWORD=...somesecretpassword.... # `openssl rand -base64 15` or `pwgen -cnys 15 1` or use keepassx "Password Generator" functionality
GITEA_ADMIN_USERNAME=giteaadmin
GITEA_ADMIN_PASSWORD=...somesecretpassword.... # `openssl rand -base64 15` or `pwgen -cnys 15 1` or use keepassx "Password Generator" functionality
GITEA_ADMIN_EMAIL=giteaadmin@gitea.your-domain.tld
GITEA_URL=https://gitea.your-domain.tld
GITEA_HOSTNAME=gitea.your-domain.tld
GITEA_SHELL_SSH_PORT=2222
DATA_PATH=/bitnami/gitea
Now, verify that everything is set up correctly. Run:
docker compose config
The output should confirm that all variables have been successfully replaced with the values from your .env
file.
Finally, you’re ready to start your Docker stack. Run:
docker compose up -d
This command will start all the services in the background, and your Gitea instance will be up and running.
On your Virtual Private Server (VPS)
Let’s continue with extending our set-up of our VPS server. You’ll need to make a few adjustments to your Traefik Reverse Proxy configurations.
Start by updating the configuration in /opt/traefik/docker-compose.yml
. Add or adapt the following settings as needed:
ports:
- "2222:2222"
volumes:
- ./traefik-config/traefik.yml:/etc/traefik/traefik.yml
- ./traefik-config/dynamic.yml:/etc/traefik/dynamic/dynamic.yml
- ./traefik-config/dynamic-tcp.yml:/etc/traefik/dynamic/dynamic-tcp.yml
Next, modify the Traefik configuration file at /opt/traefik/traefik-config/traefik.yml
. Make sure to include or update the following:
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: web-secure
scheme: https
web-secure:
address: ":443"
http2:
maxConcurrentStreams: 250
http3:
advertisedPort: 443
git-ssh:
address: ":2222"
providers:
file:
directory: /etc/traefik/dynamic
watch: true
Then, head over to /opt/traefik/traefik-config/dynamic.yml
and add or adjust the following lines:
http:
middlewares:
compresstraefik:
compress: true
routers:
gitea-router:
rule: "Host(`gitea.your-domain.tld`)"
service: gitea-service
middlewares:
- compresstraefik
tls:
certResolver: default
services:
gitea-service:
loadBalancer:
passHostHeader: true
servers:
- url: "http://10.0.1.5:3000"
Finally, update the TCP configuration in /opt/traefik/traefik-config/dynamic-tcp.yml
. Add or adapt the following:
tcp:
routers:
gitea-ssh-router:
rule: "HostSNI(`*`)"
service: gitea-ssh-service
entrypoints:
- git-ssh
services:
gitea-ssh-service:
loadBalancer:
servers:
- address: "10.0.1.5:2222"
Gitea Configuration
The Gitea configuration file is located at config/gitea/data/custom/conf/app.ini
.
Below are some key settings you may want to adjust to make your self-hosted repository more private and secure.
Here’s a quick overview in diff
format of the changes I’d suggest you might want to consider:
7a8,9
> DEFAULT_PRIVATE=private
> FORCE_PRIVATE=true
78c80
< DISABLE_REGISTRATION=false
---
> DISABLE_REGISTRATION=true
81,82c83,84
< REQUIRE_SIGNIN_VIEW=false
< DEFAULT_KEEP_EMAIL_PRIVATE=false
---
> REQUIRE_SIGNIN_VIEW=true
> DEFAULT_KEEP_EMAIL_PRIVATE=true
DEFAULT_PRIVATE=private
andFORCE_PRIVATE=true
: These settings ensure that all new repositories are private by default and prevent users from creating public repositories, which is ideal for keeping your code secure and private.DISABLE_REGISTRATION=true
: This disables user registration, meaning only you (or users you explicitly create) can access your Gitea instance. This is a good security measure if you’re the only one who needs access.REQUIRE_SIGNIN_VIEW=true
andDEFAULT_KEEP_EMAIL_PRIVATE=true
: These settings require users to sign in before viewing repositories and keep their email addresses private by default, adding an extra layer of security and privacy for your users.
I’d also advise to turn on Multi-factor Authentication (MFA) for your user accounts on Gitea in the > Settings > Security
section.
Enabling MFA on a user does affect how the Git HTTP protocol can be used with the Git CLI. This interface does not support MFA, and trying to use a password normally will no longer be possible whilst MFA is enabled.
Your usual way of dealing with repositories where MFA is enabled is to use SSH Keys
in the > Settings > SSH / GPG Keys
section.
If SSH is not an option for Git operations, an access token can be generated within the “Applications” tab of the user settings page. This access token can be used as if it were a password in order to allow the Git CLI to function over HTTP.
Warning: By its very nature, an access token sidesteps the security benefits of MFA. It must be kept secure and should only be used as a last resort.
For a full list of configuration options, check out the Gitea Configuration Cheat Sheet.
Conclusions
That’s it! You’ve successfully set up your own self-hosted Gitea server with Traefik as a reverse proxy. By taking these steps, you’ve not only created a private and secure space for your code but also taken a significant step toward reclaiming control over your digital privacy and independence.
Now that you’re up and running, think about all the possibilities - hosting your personal projects, collaborating with trusted contributors, or even creating a private repository for sensitive work. You’re in full control of your data, free from third-party dependencies.
Appendix
Git Remote Gcrypt
While git-remote-gcrypt has nothing to do with Gitea, it can be a useful addition for secure Git hosting.
git-remote-gcrypt
lets you maintain PGP-encrypted Git remotes.
This means only the local machine, where you work with the repository, can see the unencrypted files. All data on the remote server stays encrypted.
Internally, git-remote-gcrypt
uses GnuPG to encrypt all objects in the repository, so the remote server never sees the repository in plain text.
For efficiency, git-remote-gcrypt
is best used with rsync.
Although other options exist, we won’t cover them here.
rsync
connects to the remote server over SSH, so you do need SSH access on that server.
As an example, I keep my private “paperless office” in a Git repository and use git-remote-gcrypt
to synchronize it to my home server (the same server running Gitea).
The first step is to install git-remote-gcrypt
:
apt install git-remote-gcrypt
I will use a hardware GPG key backed by a Trezor as described in PGP via Roman Zayde’s Trezor-agent.
That post also explains how to set up the trezor-venv
alias we use to configure the system for a Trezor-backed GPG key.
I place these git-remote-gcrypt
remotes under /opt/offsite_backup_storage/git-remote-gcrypt
so they’re included in my daily offsite backups, as described in Home Server Blueprint: Rock-Solid Home Server with Unattended Reboots, Secure Disk Encryption, and Cost-Effective Offsite Backups.
Let’s assume you are already inside the paperless-office Git repository that you want to push via git-remote-gcrypt
to your home server. First, configure the new remote:
trezor-venv
cd paperless-office
git remote add cryptremote gcrypt::rsync://homeserver-as-root/opt/offsite_backup_storage/git-remote-gcrypt/paperless-office
Next, make a few adaptations to some git config values, as explained in the README.rst of git-remote-gcrypt
:
KEY_ID="$(gpgconf --list-options gpg | awk -F: '/^default-key:/ {gsub(/"/,"",$NF); print $NF}')" && git config gcrypt.participants $KEY_ID && git config remote.cryptremote.gcrypt-signingkey $KEY_ID && git config gcrypt.publish-participants true && git config gcrypt.require-explicit-force-push true
cat .git/config
Finally, you can push to the remote:
git push --progress -v --force cryptremote --all --tags
And fetch from it:
git fetch --progress -v cryptremote
A git clone
would look like this:
git clone gcrypt::rsync://homeserver-as-root/opt/offsite_backup_storage/git-remote-gcrypt/paperless-office
Syncing from a Git Bundle
Sometimes I keep a Git repository entirely on a local computer but occasionally make a git bundle
backup:
cd paperless-office
git bundle create YYYY-mm-dd-paperless-office.bundle --all
If you decide later that you also want to replicate that same repo to a git-remote-gcrypt
location, you can do the following:
First, clone the bundle locally:
git clone YYYY-mm-dd-paperless-office.bundle YYYY-mm-dd-paperless-office
Then proceed as we did above:
cd YYYY-mm-dd-paperless-office
git remote add cryptremote gcrypt::rsync://homeserver-as-root/opt/offsite_backup_storage/git-remote-gcrypt/paperless-office
KEY_ID="$(gpgconf --list-options gpg | awk -F: '/^default-key:/ {gsub(/"/,"",$NF); print $NF}')" && git config gcrypt.participants $KEY_ID && git config remote.cryptremote.gcrypt-signingkey $KEY_ID && git config gcrypt.publish-participants true && git config gcrypt.require-explicit-force-push true
cat .git/config
git push --progress -v --force cryptremote --all --tags
Then you clone from your remote git-remote-gcrypt
to its “persistent” name without the YYYY-mm-dd-
and .bundle
:
cd ..
git clone gcrypt::rsync://homeserver-as-root/opt/offsite_backup_storage/git-remote-gcrypt/paperless-office
After this clone, your remote will be called origin
rather than cryptremote
, but that’s fine.
git remote -v
From now on, you can handle every incremental .bundle
file as follows:
cd paperless-office
git fetch ../YYYY-mm-dd-paperless-office.bundle refs/heads/*:refs/heads/*
git checkout master
# FETCH_HEAD is a temporary reference Git creates whenever you fetch from a remote (or bundle).
# Git stores the tip commit(s) you fetched in a file named .git/FETCH_HEAD
git tag YYYY-mm-dd-paperless-office.bundle FETCH_HEAD
git merge FETCH_HEAD
git log --graph --oneline --decorate # Review the merge result by running
git push --progress -v --force origin --all --tags
# Or: git push --progress -v --force origin --mirror
Using git-remote-gcrypt
with rsync and SSH provides an efficient and secure way to store and back up your Git repositories.
It helps ensure your data remains encrypted at all times when it’s on a remote server, while still allowing you to work with unencrypted data locally.
This method is especially useful for confidential or personal projects where you need full control over your repository’s security.