Zum Inhalt springen
Serverküche
Suche

Die Suche wird geladen … (nur in der veröffentlichten Seite verfügbar).

Anwendungen Schwierigkeit: Fortgeschritten

Die eigene Website mit Hugo hosten – wie die Serverküche selbst

Statische Website mit Hugo bauen und im Docker-Container hinter Traefik ausliefern – schnell, sicher, ohne Datenbank. Genau das Setup dieser Seite.

· 8 Min. Lesezeit ·Dauer: ca. 45 Minuten
Inhaltsverzeichnis

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.

Was bauen wir?

Eine eigene Website mit dem statischen Site-Generator Hugo (v0.164.0), gebaut in einem mehrstufigen Docker-Image und ausgeliefert von nginx 1.31 hinter Traefik 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 PaperMod; das Prinzip gilt für jedes Hugo-Theme.

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.

Voraussetzungen

🍳 Empfehlung Anzeige

VPS 1000 G12

4 vCore · 8 GB RAM · 256 GB NVMe

ab 10,36 €/Monat

Eine statische Seite ist genügsam – der kleinste VPS reicht locker.

Zu netcup →

💶 5 € Gutschein für netcup-Neukunden:36nc17844976032 (nur Neukunden, keine Domains)

Schritt für Schritt

Schritt 1: Das Hugo-Projekt anlegen

Lege den Projektordner an. Alles, was die Seite ausmacht, lebt in diesem Verzeichnis – inklusive der Docker-Dateien:

Terminal
mkdir -p /opt/hugo-demo/content/posts && cd /opt/hugo-demo

Hol dir das Theme PaperMod. Wir klonen es als normalen Ordner (nicht als Git-Submodul), damit es beim Docker-Build garantiert im Build-Kontext liegt:

Terminal
git clone --depth=1 https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod
rm -rf themes/PaperMod/.git

Das --depth=1 holt nur den aktuellen Stand (kein Verlauf), und das anschließende Löschen von .git macht das Theme zu einem festen Bestandteil deines Projekts.

Schritt 2: Hugo konfigurieren

Die zentrale Konfiguration ist hugo.toml. Ersetze DEINE_DOMAIN durch deine echte Domain – Hugo baut absolute Links darauf auf:

TOML
baseURL = "https://DEINE_DOMAIN/"
languageCode = "de-de"
title = "Meine Serverküche-Seite"
theme = "PaperMod"

[params]
  description = "Ein Testblog, gehostet mit Hugo und Docker."

[[menu.main]]
  name = "Beiträge"
  url = "/posts/"
  weight = 1

baseURL 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 content/posts/ erzeugt.

Schritt 3: Inhalte als Markdown anlegen

Jede Seite ist eine Markdown-Datei mit einem kleinen Kopf (Front Matter). Die Startseite:

Terminal
cat > content/_index.md <<'EOF'
---
title: "Willkommen"
---
Diese Seite wird von **Hugo** erzeugt und als statisches HTML von nginx
ausgeliefert – genau wie die Serverküche selbst.
EOF

Und ein erster Blog-Beitrag unter content/posts/:

Terminal
cat > content/posts/erster-beitrag.md <<'EOF'
---
title: "Mein erster Beitrag"
date: 2026-07-20
tags: ["hugo", "docker"]
---
Hallo Welt! Diese Seite läuft im Docker-Container hinter Traefik mit
automatischem HTTPS.
EOF

Zukunftsdatierte Beiträge werden nicht gebaut

Hugo überspringt standardmäßig Inhalte mit einem date 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 hugo --buildFuture – sonst gilt: kein Zukunftsdatum.

Schritt 4: Das mehrstufige Dockerfile

Jetzt der Kern. Ein Multi-Stage-Build 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.

Schema des mehrstufigen Docker-Builds: Quelle → Hugo-Build-Stufe → nginx-Auslieferungs-Stufe → Browser
Multi-Stage-Build: Stufe 1 baut mit Hugo und wird verworfen, Stufe 2 liefert nur das fertige HTML mit nginx aus

Dockerfile
# Stufe 1: Seite mit Hugo bauen
FROM hugomods/hugo:0.164.0 AS build
WORKDIR /src
COPY . .
RUN hugo --gc --minify

