Self-Hosted GitHub: Build Your Own Git Server with Gitea on Debian 13 (Trixie)
There is a persistent myth in the developer world that running your own Git server with a full web interface, pull requests, issue trackers, and multi-user SSH support is some massive enterprise undertaking. People imagine complex containers, heavy database clusters, and hours of fighting Linux permissions.
In reality, it takes about 30 minutes – and you can easily finish the whole thing with one hand while eating a sandwich. Maybe not on the first try (because there is some reading to do), but the moral of the story is that it isn’t rocket science.
The secret weapon is Gitea. Written in Go, Gitea is distributed as a single executable, runs happily on SQLite, and provides a surprisingly complete development platform without requiring a database server or an army of dependencies.
Even better, it handles multiple users seamlessly over SSH keys without giving those users shell access to your Linux server.
In this guide, we’ll turn a fresh Debian 13 (Trixie) installation into a self-hosted GitHub alternative using:
- Gitea
- Git
- SQLite3
- OpenSSH
- Nginx
- HTTPS
We’ll cover two installation methods: installing Gitea through a third-party APT repository or downloading the official binary directly.
An important design decision: Regardless of which installation method you choose, we’ll use one dedicated Linux account named gitea to run the application and handle Git-over-SSH connections. This keeps the rest of the configuration consistent.
Index
- Understanding Linux users and SSH authentication
- Installing the prerequisites
- Installing Gitea (two methods)
- Understanding Gitea’s directories and permissions
- Configuring systemd
- Configuring Nginx and HTTPS
- Completing the web setup wizard
- Generating SSH keys and pushing code
- Adding users and creating repositories
- Backups and maintenance
- Troubleshooting
1. Wait – How Can Users Push Over SSH Without Having Linux Accounts?
Before we touch the terminal, let’s clear up the single biggest point of confusion for anyone setting up a Git server for the first time.
When you add ten friends or coworkers to your Gitea server, you do not create ten Linux users on Debian.
If you did that, they could potentially log into your server’s terminal, explore its filesystem, and generally make a mess.
Instead, we create one dedicated Linux system account named gitea. Every person who uses your server connects through that same account:
gitea@git.example.com:alice/my-project.git
Wait a minute. If everyone connects as gitea, how does the server know who is Alice and who is Bob? And what stops Bob from obtaining a command prompt on the server?
The magic of authorized_keys command interception
When Alice logs into the Gitea web interface and adds her public SSH key to her account settings, Gitea adds that key to the Linux account’s SSH authorized_keys file.
Importantly, Gitea doesn’t simply paste the raw public key. It associates the key with a special OpenSSH rule that looks approximately like this:
command="/usr/local/bin/gitea serv key-1 --config=/etc/gitea/app.ini",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... alice@laptop
The actual executable path and key identifier will depend on your installation.
Here’s what happens when Alice runs git push:
- Identification: OpenSSH checks the incoming key against
authorized_keysand identifies it askey-1, which belongs to Alice. - Interception: OpenSSH sees the
command="..."restriction. Instead of executing whatever command Alice requested, it launches Gitea’s internalservcommand. - Lockdown: Options such as
no-ptyandno-port-forwardingprevent the SSH connection from being used as an ordinary interactive terminal or network tunnel. - Authorization: Gitea checks its database, identifies Alice, verifies her permissions on the requested repository, and processes the Git operation.
If Alice tries to get clever and runs:
ssh -T gitea@git.example.com
She won’t receive a Linux shell. Instead, Gitea responds with something similar to:
Hi there, alice! You've successfully authenticated with the key named alice@laptop, but Gitea does not provide shell access.
And kicks her right out politely shows her the door.
Fast, native SSH speed, with no ordinary shell access.
A note about the Linux account: The name gitea is simply our chosen operating-system username. It is not a Gitea application username, nor is it the Git executable. Git is a separate program installed on the server and on developers’ computers.
Gitea uses Git to perform repository operations, while its own database manages application users, permissions, issues, and repositories.
2. Install the Basics on Debian 13 (Trixie)
Log into your Debian 13 virtual machine as root or a user with sudo privileges.
Install the core packages:
sudo apt update && sudo apt upgrade -y
sudo apt install -y \
git \
sqlite3 \
nginx \
certbot \
python3-certbot-nginx \
wget \
curl \
ca-certificates
Verify that Git is installed properly:
git --version
Gitea requires Git to be installed on the server because it delegates actual version-control operations to the Git executable.
3. Install Gitea
There are two convenient ways to install Gitea:
- Method 1: Install it through a third-party APT repository, allowing Debian’s package manager to handle updates.
- Method 2: Download the official precompiled binary and manage updates yourself.
Choose ONE method. Do not execute both.
Gitea is not currently packaged in Debian’s official repositories. The Debian Wiki provides information about the situation and points readers toward upstream installation methods:
The official open-source project is:
https://github.com/go-gitea/gitea
Gitea is released under the MIT license, making it a very permissively licensed piece of software.
Method 1: Install using APT
We’ll use the community-maintained Debian/Ubuntu packaging repository maintained by morph027.
This is a third-party repository, not an official Debian repository. As with any third-party package source, you’re trusting its maintainer to distribute the software and updates.
First, add the repository’s signing key:
sudo curl -fsSL \
-o /etc/apt/trusted.gpg.d/morph027-gitea.asc \
https://packaging.gitlab.io/gitea/gpg.key
Add the repository:
echo "deb [arch=$(dpkg --print-architecture)] https://packaging.gitlab.io/gitea gitea main" \
| sudo tee /etc/apt/sources.list.d/morph027-gitea.list
Update the package index and install Gitea together with the repository’s keyring package:
sudo apt update
sudo apt install -y gitea morph027-keyring
Verify the installation:
which gitea
gitea --version
The executable should normally be located at:
/usr/bin/gitea
The package also creates its own Linux service account and installs a systemd service.
Check that the account exists:
getent passwd gitea
And inspect the packaged service:
systemctl cat gitea
That’s it for the installation. The package provides the service and its initial directory structure, so we won’t replace its systemd unit with our own.
You can proceed to Step 4.
Repository instructions and updates:
https://gitlab.com/packaging/gitea
Method 2: Download the official binary
Because Gitea is written in Go, we don’t need to install dozens of runtime libraries. We simply download the precompiled executable and put it in /usr/local/bin.
Visit the official downloads page:
Choose the latest stable release and the appropriate Linux architecture.
For example, for a 64-bit Intel/AMD machine:
GITEA_VERSION="REPLACE_WITH_LATEST_VERSION"
Download the binary:
sudo wget -O /usr/local/bin/gitea \
"https://dl.gitea.com/gitea/${GITEA_VERSION}/gitea-${GITEA_VERSION}-linux-amd64"
Make it executable:
sudo chmod 755 /usr/local/bin/gitea
Test it:
/usr/local/bin/gitea --version
For security, verify the downloaded binary against the checksum or signature published for that release before using it.
Create the Gitea Linux account
Unlike the APT method, the binary installation does not automatically create the Linux account for us.
We’ll create the same gitea account used by the APT installation.
sudo adduser \
--system \
--shell /bin/bash \
--gecos 'Gitea Service' \
--group \
--disabled-password \
--home /var/lib/gitea \
gitea
A few notes about these options:
--system: Creates a dedicated system account.--shell /bin/bash: Provides a valid shell for OpenSSH to execute Gitea’s forced commands. It does not mean users receive unrestricted shell access.--group: Creates a matchinggiteagroup.--disabled-password: Prevents password-based login to this account.--home /var/lib/gitea: Establishes the service account’s home directory.
We deliberately use /var/lib/gitea as the home directory so that the binary installation has a consistent working and data location.
Continue to Step 4.
4. Set Up Gitea’s Folders and Permissions
Gitea needs somewhere to store its SQLite database, Git repositories, logs, attachments, and configuration.
We’ll use a conventional Linux directory layout:
| Directory | Purpose |
|---|---|
/var/lib/gitea | Application working directory and service account home (binary installation) |
/var/lib/gitea/data | Database, repositories and application data |
/var/lib/gitea/custom | Custom templates, configuration and other custom resources |
/var/lib/gitea/log | Application logs |
/etc/gitea | Gitea configuration, including app.ini |
/usr/local/bin/gitea | Binary installation executable |
The APT installation creates its own directory structure. Inspect it rather than blindly overwriting it.
For the binary installation, create the directories:
sudo mkdir -p /var/lib/gitea/{custom,data,log}
sudo chown -R gitea:gitea /var/lib/gitea
sudo chmod -R 750 /var/lib/gitea
Create the configuration directory:
sudo mkdir -p /etc/gitea
sudo chown root:gitea /etc/gitea
sudo chmod 770 /etc/gitea
Why 770?
During the initial web setup wizard, Gitea needs permission to create its app.ini configuration file.
We temporarily allow the service account to write to the configuration directory. Once the wizard is finished, we’ll lock it down.
For the APT installation, inspect the existing permissions and ownership before changing anything:
sudo ls -ld /var/lib/gitea /etc/gitea
The packaged configuration and data directories should already be prepared for the gitea account.
A note about SSH directories
Gitea normally stores its OpenSSH authorized_keys file in the .ssh directory belonging to its runtime account.
For the binary installation, this will normally be:
/var/lib/gitea/.ssh/authorized_keys
For the APT installation, use the home directory reported by:
getent passwd gitea
This is important because OpenSSH and Gitea must agree on where that file lives.
We will leave Gitea’s default SSH_ROOT_PATH behavior intact rather than hardcoding a different SSH directory.
5. Configure the Systemd Service
We want Gitea to start automatically whenever Debian boots and restart itself if it crashes.
The two installation methods already share the same service identity, but their service installation differs.
If you installed through APT
The package already provides a systemd service.
Enable and start it:
sudo systemctl enable --now gitea
Inspect it:
sudo systemctl status gitea
Don’t replace the package-provided service file unless you have a specific reason to do so.
If you installed the binary
We need to create the systemd service ourselves.
Open a new service file:
sudo nano /etc/systemd/system/gitea.service
Paste the following:
[Unit]
Description=Gitea (Git with a cup of tea)
After=network.target
[Service]
RestartSec=2s
Type=simple
User=gitea
Group=gitea
WorkingDirectory=/var/lib/gitea
ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini
Restart=always
Environment=USER=gitea HOME=/var/lib/gitea GITEA_WORK_DIR=/var/lib/gitea
[Install]
WantedBy=multi-user.target
Save and exit Nano (Ctrl+O, Enter, then Ctrl+X).
Reload systemd and start Gitea:
sudo systemctl daemon-reload
sudo systemctl enable --now gitea
Check that the service is running:
sudo systemctl status gitea
You should see:
Active: active (running)
Gitea should now be listening on port 3000.
Check the runtime identity
Regardless of installation method, Gitea should run as the gitea operating-system account.
The application configuration should also identify that account as its runtime user:
[DEFAULT]
RUN_USER = gitea
The packaged installation should already provide the appropriate setting. For the binary installation, Gitea normally derives it from the environment, but explicitly verifying it is worthwhile.
You can inspect the configuration after it has been created in the next steps.
6. Put Nginx in Front for HTTPS (Port 443)
Connecting to http://your-ip:3000 works in a home test lab, but typing :3000 gets old fast, and sending passwords over unencrypted HTTP is a bad habit.
Let’s configure Nginx as a reverse proxy.
Visitors will connect over standard HTTPS on port 443, and Nginx will quietly forward the traffic to Gitea on 127.0.0.1:3000.
First, decide what hostname you’ll use.
For example:
git.example.com
Or, for a private LAN:
gitea.home.arpa
Replace the examples throughout this section with your actual hostname.
Create the Nginx site configuration
Create the configuration file:
sudo nano /etc/nginx/sites-available/gitea
Paste:
server {
listen 80;
listen [::]:80;
server_name git.example.com;
client_max_body_size 512M;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
}
}
The client_max_body_size setting allows large HTTP uploads. It is not a limit on Git-over-SSH pushes, which bypass Nginx.
The WebSocket headers also allow Gitea’s real-time browser features to work correctly.
Enable the site:
sudo ln -s \
/etc/nginx/sites-available/gitea \
/etc/nginx/sites-enabled/gitea
Test the configuration:
sudo nginx -t
Reload Nginx:
sudo systemctl reload nginx
Adding HTTPS: Choose the appropriate option
There are two common scenarios.
Option A: Your server has a public domain name
If git.example.com resolves to your server’s public IP address and the server is reachable from the Internet, Certbot can obtain a free Let’s Encrypt certificate.
Make sure:
- DNS points to your server.
- TCP ports 80 and 443 are reachable.
- If you’re behind a router, the required ports are forwarded to your VM.
Run:
sudo certbot --nginx -d git.example.com
Follow the instructions.
Certbot will obtain the certificate and configure Nginx for HTTPS.
You should now be able to access:
https://git.example.com
Option B: Your server is strictly on a private LAN
Suppose your VM is at:
192.168.1.50
And you want to access it using:
gitea.home.arpa
You don’t need to expose the server to the Internet or purchase a domain.
You do, however, need to configure local DNS (or a hosts-file entry) so that gitea.home.arpa resolves to 192.168.1.50.
For HTTPS, I recommend using a locally trusted certificate authority rather than relying on an untrusted self-signed certificate.
One convenient tool is mkcert:
https://github.com/FiloSottile/mkcert
On a trusted administration workstation, install mkcert and initialize its local CA:
mkcert -install
Generate a certificate containing the hostname and IP address:
mkcert \
-cert-file gitea.pem \
-key-file gitea-key.pem \
gitea.home.arpa \
192.168.1.50
Copy the generated certificate and private key to your Debian server.
For example:
/etc/nginx/ssl/gitea.pem
/etc/nginx/ssl/gitea-key.pem
Protect the private key:
sudo chown root:root /etc/nginx/ssl/gitea-key.pem
sudo chmod 600 /etc/nginx/ssl/gitea-key.pem
You must also distribute the mkcert root CA certificate to the computers that will access Gitea and install it into their trusted certificate stores.
You can locate the CA directory on the workstation with:
mkcert -CAROOT
Copy rootCA.pem to your clients, not rootCA-key.pem. Never distribute the CA private key.
Now replace the Nginx configuration with:
server {
listen 80;
listen [::]:80;
server_name gitea.home.arpa;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name gitea.home.arpa;
ssl_certificate /etc/nginx/ssl/gitea.pem;
ssl_certificate_key /etc/nginx/ssl/gitea-key.pem;
client_max_body_size 512M;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
}
}
Test and reload:
sudo nginx -t
sudo systemctl reload nginx
You can now access Gitea over HTTPS on your LAN without browser certificate warnings, provided the client trusts your local CA.
A final note: A self-signed certificate is also possible, but encryption and certificate trust are two different things. A self-signed certificate encrypts traffic, but browsers and Git clients will still complain until you explicitly trust it.
7. Complete the Web Setup Wizard
Open your browser and visit:
https://git.example.com
Or your private LAN hostname:
https://gitea.home.arpa
You’ll be greeted by Gitea’s initial configuration page.
Because we’re using SQLite and standard OpenSSH, most defaults are already suitable.
1. Database settings
- Database Type: SQLite3.
- Database Path:
/var/lib/gitea/data/gitea.db.
This gives us a simple database file instead of requiring a separate database server.
2. General settings
- Site Title: Choose whatever you like.
- Server Domain: Your actual hostname.
- SSH Server Domain: Your actual hostname.
- SSH Server Port:
22. - Gitea HTTP Listen Port:
3000. - Gitea Base URL:
https://git.example.com/.
For a LAN installation, use your LAN hostname instead.
3. Optional settings
Expand the Server and Third-Party Service Settings section.
I recommend disabling self-registration if this is a private server where you want to control who can create accounts.
You can always enable it later.
4. Administrator account
Expand Administrator Account Settings and create your administrator account.
Use a strong password and enable multi-factor authentication after installation.
Click Install Gitea.
Within a few seconds, your dashboard should load.
Lock down the configuration file
Now that Gitea has created /etc/gitea/app.ini, let’s make a few important adjustments.
Open:
sudo nano /etc/gitea/app.ini
Find the [server] section and make sure it contains the appropriate settings:
[server]
DOMAIN = git.example.com
ROOT_URL = https://git.example.com/
HTTP_ADDR = 127.0.0.1
HTTP_PORT = 3000
SSH_DOMAIN = git.example.com
SSH_PORT = 22
Replace the example domain with your actual hostname.
The HTTP_ADDR setting ensures Gitea only listens on localhost for HTTP requests. Visitors must go through Nginx rather than accessing port 3000 directly.
Also verify that the default runtime user is correct:
[DEFAULT]
RUN_USER = gitea
For the SSH integration, the displayed SSH username should also be gitea. Gitea normally derives this from its runtime account, but verify the generated clone URL rather than assuming.
Important: Do not change the APT package’s service configuration or SSH paths blindly. The package may provide additional defaults. We want to preserve its working directory and authorized-keys arrangement.
Lock down the configuration directory
The configuration file contains sensitive information, including database and application secrets.
For the binary installation, and for an APT installation where the ownership matches this layout, run:
sudo chown root:gitea /etc/gitea
sudo chmod 750 /etc/gitea
sudo chown root:gitea /etc/gitea/app.ini
sudo chmod 640 /etc/gitea/app.ini
The gitea service account can read the configuration through its group membership, but cannot modify it.
Restart Gitea:
sudo systemctl restart gitea
8. Generate Your SSH Key and Push Code!
Why use SSH keys instead of typing a username and password every time you run git push?
- Convenience: You don’t need to enter your credentials for every operation.
- Security: Public-key authentication avoids transmitting a reusable password during Git operations.
- Access control: You can register multiple keys and revoke individual keys without changing your account password.
1. Generate a keypair on your workstation
Important: Run these commands on your personal computer where you write code, NOT inside the Debian Git server VM.
Open your terminal and run:
ssh-keygen -t ed25519 -C "your-email@example.com"
When asked where to save the key, press Enter to accept the default:
~/.ssh/id_ed25519
When asked for a passphrase, I recommend setting one, particularly if you use a laptop or shared workstation. An SSH agent can remember it during your working session.
This creates two files:
id_ed25519 Private key
id_ed25519.pub Public key
Never share your private key.
Print your public key:
cat ~/.ssh/id_ed25519.pub
Copy the entire line.
2. Add the key to Gitea
In the Gitea web interface:
- Click your profile avatar in the top-right corner.
- Select Settings.
- Open the SSH / GPG Keys section.
- Click Add Key.
- Paste your public key and give it a recognizable name.
That’s it.
3. Peek under the hood (optional fun!)
Want to see the command interception magic we talked about earlier?
On your Debian server, inspect the authorized_keys file.
For the binary installation, assuming the home directory we created:
sudo cat /var/lib/gitea/.ssh/authorized_keys
For the APT installation, first check the service account’s home directory:
getent passwd gitea
Then inspect its .ssh/authorized_keys file.
You should see your newly registered key wrapped in a Gitea command="..." restriction.
4. Test your SSH connection
From your workstation:
ssh -T gitea@git.example.com
The first time you connect, SSH will ask whether you trust the server’s host key.
Verify its fingerprint through a trusted channel before accepting it.
Once authenticated, you should receive a response similar to:
Hi there, your-username! You've successfully authenticated with the key named my-laptop, but Gitea does not provide shell access.
Congratulations! Your SSH authentication is working.
5. Configure Git on your workstation
If this is your first time using Git on this machine, configure your identity:
git config --global user.name "Your Name"
git config --global user.email "your-email@example.com"
These settings identify you in your Git commits. They are separate from your Gitea login credentials.
6. Clone a repository
Create a repository in Gitea, then use its SSH clone URL.
It should look something like:
git clone gitea@git.example.com:alice/my-project.git
You can now work normally:
cd my-project
# Make changes to your files
git add .
git commit -m "My first commit"
git push
Your code is now being pushed to your own server!
9. Adding Users and Creating Repositories
Now that your server is running, adding friends, teammates, or clients is straightforward.
Log into Gitea using your administrator account.
To create a user:
- Click your avatar.
- Go to Site Administration.
- Open the User Accounts section.
- Select Create User Account.
- Provide the username, email and initial password.
When they log into the web interface:
- They can create their own repositories.
- They can add their own SSH public keys.
- They can clone, pull and push code.
- They can participate in collaborative projects according to the permissions you assign.
For example, Alice can clone her repository with:
git clone gitea@git.example.com:alice/my-project.git
Bob can do exactly the same with his own account and repositories.
Both use the same Linux account, gitea, but Gitea knows which application user is connecting based on the registered SSH key.
Neither receives ordinary access to your Debian operating system.
Organizations and teams
If you’re hosting repositories for a group of developers, don’t overlook Gitea Organizations.
Organizations allow you to group repositories and manage access through teams, rather than individually assigning permissions to every repository.
This is particularly useful when several developers need access to shared projects.
10. Backups and Maintenance
A Git server is only as reliable as its backups.
Although Git repositories contain their own history, Gitea stores much more than Git data.
You should protect:
- The SQLite database.
- Git repositories.
- Gitea configuration.
- Attachments and uploaded files.
- Git LFS objects, if enabled.
- Custom application data.
A simple backup procedure
For a small installation, a straightforward approach is to stop Gitea briefly and archive its data and configuration directories.
Create a backup directory on a separate storage device:
sudo mkdir -p /mnt/backups/gitea
Stop the service:
sudo systemctl stop gitea
Create the archive:
sudo tar -C / -czf \
"/mnt/backups/gitea/gitea-$(date +%F).tar.gz" \
var/lib/gitea \
etc/gitea
Restart Gitea:
sudo systemctl start gitea
This captures the database, repositories, attachments, and configuration in a consistent stopped-service state.
Do not rely on a backup stored on the same disk as your server. Copy your backups to another machine or storage location.
For larger installations, investigate Gitea’s built-in dump and restore functionality:
https://docs.gitea.com/administration/backup-and-restore
Updating Gitea
The update procedure depends on your installation method.
APT installation:
sudo apt update
sudo apt upgrade gitea
Binary installation:
- Back up your installation.
- Check the release notes.
- Download the new binary.
- Verify its checksum or signature.
- Stop Gitea.
- Replace
/usr/local/bin/gitea. - Start Gitea again.
For example:
sudo systemctl stop gitea
# Replace the binary with the verified new release.
sudo systemctl start gitea
Always check the release notes before upgrading, especially when crossing major versions.
11. Troubleshooting
A few commands will save you a lot of time when something goes wrong.
Check whether Gitea is running
sudo systemctl status gitea
Follow the application logs
sudo journalctl -u gitea -f
Check Nginx
sudo nginx -t
sudo systemctl status nginx
Check whether Gitea is listening on port 3000
sudo ss -ltnp | grep 3000
After completing the configuration, you should see it listening on 127.0.0.1:3000.
Check the Linux account
getent passwd gitea
id gitea
Check SSH authentication
ssh -vT gitea@git.example.com
The verbose output can help identify whether the problem is related to the SSH key, server fingerprint, SSH account or authentication process.
Common problems
| Symptom | What to investigate |
|---|---|
| Gitea won’t start | RUN_USER, service ownership, configuration and logs |
| HTTP 502 from Nginx | Gitea service status and port 3000 |
| SSH permission denied | SSH key registration, account home and authorized_keys permissions |
| SSH authentication works but push fails | Repository permissions and Gitea account ownership |
| Browser certificate warning | Certificate hostname, SAN and client trust store |
| HTTP 413 error | Nginx client_max_body_size |
| Gitea redirects to the wrong URL | ROOT_URL and reverse-proxy headers |
Final Thoughts
And just like that, you have your own self-hosted GitHub alternative running on Debian 13.
No Docker containers, no Kubernetes clusters, no separate database server, and no need to pay for a hosted Git platform.
You have a proper multi-user development environment, complete with SSH authentication, repositories, issue tracking, collaboration tools and a web interface.
And the best part? You control the server, the data, the backups and the users.
You probably still have half your sandwich left.
One thought on “Self-Hosted GitHub”