Contents

Statische Website auf VM hosten – mit Docker & Caddy

In diesem Beitrag dokumentiere ich die wichtigsten Schritte, mit denen ich diese statische Website auf einem Hetzner-Server (Bitte verwenden Sie meinen Empfehlungslink :-)) bereitgestellt habe.

Anforderungen

Du sollst:

Zusammenfassung

  1. Hugo (oder Jekyll) generiert die Seiten und kopiert sie auf meinem Server.
  2. Caddy dient die statischen Dateien aus.
  3. Docker sorgt für sauberes Deploy & Auto-HTTPS.

Ein Wort zu Caddy: Vorteile & Nutzung

Caddy ist eine leistungstärke und erweiterbare platform für Webseiten, Dienste und Apps. Caddy ist in Go programmiert und einige seiner Vorteile sind:

  • Auto-HTTPS out of the box: Zertifikatsverwaltung ohne Cronjobs oder externe Hooks.
  • Einfaches, lesbares Konfig-Format: Das Caddyfile ist kurz und gut verständlich.
  • Gute Defaults: Redirects, Kompression, Security-Header sind schnell gesetzt.
  • Performant für statische Assets: Ideal für Jekyll-Seiten, Blogs, Docs, Portfolios.
  • Erweiterbar: Wenn später eine dynamische Site (z. B. WordPress) dazukommt, kann Caddy per php_fastcgi/Reverse-Proxy erweitert werden – ohne die statische Site anzufassen.

Architektur-Überblick

[Meine Lokale Maschine/CI]
       Hugo-Build
           │  (rsync/scp)
┌──────────────────────────────────────────┐
│            Hetzner-VM (Linux)            │
│  - Firewall: 80/tcp, 443/tcp erlaubt     │
│  - Docker Engine                         │
│  - Dateipfade unter /srv/www             │
│                                          │
│   docker-compose.yml                     │
│        └─ caddy (Container)              │
│             - Caddyfile                  │
│             - /srv/static_site (ro)      │
│             - Auto-HTTPS (Let's Encrypt) │
└──────────────────────────────────────────┘
        ▲                        ▲
        │                        │
   DNS A/AAAA -> Server-IP    Browser-User

Warum habe ich diese Architektur benutzt?

  • Klar getrennte Rollen

    • Hugo: erzeugt statische HTML/CSS/JS (Build lokal oder in CI).
    • Caddy (im Docker-Container): liefert Dateien aus, holt automatisch Let’s-Encrypt-Zertifikate und
  • Wartungsarm & portabel – durch Docker reproduzierbar; Updates sind ein docker compose pull && up -d.


1) Statische Seite mit Hugo bauen

Sie können eine gute Dokumentation auf der Webseite von Hugo finden. Wie schon gesagt, können Sie auch Jekyll statt Hugo für die Generierung der statischen Webseite verwenden. Ich werde auch wahrscheinlich einen Post über Jekyll schreiben.

Nach der Generierung der Seiten deploye ich den Inhalt von public/ danach auf meinem Server (siehe Schritt 5).


2) Verzeichnisstruktur auf dem Server

sudo mkdir -p /srv/www/incompletenotes/public
sudo mkdir -p /srv/www/caddy
  • public/ enthält die fertigen statischen Dateien (aus dem vorherigen Schritt).
  • In caddy/ liegt die Caddy-Konfiguration Datei Caddyfile.

3) docker-compose.yml für Caddy

version: "3.9"

services:
  caddy:
    image: caddy:2
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /srv/www/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
      - /srv/www/incompletenotes/public:/srv/static/site:ro
      - caddy_data:/data
      - caddy_config:/config

volumes:
  caddy_data:
  caddy_config:

caddy_data speichert u. a. Zertifikate persistent.


4) Caddyfile (Konfiguration)

www.incompletenotes.com {
    redir https://incompletenotes.com{uri} permanent
}

# Statische Seite
incompletenotes.com {
    root * /srv/static/site
    encode zstd gzip
    file_server

    # Cache-Header für Assets
    @assets path_regexp assets \.(?:css|js|png|jpe?g|gif|svg|ico|webp|woff2?)$
    header @assets Cache-Control "public, max-age=31536000, immutable"

    # Basisschutz-Header
    header {
        X-Content-Type-Options "nosniff"
        Referrer-Policy "strict-origin-when-cross-origin"
        Permissions-Policy "geolocation=(), microphone=()"
    }
}

Was Caddy hier automatisch übernimmt:

  • Let’s-Encrypt-Zertifikate (sehr hilfreiche Option).
  • HTTP→HTTPS-Redirects. (finde ich auch cool)
  • Kompression (gzip/zstd) und effizientes Ausliefern statischer Dateien.

5) Deployment des Hugo-Builds

#Building Hugo site
hugo --minify

#Transferring files to server
rsync -avz --delete public/ my_user@server_ip:/srv/www/incompletenotes.com/public/

--delete sorgt dafür, dass entfernte Dateien auf dem Server entfernt werden – ideal für „saubere“ Deploys.


6) DNS & Firewall (Teilweise Optional)

  • DNS: Ich setze A (IPv4) und ggf. AAAA (IPv6) für incompletenotes.com (und www.incompletenotes.com) auf die IP(s) meiner VM.

  • Hetzner-Cloud-Firewall (Inbound erlauben): 80/tcp, 443/tcp.

  • Host-Firewall (UFW):

    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp

7) Start & Test

# auf dem Server
docker compose -f /srv/www/docker-compose.yml up -d
docker logs -f caddy            # prüft Zertifikatsabruf/Start
curl -I https://incompletenotes.com

Wenn alles passt, solltest du HTTP/2 200 (oder HTTP/3) sehen.


Fazit

Mit Hugo -> Docker/Caddy -> VM ist eine statische Webseite in wenigen Schritten sauber live: einfaches Deployment, automatisches HTTPS, gute Performance und wenig Moving Parts. Wenn später eine dynamische Seite hinzukommt, lässt sich die Architektur problemlos erweitern – Caddy bleibt der Front-Door-Server, statische und dynamische Routen koexistieren sauber.