<?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>Website – Serverküche</title><link>https://serverkueche.de/tags/website/</link><description>Website – Neueste Beiträge von Serverküche</description><generator>Hugo</generator><language>de-DE</language><managingEditor>feedback@serverkueche.de (Serverküche)</managingEditor><webMaster>feedback@serverkueche.de (Serverküche)</webMaster><copyright>2026 Serverküche</copyright><lastBuildDate>Thu, 27 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/tags/website/index.xml" rel="self" type="application/rss+xml"/><item><title>Die eigene Website mit Hugo hosten – wie die Serverküche selbst</title><link>https://serverkueche.de/tutorials/eigene-website-mit-hugo/</link><pubDate>Thu, 27 Aug 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/tutorials/eigene-website-mit-hugo/</guid><description>Statische Website mit Hugo bauen und im Docker-Container hinter Traefik ausliefern – schnell, sicher, ohne Datenbank. Genau das Setup dieser Seite.</description><content:encoded><![CDATA[<p>Diese Seite, die du gerade liest, läuft mit genau dem Setup aus diesem Tutorial: Hugo baut aus Markdown-Dateien statisches HTML, das ein winziger nginx-Container hinter Traefik ausliefert. Kein WordPress, keine Datenbank, keine PHP-Sicherheitsupdates – und trotzdem eine vollwertige Website. Das ist maximales Dogfooding: Du liest ein Rezept auf seinem eigenen Ergebnis.</p>
<h2 id="was-bauen-wir">Was bauen wir?</h2>
<p>Eine eigene Website mit dem statischen Site-Generator <strong>Hugo</strong> (v0.164.0), gebaut in einem mehrstufigen Docker-Image und ausgeliefert von <strong>nginx 1.31</strong> hinter <a href="/tutorials/reverse-proxy-traefik/">Traefik</a> mit automatischem HTTPS. Am Ende hast du eine Blog-fähige Seite unter deiner Domain, die aus einfachen Markdown-Dateien entsteht – und du weißt, wie du neue Beiträge mit einem einzigen Rebuild online bringst. Als Design nutzen wir das populäre Theme <strong>PaperMod</strong>; das Prinzip gilt für jedes Hugo-Theme.</p>
<p>Der große Vorteil gegenüber einem klassischen CMS wie WordPress: Es gibt keine Angriffsfläche im Betrieb. Ausgeliefert wird reines HTML, der Container enthält keinen Interpreter und keine Datenbank, die kompromittiert werden könnte.</p>
<h2 id="voraussetzungen">Voraussetzungen</h2>
<ul>
<li>Ein Server mit laufendem <a href="/tutorials/reverse-proxy-traefik/">Traefik als Reverse Proxy</a> (liefert das HTTPS-Zertifikat)</li>
<li><a href="/tutorials/docker-installieren/">Docker &amp; Docker Compose</a> installiert</li>
<li>Eine <a href="/tutorials/domain-mit-server-verbinden/">Domain, die auf den Server zeigt</a> – ersetze im Folgenden <code>DEINE_DOMAIN</code></li>
<li><code>git</code> auf dem Server (<code>apt install git</code>), um das Theme zu holen</li>
</ul>
<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 · /tutorials/eigene-website-mit-hugo/" 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>🍳 Empfehlung</span>
    <span title="Mit * markierte Links sind Affiliate-Links.">Anzeige</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 vCore · 8 GB RAM · 256 GB NVMe</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">ab 10,36 €/Monat</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">Eine statische Seite ist genügsam – der kleinste VPS reicht locker.</p>
    </div>
    <a href="https://www.netcup.com/de/server/vps/vps-1000-g12-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">
  Zu 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 € Gutschein</strong> für netcup-Neukunden:</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">(nur Neukunden, keine Domains)</span>
</p></div>
</div>

