<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Development – Serverküche</title><link>https://serverkueche.de/en/tags/development/</link><description>Development – Neueste Beiträge von Serverküche</description><generator>Hugo</generator><language>en-US</language><managingEditor>feedback@serverkueche.de (Serverküche)</managingEditor><webMaster>feedback@serverkueche.de (Serverküche)</webMaster><copyright>2026 Serverküche</copyright><lastBuildDate>Mon, 03 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/en/tags/development/index.xml" rel="self" type="application/rss+xml"/><item><title>Forgejo: your own Git server behind Traefik</title><link>https://serverkueche.de/en/tutorials/forgejo-git-server/</link><pubDate>Mon, 03 Aug 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/forgejo-git-server/</guid><description>Set up Forgejo with Docker &amp; Traefik: your own Git server with HTTPS, repos via web UI, cloning over HTTPS and SSH – the self-hosted GitHub alternative.</description><content:encoded><![CDATA[<p>GitHub is convenient – but your code then lives on someone else&rsquo;s servers. With <strong>Forgejo</strong> you host your repositories yourself: a lean, completely open-source Git platform that runs on the smallest VPS and still brings issues, pull requests, wiki and CI.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>By the end, <strong>Forgejo 16.0.3</strong> runs as a single container behind your Traefik, reachable at <code>https://YOUR_DOMAIN</code> with an automatic Let&rsquo;s Encrypt certificate. You create repositories via the web interface and clone/push them either <strong>over HTTPS</strong> or <strong>over SSH</strong>. As the database we use <strong>SQLite</strong> – for a personal or small-team Git server that&rsquo;s easily enough and saves an additional database container. Forgejo is the community fork of Gitea and clearly on the rise in the self-hosting scene.</p>
<p>Why self-host at all? Your code, your issues and your project history then live exclusively on <strong>your</strong> server – no dependence on the terms, rate limits or acquisitions of an external provider, and full data sovereignty. Forgejo is no stripped-down toy: it can do almost everything you know from GitHub (see step 7), but stays lean enough for a small VPS.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>A server with <strong>Debian 13</strong> and running Docker (tested on a netcup VPS).</li>
<li>A <strong><a href="/en/tutorials/traefik-reverse-proxy/">reverse proxy with Traefik</a></strong> (the <code>proxy</code> network and the resolver <code>le</code> from it are assumed) – Forgejo brings no own HTTPS, Traefik handles the encryption.</li>
<li>A <strong>(sub)domain</strong> that points to your server via an A/AAAA record (<code>YOUR_DOMAIN</code>).</li>
<li>For real backups: <strong><a href="/en/tutorials/restic-backups/">encrypted backups with Restic</a></strong>.</li>
</ul>
<p>Forgejo is frugal and runs even on the smallest vServer. How much server your planned setup needs in total is estimated by the <a href="/en/server-calculator/">server calculator</a>.</p>
<div class="not-prose my-6 overflow-hidden rounded-xl border border-paprika-200 bg-paprika-50 dark:border-paprika-800 dark:bg-paprika-900/20"
     data-track-content data-content-name="Affiliate-Box · /en/tutorials/forgejo-git-server/" data-content-piece="VPS 1000 G12">
  <div class="flex items-center justify-between border-b border-paprika-200 bg-paprika-100 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-paprika-700 dark:border-paprika-800 dark:bg-paprika-900/40 dark:text-paprika-300">
    <span>🍳 Recommendation</span>
    <span title="Links marked with * are affiliate links.">Ad</span>
  </div>
  <div class="flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:justify-between">
    <div>
      <p class="text-lg font-bold text-slate-900 dark:text-white">VPS 1000 G12</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">4 vCores · 8 GB RAM · 256 GB NVMe</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">from €10.36/month</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">Forgejo with SQLite runs comfortably on the VPS 1000.</p>
    </div>
    <a href="https://www.netcup.com/en/server/vps/vps-1000-g12-iv-12m?ref=44083" rel="sponsored noopener" target="_blank"
   data-track-event="Affiliate|netcup: Affiliate-Box|VPS 1000 G12 · {page}"
   class="inline-flex shrink-0 items-center justify-center rounded-lg bg-paprika-600 px-5 py-2.5 font-semibold text-white transition-colors hover:bg-paprika-700">
  Go to netcup →
