Zum Hauptinhalt springen
S-EDV news
← Alle Anleitungen
📘 Anleitung Docker 04.11.2026 · 11 min Lesezeit

Eigene Website mit Hugo: statisch, schnell und mit strenger CSP

Teil 5 der Reihe Server im Netz: eine statische Website mit Hugo und PaperMod per Docker bauen, auf Caddy ausrollen und mit Lighthouse messen. Mit dem Theme-Release, das nicht mehr baut, und Hashes statt unsafe-inline für die Content-Security-Policy.

Geprüft am 04.11.2026 · für Hugo 0.169.0, PaperMod 1181196 (25.10.2026), Lighthouse 13.6.0, Caddy 2.11.7

Mit KI erstellt – redaktionelle Prüfung ausstehend

WerbelinksMit * markierte Links sind Werbelinks: Bei einem Kauf erhalten wir eine Provision, der Preis bleibt gleich. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Mehr dazu

Symbolbild: Server mit Netzkabel vor einer Weltkarte, Titel Website mit Hugo

Eine Website muss keine Datenbank haben. Für Firmenseiten, Projektseiten und Blogs reicht oft eine Sammlung fertiger HTML-Dateien, die der Webserver nur ausliefert. Es gibt kein PHP, kein Login und keine Plugins, die ständig Updates brauchen. Hugo baut solche Seiten aus Markdown-Texten in Sekundenbruchteilen. Diese Anleitung setzt eine Hugo-Website mit dem verbreiteten Theme PaperMod auf, rollt sie auf den Caddy-Server aus Teil 4 aus und misst sie mit Lighthouse. Dabei zeigen wir zwei Stolpersteine aus dem Test: ein Theme-Release, das mit dem aktuellen Hugo nicht mehr baut, und die strenge Content-Security-Policy aus Teil 4, die mehrere Funktionen der Seite blockiert.

Dies ist Teil 5 der Reihe „Server im Netz“. Auf dem VPS mit der Domain sedv-lab.de läuft bereits Caddy mit HTTPS und Sicherheits-Headern. Als Beispiel dient ein netcup-VPS mit Ubuntu und Docker.

Voraussetzungen

  • Der Server aus Teil 4 mit Caddy, der einen Ordner als Website ausliefert
  • Docker, git und rsync auf dem Server
  • Grundkenntnisse in Markdown

Schritt 1: Ein Hugo-Projekt anlegen

Das offizielle Hugo-Image läuft als Benutzer mit der ID 1000. Der Projektordner muss diesem Benutzer gehören, sonst kann Hugo nicht schreiben:

mkdir -p /opt/web && chown 1000:1000 /opt/web && cd /opt/web
docker run --rm -u 1000:1000 -v /opt/web:/project -w /project ghcr.io/gohugoio/hugo:v0.169.0 new site lab --format toml

Hugo meldet Congratulations! Your new Hugo project was created und legt den Ordner lab mit der Grundstruktur an. Damit Sie den langen Aufruf nicht jedes Mal tippen, lohnt ein Alias:

alias hugo='docker run --rm -u 1000:1000 -v /opt/web/lab:/project -w /project ghcr.io/gohugoio/hugo:v0.169.0'

Verifizieren: hugo version beginnt mit hugo v0.169.0+extended.

Schritt 2: Das Theme holen

PaperMod ist eines der meistgenutzten Hugo-Themes, schlicht, schnell und mit Hell-Dunkel-Schalter. Naheliegend wäre das letzte Release:

git clone --depth 1 --branch v8.0 https://github.com/adityatelange/hugo-PaperMod.git lab/themes/PaperMod

Im Test brach der Bau damit ab:

ERROR error building site: render: ... execute of template failed: ...
error calling partial: partial "partials/templates/_funcs/get-page-images" not found

Das Release v8.0 stammt von November 2024, Hugo hat sein Vorlagensystem seitdem umgebaut, und die Ablage der Teilvorlagen hat sich geändert. Das Repository selbst wird weiter gepflegt, der letzte Stand war im Test vom 25.10.2026 und baute fehlerfrei. Nehmen Sie deshalb den aktuellen Stand und notieren Sie den Commit:

rm -rf lab/themes/PaperMod
git clone https://github.com/adityatelange/hugo-PaperMod.git lab/themes/PaperMod
git -C lab/themes/PaperMod log -1 --format="%h %cd"
chown -R 1000:1000 lab

Wer den Bau genau wiederholbar haben will, wechselt später mit git -C lab/themes/PaperMod checkout <commit> auf genau diesen Stand.

Verifizieren: git log zeigt Kurz-Hash und Datum des neuesten Commits, im Test 1181196 vom 25.10.2026.

Schritt 3: Konfiguration und erste Inhalte