<h2 id="schritt-für-schritt">Schritt für Schritt</h2>
<h3 id="schritt-1-das-hugo-projekt-anlegen">Schritt 1: Das Hugo-Projekt anlegen</h3>
<p>Lege den Projektordner an. Alles, was die Seite ausmacht, lebt in diesem Verzeichnis – inklusive der Docker-Dateien:</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/hugo-demo/content/posts <span class="o">&amp;&amp;</span> <span class="nb">cd</span> /opt/hugo-demo</span></span></code></pre></div>
</div>
<p>Hol dir das Theme <strong>PaperMod</strong>. Wir klonen es als normalen Ordner (nicht als Git-Submodul), damit es beim Docker-Build garantiert im Build-Kontext liegt:</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 --depth<span class="o">=</span><span class="m">1</span> https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod
</span></span><span class="line"><span class="cl">rm -rf themes/PaperMod/.git</span></span></code></pre></div>
</div>
<p>Das <code>--depth=1</code> holt nur den aktuellen Stand (kein Verlauf), und das anschließende Löschen von <code>.git</code> macht das Theme zu einem festen Bestandteil deines Projekts.</p>
<h3 id="schritt-2-hugo-konfigurieren">Schritt 2: Hugo konfigurieren</h3>
<p>Die zentrale Konfiguration ist <code>hugo.toml</code>. Ersetze <code>DEINE_DOMAIN</code> durch deine echte Domain – Hugo baut absolute Links darauf auf:</p>
<div class="sk-code">
  <span class="sk-code-head">TOML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="nx">baseURL</span> <span class="p">=</span> <span class="s2">&#34;https://DEINE_DOMAIN/&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">languageCode</span> <span class="p">=</span> <span class="s2">&#34;de-de&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">title</span> <span class="p">=</span> <span class="s2">&#34;Meine Serverküche-Seite&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">theme</span> <span class="p">=</span> <span class="s2">&#34;PaperMod&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">params</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Ein Testblog, gehostet mit Hugo und Docker.&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[[</span><span class="nx">menu</span><span class="p">.</span><span class="nx">main</span><span class="p">]]</span>
