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.
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
- Ein Server mit laufendem Traefik als Reverse Proxy (liefert das HTTPS-Zertifikat)
- Docker & Docker Compose installiert
- Eine Domain, die auf den Server zeigt – ersetze im Folgenden
DEINE_DOMAIN gitauf dem Server (apt install git), um das Theme zu holen
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.
💶 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:
mkdir -p /opt/hugo-demo/content/posts && cd /opt/hugo-demoHol dir das Theme PaperMod. Wir klonen es als normalen Ordner (nicht als Git-Submodul), damit es beim Docker-Build garantiert im Build-Kontext liegt:
git clone --depth=1 https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod
rm -rf themes/PaperMod/.gitDas --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:
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 = 1baseURL 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:
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.
EOFUnd ein erster Blog-Beitrag unter content/posts/:
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.
EOFZukunftsdatierte Beiträge werden nicht gebaut
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.
# 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 1hugo --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:
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:
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: trueDas 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
docker compose up -d --buildDocker baut zuerst das Image (Hugo-Build läuft in der ersten Stufe) und startet dann den Container. Prüfe den Status:
docker compose psNAME 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:
curl -sI https://DEINE_DOMAIN/ | head -1
curl -s https://DEINE_DOMAIN/ | grep -o 'content="Hugo [0-9.]*"'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:

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

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

Schritt 8: Neue Beiträge veröffentlichen
Der Alltag ist denkbar einfach: Neue Markdown-Datei anlegen, neu bauen:
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 --buildWeil 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) undnginx:1.31-alpinegelegentlich aktuell und baue neu; das übernimmt ansonsten dein normaler Update-Prozess. Das Theme aktualisierst du bei Bedarf mit einem erneutengit 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
Forgejo mit Docker & Traefik aufsetzen: eigener Git-Server mit HTTPS, Repos per Web-UI, Klonen über HTTPS und SSH – die …

HitKeep: datenschutzfreundliche Web-Analytics selbst hosten
HitKeep mit Docker und Traefik aufsetzen: cookielose, DSGVO-freundliche Website-Statistik auf dem eigenen Server – mit …

Jellyfin selbst hosten: dein eigener Medienserver hinter Traefik
Jellyfin mit Docker hinter Traefik aufsetzen: deine Filme, Serien und Musik streamen – mit HTTPS und der Mediathek auf …