</a>

  </div><div class="px-4 pb-4"><p class="not-prose my-3 flex flex-wrap items-center gap-x-2 gap-y-1 rounded-lg border border-herb-500/40 bg-herb-50 px-3 py-2 text-sm text-slate-700 dark:bg-herb-900/20 dark:text-slate-200">
  <span>💶 <strong>5 € voucher</strong> for new netcup customers:</span><code data-track-voucher="36nc17844976032"
        class="rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 dark:bg-slate-800 dark:text-herb-400">36nc17844976032</code>
  <span class="text-xs text-slate-500 dark:text-slate-400">(new customers only, no domains)</span>
</p></div>
</div>

<h2 id="step-by-step">Step by step</h2>
<h3 id="step-1-create-the-compose-file">Step 1: Create the Compose file</h3>
<p>Create a folder for the stack and change into it:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p /opt/forgejo <span class="o">&amp;&amp;</span> <span class="nb">cd</span> /opt/forgejo</span></span></code></pre></div>
</div>
<p>Create the file <code>compose.yaml</code>. Replace <code>YOUR_DOMAIN</code> with your real domain:</p>
<div class="sk-code">
  <span class="sk-code-head">YAML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">forgejo</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">codeberg.org/forgejo/forgejo:16.0.3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">forgejo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">USER_UID</span><span class="p">:</span><span class="w"> </span><span class="m">1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">USER_GID</span><span class="p">:</span><span class="w"> </span><span class="m">1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">FORGEJO__server__DOMAIN</span><span class="p">:</span><span class="w"> </span><span class="l">YOUR_DOMAIN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">FORGEJO__server__ROOT_URL</span><span class="p">:</span><span class="w"> </span><span class="l">https://YOUR_DOMAIN/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">FORGEJO__server__SSH_DOMAIN</span><span class="p">:</span><span class="w"> </span><span class="l">YOUR_DOMAIN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">FORGEJO__server__START_SSH_SERVER</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">FORGEJO__server__SSH_PORT</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;2222&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">FORGEJO__server__SSH_LISTEN_PORT</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;2222&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">FORGEJO__service__DISABLE_REGISTRATION</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">FORGEJO__database__DB_TYPE</span><span class="p">:</span><span class="w"> </span><span class="l">sqlite3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">forgejo_data:/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/etc/timezone:/etc/timezone:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/etc/localtime:/etc/localtime:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;2222:2222&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">healthcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">test</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;CMD&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;wget&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-q&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-O&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;/dev/null&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;http://localhost:3000/api/healthz&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">start_period</span><span class="p">:</span><span class="w"> </span><span class="l">30s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">start_interval</span><span class="p">:</span><span class="w"> </span><span class="l">2s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">30s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">retries</span><span class="p">:</span><span class="w"> </span><span class="m">3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.enable=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.forgejo.rule=Host(`YOUR_DOMAIN`)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.forgejo.entrypoints=websecure&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.forgejo.tls.certresolver=le&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.services.forgejo.loadbalancer.server.port=3000&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">proxy]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">forgejo_data</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">proxy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></div>
</div>
<p>The most important points in detail:</p>
<ul>
<li><strong><code>FORGEJO__…</code> variables</strong> configure Forgejo directly via environment variables (section and key via double underscore). <code>ROOT_URL</code> must be exactly the public HTTPS address, otherwise clone links and redirects point nowhere.</li>
<li><strong>Git over SSH</strong> is handled by Forgejo&rsquo;s <strong>built-in SSH server</strong> (<code>START_SSH_SERVER=true</code>) – so you don&rsquo;t have to touch your host&rsquo;s hardened SSH access (port 22). It listens on <strong>2222</strong> in the container and is published to the same host port.</li>
<li><strong><code>loadbalancer.server.port=3000</code></strong> tells Traefik the web interface runs internally on port 3000. Only SSH (2222) is published directly as a port – web access goes exclusively via Traefik.</li>
<li>The <strong>healthcheck</strong> with <strong><code>start_interval: 2s</code></strong> is deliberately set that way (more on that in &ldquo;When things go wrong&rdquo;).</li>
<li><strong><code>DISABLE_REGISTRATION: &quot;true&quot;</code></strong> closes open self-registration from the start – your Git server is thus not open to strangers. You still create your <strong>admin account</strong> perfectly normally in the initial install wizard (step 3); alternatively you could generate it automatically via <code>FORGEJO__admin__*</code> variables.</li>
</ul>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>SSH port: definitely set both values
  </p>
  <div class="prose-kitchen text-sm"><code>SSH_PORT</code> (the port number shown in the clone link) <strong>and</strong> <code>SSH_LISTEN_PORT</code> (the port the server actually listens on) must match. If you set only <code>SSH_PORT</code> and leave out <code>SSH_LISTEN_PORT</code>, Forgejo tries to listen on the old default port and crashes at startup with <code>bind: address already in use</code> in a restart loop.</div>
