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