Ersetzen Sie lab/hugo.toml durch diese Grundkonfiguration:

baseURL = "https://sedv-lab.de/"
locale = "de-DE"
defaultContentLanguage = "de"
title = "sedv-lab.de"
theme = "PaperMod"
enableRobotsTXT = true

[params]
  description = "Testserver der Anleitungsreihe Server im Netz"
  defaultTheme = "auto"
  ShowReadingTime = true
  ShowCodeCopyButtons = true

[params.homeInfoParams]
  Title = "Server im Netz"
  Content = "Ein Server, eine Domain, echte Messwerte aus dem Internet."

[[menu.main]]
  name = "Beiträge"
  url = "/posts/"
  weight = 10
[[menu.main]]
  name = "Über"
  url = "/ueber/"
  weight = 20

[markup.highlight]
  noClasses = false

Zwei Zeilen verdienen einen Blick. locale ersetzt das ältere languageCode, für das Hugo 0.169 eine Warnung ausgibt: project config key languageCode was deprecated in Hugo v0.158.0. Und noClasses = false lässt die Code-Hervorhebung mit CSS-Klassen statt mit Inline-Stilen arbeiten, warum das wichtig ist, zeigt Schritt 5. PaperMod bringt die passenden Klassen mit, bei anderen Themes erzeugen Sie sie mit hugo gen chromastyles.

Ein erster Beitrag als lab/content/posts/willkommen.md:

---
title: "Willkommen auf dem Testserver"
date: 2026-11-04
tags: ["Server im Netz"]
---
Diese Seite ist mit Hugo gebaut und wird von Caddy ausgeliefert. Sie besteht nur aus
fertigen HTML-, CSS- und Bilddateien.

```bash
hugo --minify
```

Für den Menüpunkt „Über“ legen Sie nach demselben Muster lab/content/ueber.md mit einem kurzen Text an.

Verifizieren: hugo --minify endet ohne ERROR. Im Test baute Hugo 15 Seiten in 102 Millisekunden, der Ordner public hatte rund 212 KiB in 20 Dateien.

Schritt 4: Ausrollen

Caddy liefert in Teil 4 den Ordner /opt/caddy/site aus. Dort liegt auch die security.txt, die beim Ausrollen nicht verschwinden darf:

rsync -a --delete --exclude ".well-known/" lab/public/ /opt/caddy/site/

--delete entfernt Dateien, die es in der neuen Fassung nicht mehr gibt, --exclude schützt den Ordner .well-known. Caddy muss dafür nicht neu starten. Wichtig: Bauen und hashen Sie in Schritt 5 immer genau den Ordner, den Sie hier ausrollen.

Verifizieren: curl -s -o /dev/null -w "%{http_code}\n" https://sedv-lab.de/ liefert 200.

Beitragsseite der Hugo-Website mit PaperMod, Hell-Dunkel-Schalter und Codeblock
Die fertige Beitragsseite mit PaperMod: Hell-Dunkel-Schalter (1) und Codeblock mit Hervorhebung (2), beide funktionieren erst nach der Anpassung der CSP.

Schritt 5: Die Content-Security-Policy anpassen

Die Seite sah gut aus, aber die Browser-Konsole war voller Fehler. Die CSP aus Teil 4 erlaubt Skripte nur als Dateien vom eigenen Server. PaperMod setzt aber kleine Skripte direkt in die Seite, etwa für den Hell-Dunkel-Schalter, das Menü und den Knopf nach oben. Der Browser blockierte alle vier:

Executing inline script violates the following Content Security Policy directive
'default-src 'self''. Either the 'unsafe-inline' keyword, a hash ('sha256-BpEdBCM/...'),
or a nonce ('nonce-...') is required to enable inline execution.

Ohne die Zeile noClasses = false aus Schritt 3 kämen auf der Beitragsseite zwei Meldungen Applying inline style violates ... dazu. Im Test hatten wir die Zeile zuerst vergessen: Die Code-Hervorhebung schreibt dann Inline-Stile, und die CSP sperrt sie.

Für die Skripte gibt es drei Wege. 'unsafe-inline' würde jedes eingeschleuste Skript erlauben und die CSP fast wertlos machen. Nonces müssen bei jeder Antwort neu und zufällig sein, das passt nicht zu fertigen Dateien. Bleiben Hashes: Die CSP erlaubt genau die Skripte, deren SHA-256-Prüfsumme sie kennt. Dieses kleine Skript sammelt die Hashes aller Inline-Skripte der gebauten Seite:

#!/usr/bin/env python3
# Sammelt die SHA-256-Hashes aller Inline-Skripte unter einem Ordner (Hugo: public/).
# read_text liest mit Zeilenende LF, wie der Browser beim Hashen. Nicht auf read_bytes umbauen.
import base64, hashlib, pathlib, re, sys
ordner = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else "public")
muster = re.compile(r"<script(?![^>]*(?<![-\w])src\s*=)(?![^>]*application/ld\+json)[^>]*>(.*?)</script>", re.S | re.I)
hashes = set()
for datei in ordner.rglob("*.html"):
    for inhalt in muster.findall(datei.read_text(encoding="utf-8")):
        if inhalt.strip():
            hashes.add("'sha256-" + base64.b64encode(hashlib.sha256(inhalt.encode()).digest()).decode() + "'")
print(" ".join(sorted(hashes)))

Strukturierte Daten vom Typ application/ld+json führt der Browser nicht aus, sie brauchen keinen Hash. Gespeichert als csp-hashes.py und aufgerufen mit python3 csp-hashes.py lab/public, fand das Skript im Test fünf Hashes: die vier aus der Konsole und einen für den Kopierknopf an Codeblöcken, der nur auf Beitragsseiten vorkommt. Im Caddyfile kommt hinter default-src 'self'; eine eigene Angabe für Skripte:

Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-...' 'sha256-...'; img-src 'self' data:; ..."

Setzen Sie die vollständige Ausgabe des Skripts an die Stelle der Platzhalter und laden Sie Caddy mit docker exec caddy caddy reload --config /etc/caddy/Caddyfile neu. Wichtig für später: Ändert ein Theme-Update ein Inline-Skript, ändert sich sein Hash. Auch --minify schreibt Inline-Skripte um, bauen Sie deshalb immer mit denselben Optionen. Lassen Sie das Skript nach jedem Bau laufen und vergleichen Sie die Ausgabe. In einem späteren Teil der Reihe automatisieren wir das. Ereignis-Attribute wie onclick deckt ein Hash übrigens nicht ab, PaperMod nutzt im Test keine.

Verifizieren: In einem frischen Browserfenster zeigt die Konsole auf Start- und Beitragsseite keine Meldung mehr, und der Hell-Dunkel-Schalter wechselt das Farbschema. Mozilla Observatory vergab nach der Änderung wie in Teil 4 die Note A+ mit 125 Punkten.

Schritt 6: Mit Lighthouse messen

Lighthouse prüft Ladezeit, Barrierefreiheit, gute Praxis und Suchmaschinen-Tauglichkeit, in Version 13 zusätzlich die Eignung für KI-Agenten unter „Agentic Browsing“. Die bequeme Variante ist PageSpeed Insights von Google. Im Test lieferte die Programmierschnittstelle ohne eigenen Schlüssel aber nur Quota exceeded for quota metric 'Queries' mit einem Limit von 0, und auch die Weboberfläche konnte die Seite an diesem Tag nicht prüfen. Lighthouse lässt sich genauso gut selbst ausführen, etwa in einem Wegwerf-Container:

docker run --rm -v "$PWD":/out node:22-bookworm-slim bash -c \
  "apt-get update -qq && apt-get install -y -qq chromium && \
   npx -y lighthouse@13.6.0 https://sedv-lab.de/ --chrome-flags='--headless=new --no-sandbox' \
   --output=json --output=html --output-path=/out/bericht-mobil --quiet"

Lighthouse legt dann bericht-mobil.report.html und bericht-mobil.report.json ab. Für die Desktop-Messung hängen Sie --preset=desktop an. Die erste Messung ergab 100 Punkte für Leistung, Barrierefreiheit und SEO, aber nur 96 bei den Best Practices. Die fehlenden Punkte kamen von Konsolenfehlern, weil favicon.ico, favicon-16x16.png und favicon-32x32.png fehlten. Zusätzlich wies der Leistungsteil darauf hin, dass die CSS-Datei ohne Angabe kam, wie lange der Browser sie speichern darf.