</div>
<h3 id="step-2-start-and-wait-for-healthy">Step 2: Start and wait for &ldquo;healthy&rdquo;</h3>
<p>Start the container:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose up -d</span></span></code></pre></div>
</div>
<p>Check the status:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose ps</span></span></code></pre></div>
</div>
<p>After a few seconds the container is <code>healthy</code>:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">NAME      IMAGE                                 SERVICE   STATUS                    PORTS
</span></span><span class="line"><span class="cl">forgejo   codeberg.org/forgejo/forgejo:16.0.3   forgejo   Up 12 seconds (healthy)   22/tcp, 3000/tcp, 0.0.0.0:2222-&gt;2222/tcp, [::]:2222-&gt;2222/tcp</span></span></code></pre></div>
</div>
<p>The <code>22/tcp</code> in the port list is only a port declared by the image, not a published one – the only port reachable from outside is the 2222 you mapped yourself.</p>
<p>Traefik now fetches the certificate in the background. Check from your machine that the web interface responds over HTTPS:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s https://YOUR_DOMAIN/api/healthz</span></span></code></pre></div>
</div>
<p>Expected output – Forgejo reports itself healthy. As long as the initial install (step 3) isn&rsquo;t finished, the endpoint answers briefly with status and slogan:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{
</span></span><span class="line"><span class="cl">  &#34;status&#34;: &#34;pass&#34;,
</span></span><span class="line"><span class="cl">  &#34;description&#34;: &#34;Forgejo: Beyond coding. We forge.&#34;
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<p>Once the wizard is done, the same URL additionally lists the individual checks <code>cache:ping</code> and <code>database:ping</code> – and shows your instance name instead of the slogan.</p>
<h3 id="step-3-initial-install--admin-account">Step 3: Initial install &amp; admin account</h3>
<p>Open <code>https://YOUR_DOMAIN</code> in the browser. On the first start, Forgejo shows the <strong>initial install</strong>. The database and server settings are already correctly pre-filled thanks to the environment variables – you only have to create the <strong>administrator account</strong> here (expand the corresponding section and enter a username, email and a strong password). A click on <strong>Install Forgejo</strong>, and after a brief restart you land on your empty dashboard:</p>
<p><figure class="my-6"><img src="/en/tutorials/forgejo-git-server/forgejo-dashboard_hu_61545266f48bd407.webp" srcset="/en/tutorials/forgejo-git-server/forgejo-dashboard_hu_6320e3496316b9ad.webp 480w, /en/tutorials/forgejo-git-server/forgejo-dashboard_hu_61545266f48bd407.webp 768w, /en/tutorials/forgejo-git-server/forgejo-dashboard_hu_daa5dccabd5ca7ac.webp 1200w, /en/tutorials/forgejo-git-server/forgejo-dashboard_hu_8f76629141bdd973.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/forgejo-git-server/forgejo-dashboard_hu_f9283f6aed07e8ec.webp"
    alt="Forgejo dashboard right after the initial setup – still without repositories." title="The dashboard after the first login."
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The dashboard after the first login.</figcaption></figure></p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>Tip
  </p>
  <div class="prose-kitchen text-sm">The install page stays open until the wizard has been completed once – so finish it <strong>right after <code>docker compose up -d</code></strong> and create your admin account in the process. Open self-registration is already disabled via <code>FORGEJO__service__DISABLE_REGISTRATION: &quot;true&quot;</code> in the Compose, so no one can create an account from outside.</div>
</div>
<h3 id="step-4-create-the-first-repository">Step 4: Create the first repository</h3>
<p>Click the <strong>+</strong> at the top right and then <strong>New Repository</strong>. Assign a name and check <strong>Initialize repository</strong> (creates a <code>README</code> right away so the repo isn&rsquo;t empty):</p>
<p><figure class="my-6"><img src="/en/tutorials/forgejo-git-server/forgejo-repo-anlegen_hu_12da8fc6c5a7f951.webp" srcset="/en/tutorials/forgejo-git-server/forgejo-repo-anlegen_hu_113cc065af230282.webp 480w, /en/tutorials/forgejo-git-server/forgejo-repo-anlegen_hu_12da8fc6c5a7f951.webp 768w, /en/tutorials/forgejo-git-server/forgejo-repo-anlegen_hu_8d21835a02d8b9d1.webp 1200w, /en/tutorials/forgejo-git-server/forgejo-repo-anlegen_hu_a6931b20498f6b83.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/forgejo-git-server/forgejo-repo-anlegen_hu_3c33d7e7a900dcee.webp"
    alt="The &ldquo;New Repository&rdquo; form in Forgejo with a name field and initialization option." title="Create a new repository."
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Create a new repository.</figcaption></figure></p>
<p>After creating it, you see the repository view. Via the <strong>HTTPS / SSH</strong> toggle you get the matching clone address:</p>
<p><figure class="my-6"><img src="/en/tutorials/forgejo-git-server/forgejo-repository_hu_8505c999f33cde90.webp" srcset="/en/tutorials/forgejo-git-server/forgejo-repository_hu_29ee2437e4f37723.webp 480w, /en/tutorials/forgejo-git-server/forgejo-repository_hu_8505c999f33cde90.webp 768w, /en/tutorials/forgejo-git-server/forgejo-repository_hu_48bd6aeb47f9832e.webp 1200w, /en/tutorials/forgejo-git-server/forgejo-repository_hu_e93682661bc6332f.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/forgejo-git-server/forgejo-repository_hu_856d0fe22716eeb2.webp"
    alt="Repository view in Forgejo with a README and the HTTPS/SSH toggle for the clone URL." title="The clone URL is available for either HTTPS or SSH."
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The clone URL is available for either HTTPS or SSH.</figcaption></figure></p>
<h3 id="step-5-clone-and-push-over-https">Step 5: Clone and push over HTTPS</h3>
<p>The fastest way is over HTTPS – it works immediately, without setting up keys:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone https://YOUR_DOMAIN/YOUR_USER/my-first-repo.git</span></span></code></pre></div>
</div>
<p>A <strong>public</strong> repository clones without any login – expected output:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Cloning into &#39;my-first-repo&#39;...
</span></span><span class="line"><span class="cl">remote: Enumerating objects: 3, done.
</span></span><span class="line"><span class="cl">remote: Counting objects: 100% (3/3), done.
</span></span><span class="line"><span class="cl">remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
</span></span><span class="line"><span class="cl">Receiving objects: 100% (3/3), done.</span></span></code></pre></div>
</div>
<p>For a private repository, Git asks for a username and password right here. That closes the loop – you create a file, commit it and push it back to your server:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> my-first-repo
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;My first note&#34;</span> &gt;&gt; notes.txt
</span></span><span class="line"><span class="cl">git add notes.txt
</span></span><span class="line"><span class="cl">git commit -m <span class="s2">&#34;First note&#34;</span>
</span></span><span class="line"><span class="cl">git push</span></span></code></pre></div>
</div>
<p>At the latest on <code>git push</code>, Forgejo wants to know who you are: Git asks for a username and password. At the end, Git confirms the new state on the server:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">To https://YOUR_DOMAIN/YOUR_USER/my-first-repo.git
</span></span><span class="line"><span class="cl">   f398f79..7bad57b  main -&gt; main</span></span></code></pre></div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>Tip
  </p>
  <div class="prose-kitchen text-sm">On a fresh client, the first commit fails with <code>Please tell me who you are</code> as long as Git doesn&rsquo;t know your name and email. Set them once globally: <code>git config --global user.name &quot;YOUR NAME&quot;</code> and <code>git config --global user.email &quot;YOUR_EMAIL&quot;</code>.</div>
</div>
<p>Seconds later the commit appears in the web interface – your code now lives versioned on your own server.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>Tip
  </p>
  <div class="prose-kitchen text-sm">For HTTPS, use an <strong>access token</strong> (under <strong>Settings → Applications</strong>) instead of your password. It can be revoked individually and is mandatory once you enable two-factor auth for your account.</div>
</div>
<h3 id="step-6-clone-and-push-over-ssh">Step 6: Clone and push over SSH</h3>
<p>For daily use, SSH is more convenient (no password per push). For that, store your <strong>public</strong> SSH key under <strong>Settings → SSH / GPG keys → Add key</strong>:</p>
<p><figure class="my-6"><img src="/en/tutorials/forgejo-git-server/forgejo-ssh-key_hu_79b231c9ee254ef8.webp" srcset="/en/tutorials/forgejo-git-server/forgejo-ssh-key_hu_c650c2c66f4f52dc.webp 480w, /en/tutorials/forgejo-git-server/forgejo-ssh-key_hu_79b231c9ee254ef8.webp 768w, /en/tutorials/forgejo-git-server/forgejo-ssh-key_hu_aa7e2af8e7734959.webp 1200w, /en/tutorials/forgejo-git-server/forgejo-ssh-key_hu_b8c26aaed3a380a.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/forgejo-git-server/forgejo-ssh-key_hu_1b89bdf5f36f7a52.webp"
    alt="Forgejo settings page &ldquo;Manage SSH keys&rdquo; with the &ldquo;Add key&rdquo; button." title="Store the public SSH key in your account."
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Store the public SSH key in your account.</figcaption></figure></p>
<p>You display your public key locally with <code>cat ~/.ssh/id_ed25519.pub</code> (if none exists yet: <code>ssh-keygen -t ed25519</code>). Then clone over SSH – note <strong>port 2222</strong>:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone ssh://git@YOUR_DOMAIN:2222/YOUR_USER/my-first-repo.git</span></span></code></pre></div>
</div>
<p>For that to work, the SSH port must be <strong>open in both firewalls</strong> – in the <a href="/en/tutorials/firewall-ufw-setup/">UFW firewall</a> (<code>ufw allow 2222/tcp</code>) and, if used, in the <a href="/en/tutorials/netcup-firewall-setup/">netcup firewall</a>.</p>
<h3 id="step-7-more-than-just-git-hosting">Step 7: More than just Git hosting</h3>
<p>A repository and cloning are just the beginning – Forgejo brings the complete workbench around it. You find these features in the navigation bar of every repository or your account:</p>
<ul>
<li><strong>Issues &amp; pull requests:</strong> a full-featured ticket system and code review including labels, milestones and assignments – the basis for team collaboration.</li>
<li><strong>Forgejo Actions:</strong> a CI/CD engine <strong>compatible with GitHub Actions</strong>. Existing <code>.github/workflows</code> files often run unchanged. Actions do, however, need a separate <strong>runner</strong> that executes the jobs – for security reasons it doesn&rsquo;t belong on the production host and has its own tutorial: <a href="/en/tutorials/forgejo-actions-runner/">Forgejo Actions: your own CI/CD runner with Docker</a> (makes the case for dedicated cores: a good reason for a root server, see <a href="/en/netcup-recommendation/">netcup recommendation</a>).</li>
<li><strong>Wiki &amp; releases:</strong> documentation right at the project and versioned release downloads.</li>
<li><strong>Package registry:</strong> Forgejo can host container images, npm, Maven, PyPI and other packages – handy if you want to store your own artifacts without running another service.</li>
<li><strong>Organizations &amp; teams:</strong> bundle repositories and control access rights per team.</li>
</ul>
<p>You also don&rsquo;t have to migrate your projects by hand: via <strong>+ → Migration</strong>, Forgejo imports an existing repository from GitHub, GitLab or another Forgejo/Gitea instance – including issues, pull requests and releases, not just the Git history. This way you switch from GitHub to your own server in a few minutes without losing anything.</p>
<h2 id="when-things-go-wrong">When things go wrong</h2>
<div class="troubleshoot not-prose">
<p><strong>The container restarts repeatedly (<code>Restarting</code>), the log says <code>bind: address already in use</code>.</strong>
The built-in SSH server collides with itself because <code>SSH_PORT</code> and <code>SSH_LISTEN_PORT</code> don&rsquo;t match. Set <strong>both</strong> to the same value (here <code>2222</code>) – then Forgejo starts cleanly.</p>
<p><strong>The container takes forever to become <code>healthy</code>.</strong> By default, Docker runs the first healthcheck only after the <code>interval</code> (30 s) – so the container looks &ldquo;unhealthy&rdquo; for 30 s+, even though Forgejo has long been ready in ~2 s. The solution is already in the Compose above: <strong><code>start_interval: 2s</code></strong> checks every 2 seconds during the startup phase and switches to <code>healthy</code> as soon as the app responds. (Requires Docker 25+ / Compose v2.20+ – given on Debian 13.)</p>
<p><strong>Traefik returns 502 Bad Gateway.</strong> Almost always the wrong port: Forgejo&rsquo;s web interface listens internally on <strong>3000</strong>, so <code>loadbalancer.server.port=3000</code> must be set and the container must be on the <code>proxy</code> network.</p>
<p><strong>Clone links show <code>localhost</code> or the wrong port.</strong> Then <code>ROOT_URL</code>, <code>SSH_DOMAIN</code> or <code>SSH_PORT</code> are wrong. Correct the values in the Compose and restart with <code>docker compose up -d</code>.</p>
<p><strong>SSH clone fails with <code>Permission denied (publickey)</code>.</strong> The SSH server is running, but your public key isn&rsquo;t stored in the account yet (step 6) – or you forgot the port <code>2222</code>.</p>

</div>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<p><strong>Everything lives in the <code>forgejo_data</code> volume</strong> (<code>/data</code>): the SQLite database, your repositories and the SSH server&rsquo;s host keys. This very volume is what should be backed up – cleanest with <a href="/en/tutorials/restic-backups/">Restic</a>. Because a SQLite file is written during operation, you back it up consistently by either briefly stopping the container (<code>docker compose stop</code>) or using Forgejo&rsquo;s built-in dump:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose <span class="nb">exec</span> -u git forgejo forgejo dump -t /tmp -f /tmp/forgejo-dump.zip</span></span></code></pre></div>
</div>
<p><code>-u git</code> runs the dump as the <code>git</code> user instead of root (as root, Forgejo refuses the dump). To <code>/tmp</code> instead of <code>/data</code>, because the <code>git</code> account can&rsquo;t write in <code>/data</code> itself (the root directory <code>/data</code> belongs to root, only the subfolders like <code>/data/gitea</code> and <code>/data/git</code> belong to the <code>git</code> user) and the dump would otherwise write recursively into the packed data directory. You then fetch the finished zip out of the container – to where Restic backs it up:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose cp forgejo:/tmp/forgejo-dump.zip ./forgejo-dump.zip</span></span></code></pre></div>
</div>
<p>The zip contains the database dump (<code>forgejo-db.sql</code>), the <code>app.ini</code> and all repository data – a complete, self-contained restore point.</p>
<p><strong>Updates:</strong> Forgejo releases new versions regularly (currently the 16 series). For an update, set the new tag in the <code>compose.yaml</code> (instead of <code>16.0.3</code>) and pull it:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose pull <span class="o">&amp;&amp;</span> docker compose up -d</span></span></code></pre></div>
</div>
<p>Because the data lives in the volume, repos and accounts are preserved. Deliberately pin the version to a fixed tag instead of <code>latest</code> and take a look at the <a href="https://forgejo.org/releases/">release notes</a> <strong>before</strong> a major jump – any migration hints are there. Also make a fresh backup before every update: a Git server without a working backup is a concentrated risk for all your code.</p>
<p><strong>Securing it:</strong> A publicly reachable Git server is a worthwhile target. Three things you should do right away: <strong>disable open registration</strong> (see step 3), enable <strong>two-factor authentication</strong> for your admin account under <strong>Settings → Security</strong>, and use a normal account instead of the administrator for everyday work. Keep Forgejo up to date promptly – the fast releases also contain security fixes. And publish only the ports you really need: outward, 443 (Traefik) and your SSH Git port 2222 suffice.</p>
]]></content:encoded></item></channel></rss>