# Stufe 2: statisch mit nginx ausliefern
FROM nginx:1.31-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /src/public /usr/share/nginx/html
EXPOSE 80
HEALTHCHECK --start-period=10s --start-interval=2s --interval=30s --timeout=3s --retries=3 \
  CMD wget -q --spider http://127.0.0.1/ || exit 1

hugo --gc --minify räumt beim Bauen auf (--gc) und komprimiert HTML/CSS/JS (--minify). Das COPY --from=build holt nur den Ordner /src/public (das fertige Ergebnis) in die zweite Stufe. Der HEALTHCHECK sorgt dafür, dass Traefik erst auf den Container leitet, wenn nginx wirklich antwortet.

Healthcheck gegen 127.0.0.1, nicht localhost

localhost löst im Container zuerst auf die IPv6-Adresse ::1 auf. Wenn deine nginx-Konfiguration nur auf IPv4 lauscht (listen 80;), scheitert ein Healthcheck gegen http://localhost/ mit „Connection refused“ – der Container bleibt unhealthy und Traefik leitet nie auf ihn. Nutze deshalb http://127.0.0.1/. (Genau dieser Fehler ist mir beim Testen dieses Tutorials passiert.)

Schritt 5: nginx sauber konfigurieren

Ohne eigene Konfiguration liefert nginx bei Tippfehlern in der URL seine nackte Standard-404-Seite aus. Hugo bringt eine eigene, zum Design passende 404.html mit – die soll nginx nutzen:

NGINX
server {
    listen 80;
    server_name _;
    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    # Hugos eigene 404-Seite ausliefern statt der nginx-Standardseite
    error_page 404 /404.html;

    # Statische Assets lange cachen (Hugo hängt Hashes an Dateinamen)
    location ~* \.(css|js|woff2?|png|jpg|jpeg|svg|webp|ico)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

try_files $uri $uri/ =404 sucht die angefragte Datei, dann das Verzeichnis (Hugos „Pretty URLs“ wie /posts/erster-beitrag/ sind Ordner mit index.html), sonst 404. Das expires 30d für Assets ist unbedenklich, weil Hugo bei jeder Änderung einen neuen Datei-Hash vergibt – der Browser lädt neue Versionen trotzdem sofort.

Schritt 6: Compose mit Traefik verbinden

Die compose.yaml baut das Image und hängt den Container ins proxy-Netz zu Traefik. Ersetze DEINE_DOMAIN:

YAML
services:
  website:
    build: .
    image: meine-website:latest
    restart: unless-stopped
    networks: [proxy]
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.website.rule=Host(`DEINE_DOMAIN`)"
      - "traefik.http.routers.website.entrypoints=websecure"
      - "traefik.http.routers.website.tls.certresolver=le"
      - "traefik.http.services.website.loadbalancer.server.port=80"

networks:
  proxy:
    external: true

Das ist das etablierte Traefik-Rezept: vier Labels für Router, HTTPS-Entrypoint, Zertifikats-Resolver und Ziel-Port. Warum kein ports:? Weil Traefik den Container über das proxy-Netz erreicht – mehr dazu in Docker-Netzwerke verstehen.

Schritt 7: Bauen, starten, prüfen

Terminal
docker compose up -d --build

Docker baut zuerst das Image (Hugo-Build läuft in der ersten Stufe) und startet dann den Container. Prüfe den Status:

Terminal
docker compose ps
Ausgabe
NAME                  IMAGE                  SERVICE   STATUS
hugo-demo-website-1   meine-website:latest   website   Up (healthy)

Warte auf healthy (ohne das leitet Traefik nicht). Dann prüfe von außen, dass die Seite mit gültigem HTTPS antwortet:

Terminal
curl -sI https://DEINE_DOMAIN/ | head -1
curl -s https://DEINE_DOMAIN/ | grep -o 'content="Hugo [0-9.]*"'
Ausgabe
HTTP/2 200
content="Hugo 0.164.0"

Der zweite Befehl liest das generator-Meta-Tag aus – ein netter Beweis, dass wirklich Hugo 0.164.0 die Seite gebaut hat. Im Browser siehst du jetzt deine Startseite:

Die von Hugo erzeugte Startseite mit dem PaperMod-Theme unter der eigenen HTTPS-Domain
Die Startseite – reines HTML, ausgeliefert von nginx hinter Traefik

Ein Klick auf einen Beitrag zeigt den gerenderten Markdown-Inhalt samt Datum und Tags:

Ein gerenderter Blog-Beitrag mit Titel, Datum, Fließtext und Tags
Der erste Beitrag – aus einer einzigen Markdown-Datei erzeugt

Und eine falsche URL landet nicht auf der nginx-Standardseite, sondern auf Hugos eigener 404 – dank error_page:

Hugos gestaltete 404-Seite im PaperMod-Look statt der nginx-Standardseite
Die eigene 404-Seite dank error_page in der nginx-Konfiguration

Schritt 8: Neue Beiträge veröffentlichen

Der Alltag ist denkbar einfach: Neue Markdown-Datei anlegen, neu bauen:

Terminal
cat > content/posts/zweiter-beitrag.md <<'EOF'
---
title: "Zweiter Beitrag"
date: 2026-07-21
---
Noch ein Beitrag – gleich online nach dem Rebuild.
EOF
docker compose up -d --build

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 Forgejo) und den Rebuild bei jedem Push automatisch von einem Actions-Runner auslösen lassen – so betreibt die Serverküche ihre eigene Seite.