</span></span><span class="line"><span class="cl">  <span class="nx">name</span> <span class="p">=</span> <span class="s2">&#34;Beiträge&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nx">url</span> <span class="p">=</span> <span class="s2">&#34;/posts/&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nx">weight</span> <span class="p">=</span> <span class="mi">1</span></span></span></code></pre></div>
</div>
<p><code>baseURL</code> ist entscheidend: Steht dort die falsche Domain, zeigen alle internen Links und Assets ins Leere. Das Menü verweist auf die Beitragsübersicht, die Hugo automatisch aus dem Ordner <code>content/posts/</code> erzeugt.</p>
<h3 id="schritt-3-inhalte-als-markdown-anlegen">Schritt 3: Inhalte als Markdown anlegen</h3>
<p>Jede Seite ist eine Markdown-Datei mit einem kleinen Kopf (Front Matter). Die Startseite:</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">cat &gt; content/_index.md <span class="s">&lt;&lt;&#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">---
</span></span></span><span class="line"><span class="cl"><span class="s">title: &#34;Willkommen&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">---
</span></span></span><span class="line"><span class="cl"><span class="s">Diese Seite wird von **Hugo** erzeugt und als statisches HTML von nginx
</span></span></span><span class="line"><span class="cl"><span class="s">ausgeliefert – genau wie die Serverküche selbst.
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span></span></span></code></pre></div>
</div>
<p>Und ein erster Blog-Beitrag unter <code>content/posts/</code>:</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">cat &gt; content/posts/erster-beitrag.md <span class="s">&lt;&lt;&#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">---
</span></span></span><span class="line"><span class="cl"><span class="s">title: &#34;Mein erster Beitrag&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">date: 2026-07-20
</span></span></span><span class="line"><span class="cl"><span class="s">tags: [&#34;hugo&#34;, &#34;docker&#34;]
</span></span></span><span class="line"><span class="cl"><span class="s">---
</span></span></span><span class="line"><span class="cl"><span class="s">Hallo Welt! Diese Seite läuft im Docker-Container hinter Traefik mit
</span></span></span><span class="line"><span class="cl"><span class="s">automatischem HTTPS.
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span></span></span></code></pre></div>
</div>
<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>Zukunftsdatierte Beiträge werden nicht gebaut
  </p>
  <div class="prose-kitchen text-sm">Hugo überspringt standardmäßig Inhalte mit einem <code>date</code> in der Zukunft. Trägst du versehentlich ein späteres Datum ein, ist der Beitrag online nicht zu finden (404), obwohl die Datei existiert. Für geplante Veröffentlichungen baust du bewusst mit <code>hugo --buildFuture</code> – sonst gilt: kein Zukunftsdatum.</div>
</div>
<h3 id="schritt-4-das-mehrstufige-dockerfile">Schritt 4: Das mehrstufige Dockerfile</h3>
<p>Jetzt der Kern. Ein <strong>Multi-Stage-Build</strong> trennt das Bauen vom Ausliefern: Die erste Stufe enthält Hugo und Node, baut die Seite und wird danach weggeworfen. Die zweite Stufe ist ein winziges nginx-Image, das nur das fertige HTML enthält – ohne Hugo, ohne Quellcode.</p>
<p><figure class="my-6"><img src="build-schema.svg" alt="Schema des mehrstufigen Docker-Builds: Quelle → Hugo-Build-Stufe → nginx-Auslieferungs-Stufe → Browser" title="Multi-Stage-Build: Stufe 1 baut mit Hugo und wird verworfen, Stufe 2 liefert nur das fertige HTML mit nginx aus" loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Multi-Stage-Build: Stufe 1 baut mit Hugo und wird verworfen, Stufe 2 liefert nur das fertige HTML mit nginx aus</figcaption></figure></p>
<div class="sk-code">
  <span class="sk-code-head">Dockerfile</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="cl"><span class="c"># Stufe 1: Seite mit Hugo bauen</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">hugomods/hugo:0.164.0</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">build</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">WORKDIR</span><span class="w"> </span><span class="s">/src</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> . .<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> hugo --gc --minify<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Stufe 2: statisch mit nginx ausliefern</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">nginx:1.31-alpine</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> nginx.conf /etc/nginx/conf.d/default.conf<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> --from<span class="o">=</span>build /src/public /usr/share/nginx/html<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">EXPOSE</span><span class="w"> </span><span class="s">80</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">HEALTHCHECK</span> --start-period<span class="o">=</span>10s --start-interval<span class="o">=</span>2s --interval<span class="o">=</span>30s --timeout<span class="o">=</span>3s --retries<span class="o">=</span><span class="m">3</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  CMD wget -q --spider http://127.0.0.1/ <span class="o">||</span> <span class="nb">exit</span> <span class="m">1</span></span></span></code></pre></div>
</div>
<p><code>hugo --gc --minify</code> räumt beim Bauen auf (<code>--gc</code>) und komprimiert HTML/CSS/JS (<code>--minify</code>). Das <code>COPY --from=build</code> holt nur den Ordner <code>/src/public</code> (das fertige Ergebnis) in die zweite Stufe. Der <code>HEALTHCHECK</code> sorgt dafür, dass Traefik erst auf den Container leitet, wenn nginx wirklich antwortet.</p>
<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>Healthcheck gegen 127.0.0.1, nicht localhost
  </p>
  <div class="prose-kitchen text-sm"><code>localhost</code> löst im Container zuerst auf die IPv6-Adresse <code>::1</code> auf. Wenn deine nginx-Konfiguration nur auf IPv4 lauscht (<code>listen 80;</code>), scheitert ein Healthcheck gegen <code>http://localhost/</code> mit „Connection refused“ – der Container bleibt <code>unhealthy</code> und Traefik leitet nie auf ihn. Nutze deshalb <code>http://127.0.0.1/</code>. (Genau dieser Fehler ist mir beim Testen dieses Tutorials passiert.)</div>
</div>
<h3 id="schritt-5-nginx-sauber-konfigurieren">Schritt 5: nginx sauber konfigurieren</h3>
<p>Ohne eigene Konfiguration liefert nginx bei Tippfehlern in der URL seine nackte Standard-404-Seite aus. Hugo bringt eine eigene, zum Design passende <code>404.html</code> mit – die soll nginx nutzen:</p>
<div class="sk-code">
  <span class="sk-code-head">NGINX</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="k">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">listen</span> <span class="mi">80</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">server_name</span> <span class="s">_</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">root</span> <span class="s">/usr/share/nginx/html</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">index</span> <span class="s">index.html</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">location</span> <span class="s">/</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="nv">$uri/</span> <span class="p">=</span><span class="mi">404</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Hugos eigene 404-Seite ausliefern statt der nginx-Standardseite
</span></span></span><span class="line"><span class="cl">    <span class="kn">error_page</span> <span class="mi">404</span> <span class="s">/404.html</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Statische Assets lange cachen (Hugo hängt Hashes an Dateinamen)
</span></span></span><span class="line"><span class="cl">    <span class="kn">location</span> <span class="p">~</span><span class="sr">*</span> <span class="s">\.(css|js|woff2?|png|jpg|jpeg|svg|webp|ico)</span>$ <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kn">expires</span> <span class="s">30d</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">add_header</span> <span class="s">Cache-Control</span> <span class="s">&#34;public,</span> <span class="s">immutable&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
</div>
<p><code>try_files $uri $uri/ =404</code> sucht die angefragte Datei, dann das Verzeichnis (Hugos „Pretty URLs“ wie <code>/posts/erster-beitrag/</code> sind Ordner mit <code>index.html</code>), sonst 404. Das <code>expires 30d</code> für Assets ist unbedenklich, weil Hugo bei jeder Änderung einen neuen Datei-Hash vergibt – der Browser lädt neue Versionen trotzdem sofort.</p>
<h3 id="schritt-6-compose-mit-traefik-verbinden">Schritt 6: Compose mit Traefik verbinden</h3>
<p>Die <code>compose.yaml</code> baut das Image und hängt den Container ins <code>proxy</code>-Netz zu Traefik. Ersetze <code>DEINE_DOMAIN</code>:</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">website</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">build</span><span class="p">:</span><span class="w"> </span><span class="l">.</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">meine-website:latest</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">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="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.website.rule=Host(`DEINE_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.website.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.website.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.website.loadbalancer.server.port=80&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><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>Das ist das etablierte <a href="/tutorials/reverse-proxy-traefik/">Traefik-Rezept</a>: vier Labels für Router, HTTPS-Entrypoint, Zertifikats-Resolver und Ziel-Port. Warum kein <code>ports:</code>? Weil Traefik den Container über das <code>proxy</code>-Netz erreicht – mehr dazu in <a href="/tutorials/docker-netzwerke-verstehen/">Docker-Netzwerke verstehen</a>.</p>
<h3 id="schritt-7-bauen-starten-prüfen">Schritt 7: Bauen, starten, prüfen</h3>
<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 --build</span></span></code></pre></div>
</div>
<p>Docker baut zuerst das Image (Hugo-Build läuft in der ersten Stufe) und startet dann den Container. Prüfe den 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>
<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
</span></span><span class="line"><span class="cl">hugo-demo-website-1   meine-website:latest   website   Up (healthy)</span></span></code></pre></div>
</div>
<p>Warte auf <code>healthy</code> (ohne das leitet Traefik nicht). Dann prüfe von außen, dass die Seite mit gültigem HTTPS antwortet:</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 -sI https://DEINE_DOMAIN/ <span class="p">|</span> head -1
</span></span><span class="line"><span class="cl">curl -s https://DEINE_DOMAIN/ <span class="p">|</span> grep -o <span class="s1">&#39;content=&#34;Hugo [0-9.]*&#34;&#39;</span></span></span></code></pre></div>
</div>
<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">HTTP/2 200
</span></span><span class="line"><span class="cl">content=&#34;Hugo 0.164.0&#34;</span></span></code></pre></div>
</div>
<p>Der zweite Befehl liest das <code>generator</code>-Meta-Tag aus – ein netter Beweis, dass wirklich Hugo 0.164.0 die Seite gebaut hat. Im Browser siehst du jetzt deine Startseite:</p>
<p><figure class="my-6"><img src="/tutorials/eigene-website-mit-hugo/hugo-home_hu_b92a5a5f0b2f30eb.webp" srcset="/tutorials/eigene-website-mit-hugo/hugo-home_hu_653801c263c95636.webp 480w, /tutorials/eigene-website-mit-hugo/hugo-home_hu_b92a5a5f0b2f30eb.webp 768w, /tutorials/eigene-website-mit-hugo/hugo-home_hu_eefa3dbfce7bfbee.webp 1200w, /tutorials/eigene-website-mit-hugo/hugo-home_hu_759d4bef68d7a0b9.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/eigene-website-mit-hugo/hugo-home_hu_8be8bd040f63076.webp"
    alt="Die von Hugo erzeugte Startseite mit dem PaperMod-Theme unter der eigenen HTTPS-Domain" title="Die Startseite – reines HTML, ausgeliefert von nginx hinter Traefik"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Die Startseite – reines HTML, ausgeliefert von nginx hinter Traefik</figcaption></figure></p>
<p>Ein Klick auf einen Beitrag zeigt den gerenderten Markdown-Inhalt samt Datum und Tags:</p>
<p><figure class="my-6"><img src="/tutorials/eigene-website-mit-hugo/hugo-post_hu_63af8811eb1f92a.webp" srcset="/tutorials/eigene-website-mit-hugo/hugo-post_hu_5b9b3eac840c4af1.webp 480w, /tutorials/eigene-website-mit-hugo/hugo-post_hu_63af8811eb1f92a.webp 768w, /tutorials/eigene-website-mit-hugo/hugo-post_hu_2e8d73175e561e96.webp 1200w, /tutorials/eigene-website-mit-hugo/hugo-post_hu_bb41a6378cb71922.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/eigene-website-mit-hugo/hugo-post_hu_6acc2a057c23e9d2.webp"
    alt="Ein gerenderter Blog-Beitrag mit Titel, Datum, Fließtext und Tags" title="Der erste Beitrag – aus einer einzigen Markdown-Datei erzeugt"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Der erste Beitrag – aus einer einzigen Markdown-Datei erzeugt</figcaption></figure></p>
<p>Und eine falsche URL landet nicht auf der nginx-Standardseite, sondern auf Hugos eigener 404 – dank <code>error_page</code>:</p>
<p><figure class="my-6"><img src="/tutorials/eigene-website-mit-hugo/hugo-404_hu_c705ec25b988a111.webp" srcset="/tutorials/eigene-website-mit-hugo/hugo-404_hu_8b6272f4d294b8ae.webp 480w, /tutorials/eigene-website-mit-hugo/hugo-404_hu_c705ec25b988a111.webp 768w, /tutorials/eigene-website-mit-hugo/hugo-404_hu_c0dff1cf47ca8997.webp 1200w, /tutorials/eigene-website-mit-hugo/hugo-404_hu_6bd53fba95a973df.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/eigene-website-mit-hugo/hugo-404_hu_45b33ae46f06bf7e.webp"
    alt="Hugos gestaltete 404-Seite im PaperMod-Look statt der nginx-Standardseite" title="Die eigene 404-Seite dank error_page in der nginx-Konfiguration"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Die eigene 404-Seite dank error_page in der nginx-Konfiguration</figcaption></figure></p>
<h3 id="schritt-8-neue-beiträge-veröffentlichen">Schritt 8: Neue Beiträge veröffentlichen</h3>
<p>Der Alltag ist denkbar einfach: Neue Markdown-Datei anlegen, neu bauen:</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">cat &gt; content/posts/zweiter-beitrag.md <span class="s">&lt;&lt;&#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">---
</span></span></span><span class="line"><span class="cl"><span class="s">title: &#34;Zweiter Beitrag&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">date: 2026-07-21
</span></span></span><span class="line"><span class="cl"><span class="s">---
</span></span></span><span class="line"><span class="cl"><span class="s">Noch ein Beitrag – gleich online nach dem Rebuild.
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">docker compose up -d --build</span></span></code></pre></div>
</div>
<p>Weil der Build nur Sekunden dauert und alles im selben Ordner liegt, kannst du dieses Projekt bequem in ein Git-Repository legen (z. B. dein <a href="/tutorials/forgejo-git-server/">Forgejo</a>) und den Rebuild bei jedem Push automatisch von einem <a href="/tutorials/forgejo-actions-runner/">Actions-Runner</a> auslösen lassen – so betreibt die Serverküche ihre eigene Seite.</p>
<h2 id="wenn-es-nicht-funktioniert">Wenn es nicht funktioniert</h2>
<div class="troubleshoot not-prose">
<p><strong>Der Container bleibt <code>unhealthy</code>.</strong> Fast immer der IPv6-Fallstrick aus Schritt 4: Der Healthcheck fragt <code>localhost</code> ab, nginx lauscht aber nur auf IPv4. Auf <code>http://127.0.0.1/</code> umstellen. Prüfen mit <code>docker inspect --format '{{.State.Health.Status}}' CONTAINER</code>.</p>
<p><strong>Traefik zeigt <code>404 page not found</code> (statt deiner Seite).</strong> Das ist die Traefik-404, nicht die von nginx – Traefik findet keinen passenden Router. Häufigste Ursachen: Der Container ist noch nicht <code>healthy</code>, die <code>Host()</code>-Regel enthält die falsche Domain, oder das <code>proxy</code>-Netz ist nicht <code>external</code>. Siehe das 502/404-Kapitel in <a href="/tutorials/docker-netzwerke-verstehen/">Docker-Netzwerke verstehen</a>.</p>
<p><strong>Alle Links und Bilder sind kaputt.</strong> Die <code>baseURL</code> in <code>hugo.toml</code> stimmt nicht mit der echten Domain überein. Hugo backt absolute URLs auf Basis dieses Werts ein – korrigieren und neu bauen.</p>
<p><strong>Ein Beitrag erscheint nicht.</strong> Prüfe das <code>date</code> im Front Matter: Liegt es in der Zukunft, baut Hugo den Beitrag nicht (siehe Warn-Box in Schritt 3). Auch ein <code>draft: true</code> versteckt Inhalte im normalen Build.</p>
<p><strong><code>theme &quot;PaperMod&quot; not found</code> beim Build.</strong> Der Theme-Ordner fehlt im Build-Kontext – meist, weil er ein Git-Submodul ist, das nicht mitkopiert wurde, oder eine <code>.dockerignore</code> ihn ausschließt. Theme wie in Schritt 1 als echten Ordner ablegen.</p>

</div>

<h2 id="wartung--backups">Wartung &amp; Backups</h2>
<ul>
<li><strong>Updates sind entspannt.</strong> Es gibt keine laufende Software mit Sicherheitslücken im Betrieb – nur zwei gepinnte Build-Bausteine. Halte den Hugo-Tag (<code>hugomods/hugo:0.164.0</code>) und <code>nginx:1.31-alpine</code> gelegentlich aktuell und baue neu; das übernimmt ansonsten dein normaler <a href="/tutorials/docker-stack-aktuell-halten/">Update-Prozess</a>. Das Theme aktualisierst du bei Bedarf mit einem erneuten <code>git clone</code>.</li>
<li><strong>Das Backup ist trivial – aber wichtig.</strong> Deine gesamte Website <em>ist</em> der Projektordner (Markdown, <code>hugo.toml</code>, Theme, Docker-Dateien). Er gehört in ein Git-Repository und/oder ins <a href="/tutorials/backups-mit-restic/">Restic-Backup</a>. Die generierten HTML-Dateien musst du nicht sichern – sie entstehen jederzeit neu aus der Quelle.</li>
<li><strong>Kein Datenbank-Risiko.</strong> Weil es keine Datenbank und keinen Login gibt, entfällt die größte Wartungslast klassischer CMS. Der einzige „Zustand“ deiner Seite ist der Inhalt, den du selbst schreibst – versioniert in Git ist er zugleich Backup und Änderungshistorie.
</content></li>
</ul>
]]></content:encoded></item></channel></rss>