Die Favicons legen Sie als Dateien in lab/static/ ab: favicon.ico, favicon-16x16.png, favicon-32x32.png und apple-touch-icon.png mit 180 Pixeln. Erzeugen können Sie sie mit jedem Bildprogramm oder einem Favicon-Generator, Hugo kopiert sie unverändert in die Seite. Für den Zwischenspeicher bekommt der Block sedv-lab.de im Caddyfile zwei Zeilen:

	@assets path /assets/*
	header @assets Cache-Control "public, max-age=31536000, immutable"

Das ist unbedenklich, weil PaperMod an seine Dateien unter /assets/ einen Inhalts-Hash anhängt. Ändert sich die Datei, ändert sich ihr Name, und der Browser lädt sie neu. Das gilt nur, solange Sie disableFingerprinting in den Theme-Einstellungen nicht setzen und keine eigenen Dateien nach static/assets/ legen.

Lighthouse-Bericht mit 100 Punkten in Leistung, Barrierefreiheit, Best Practices und SEO
Lighthouse 13.6.0 nach Favicons und Cache-Angabe, Messung für Mobilgeräte: 100 Punkte in den vier klassischen Bereichen (1), Agentic Browsing 2/2, erster Inhalt nach 0,8 Sekunden bei simulierter Mobilverbindung.

Nach erneutem Bauen und Ausrollen zeigte Lighthouse auf dem Handy und am Desktop in allen vier klassischen Bereichen 100 Punkte. Die Startseite wog 12 KiB und kam mit drei Anfragen aus. Bei simulierter Mobilverbindung erschien der erste Inhalt nach 0,8 Sekunden, am Desktop nach 0,2 Sekunden. Offen blieb auf dem Handy nur der Hinweis auf das Stylesheet, das das Rendern kurz aufhält. Er senkte die Punktzahl nicht.

Verifizieren: Im Bericht sind die vier Kreise grün, und unter Best Practices steht kein Hinweis auf Konsolenfehler mehr.

Typische Fehler

MeldungUrsache und Lösung
partial "partials/templates/_funcs/get-page-images" not foundAltes PaperMod-Release v8.0 mit neuem Hugo. Aktuellen Stand des Themes nehmen, siehe Schritt 2.
project config key languageCode was deprecated in Hugo v0.158.0Nur eine Warnung. languageCode durch locale ersetzen.
Executing inline script violates the following Content Security Policy directiveCSP ohne Hashes für die Inline-Skripte des Themes. Hashes wie in Schritt 5 ergänzen, nach jedem Theme-Update neu berechnen.
Applying inline style violates ...Code-Hervorhebung mit Inline-Stilen. noClasses = false unter [markup.highlight] setzen.
Quota exceeded for quota metric 'Queries'Ohne eigenen Schlüssel stellte die PageSpeed-Schnittstelle im Test kein Kontingent bereit, das Limit stand auf 0. Eigenen API-Schlüssel anlegen oder Lighthouse selbst ausführen.
Hugo kann nicht in den Projektordner schreibenDer Ordner gehört nicht dem Benutzer 1000 aus dem Image. chown -R 1000:1000 auf den Projektordner.

Häufige Fragen

Warum Hugo und kein Baukasten oder WordPress?

Eine statische Seite hat keine Datenbank, kein PHP und keine Anmeldung. Damit fallen häufige Angriffswege und die Updates von CMS und Plugins weg. Server, Caddy und Theme pflegen Sie weiter. Dafür gibt es keine Kommentarfunktion und keinen Editor im Browser, Sie schreiben in Markdown. Für Seiten, die sich selten ändern, ist das ein guter Tausch.

Muss ich Hugo auf dem Server bauen?

Nein. Sie können genauso am eigenen Rechner bauen und nur den Ordner public auf den Server kopieren. In einem späteren Teil der Reihe bauen wir automatisch nach jedem git push.

Was brauche ich rechtlich für eine öffentliche Website?

Geschäftsmäßige Seiten brauchen in Deutschland ein Impressum, auch manche private Seiten mit redaktionellen Inhalten. Eine Datenschutzerklärung ist nötig, sobald personenbezogene Daten anfallen, und dazu zählen schon die IP-Adressen im Server-Log. Beides legen Sie als normale Seiten in content/ an und verlinken sie im Menü oder in der Fußzeile. Das ist keine Rechtsberatung, lassen Sie den Inhalt im Zweifel prüfen.

Wie aktualisiere ich das Theme?

Mit git -C lab/themes/PaperMod pull, danach neu bauen und die Hashes aus Schritt 5 neu berechnen. Weichen sie ab, tragen Sie die neuen in die CSP ein.

Fazit

Hugo macht aus ein paar Markdown-Dateien in Sekundenbruchteilen eine schnelle, sichere Website, und mit dem Docker-Image ist nichts zu installieren. Die Arbeit steckt in zwei Details, die viele Anleitungen übergehen. Das letzte Theme-Release passte nicht mehr zum aktuellen Hugo, der aktuelle Stand aus dem Repository schon. Und eine strenge CSP verträgt sich nur dann mit einem Theme, wenn Sie dessen Inline-Skripte über Hashes freigeben und die Code-Hervorhebung auf Klassen umstellen. Danach stehen 12 KiB, drei Anfragen und 100 Punkte in allen vier klassischen Bereichen von Lighthouse. Im nächsten Teil schützen wir die Seite vor KI-Crawlern.

Weiterführende Anleitungen und Quellen

Server im NetzHugoPaperModContent-Security-PolicyLighthouseCaddyDocker