Wenn es nicht funktioniert

Der Container bleibt unhealthy. Fast immer der IPv6-Fallstrick aus Schritt 4: Der Healthcheck fragt localhost ab, nginx lauscht aber nur auf IPv4. Auf http://127.0.0.1/ umstellen. Prüfen mit docker inspect --format '{{.State.Health.Status}}' CONTAINER.

Traefik zeigt 404 page not found (statt deiner Seite). Das ist die Traefik-404, nicht die von nginx – Traefik findet keinen passenden Router. Häufigste Ursachen: Der Container ist noch nicht healthy, die Host()-Regel enthält die falsche Domain, oder das proxy-Netz ist nicht external. Siehe das 502/404-Kapitel in Docker-Netzwerke verstehen.

Alle Links und Bilder sind kaputt. Die baseURL in hugo.toml stimmt nicht mit der echten Domain überein. Hugo backt absolute URLs auf Basis dieses Werts ein – korrigieren und neu bauen.

Ein Beitrag erscheint nicht. Prüfe das date im Front Matter: Liegt es in der Zukunft, baut Hugo den Beitrag nicht (siehe Warn-Box in Schritt 3). Auch ein draft: true versteckt Inhalte im normalen Build.

theme "PaperMod" not found beim Build. Der Theme-Ordner fehlt im Build-Kontext – meist, weil er ein Git-Submodul ist, das nicht mitkopiert wurde, oder eine .dockerignore ihn ausschließt. Theme wie in Schritt 1 als echten Ordner ablegen.

Wartung & Backups

  • Updates sind entspannt. Es gibt keine laufende Software mit Sicherheitslücken im Betrieb – nur zwei gepinnte Build-Bausteine. Halte den Hugo-Tag (hugomods/hugo:0.164.0) und nginx:1.31-alpine gelegentlich aktuell und baue neu; das übernimmt ansonsten dein normaler Update-Prozess. Das Theme aktualisierst du bei Bedarf mit einem erneuten git clone.
  • Das Backup ist trivial – aber wichtig. Deine gesamte Website ist der Projektordner (Markdown, hugo.toml, Theme, Docker-Dateien). Er gehört in ein Git-Repository und/oder ins Restic-Backup. Die generierten HTML-Dateien musst du nicht sichern – sie entstehen jederzeit neu aus der Quelle.
  • Kein Datenbank-Risiko. 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.

Feedback per E-Mail: feedback@serverkueche.de

Das könnte dir auch schmecken

Forgejo: der eigene Git-Server hinter Traefik
Anwendungen Fortgeschritten

Forgejo: der eigene Git-Server hinter Traefik

Forgejo mit Docker & Traefik aufsetzen: eigener Git-Server mit HTTPS, Repos per Web-UI, Klonen über HTTPS und SSH – die …

· 10 Min. Lesezeit