Hosting a static site in a container

created: Mittwoch, Dez. 10, 2025

Die meisten unserer gehosteten Seiten sind statisch gerendert und werden mit Tools wie Hugo, Zola oder Jekyll erstellt. Im Allgemeinen nehmen all diese Site-Renderer vereinfachte Eingaben (meist Markdown) und erzeugen als Ausgabe wohldefiniertes HTML. So stellt sich die Frage: Wie kann ich eine solche Seite hosten?

Es gibt spezialisierte Anbieter, die Hosting für statische Seiten anbieten, aber wie Sie wissen, haben wir beim Aufbau unserer Kerninfrastruktur einen anderen Weg gewählt. Für unsere Cloud ist der gemeinsame Nenner für die Bereitstellung ein Container. Und damit stellt sich die Frage: Wie komme ich von einem statischen Site-Build zu einem Container, der die Webseite hostet?

Beginnen wir also von vorne, indem wir mit Hugo eine sehr einfache Hello-World-Seite generieren.

hugo new site hello
cd hello
git init
git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke.git themes/ananke
echo "theme = 'ananke'" >> hugo.toml

Um den Build zu überprüfen, können wir den Server lokal starten:

hugo serve

Dann erscheint unsere ganz eigene „Hello World“-Seite unter http://localhost:1313

Bisher so gut. Nun überlegen wir, dies in einen Container zu packen.

Two Approaches to Containerization

Technisch gesehen können wir hier zwei verschiedene Wege gehen:

  1. Den gesamten Quellcode in einen Container packen und den Build-Prozess darin ausführen
  2. Lokal bauen und nur die Ausgaben in den Container kopieren

Um unser Beispiel reproduzierbarer und weniger abhängig von lokalen Umgebungen zu machen, wählen wir in diesem Szenario Option 1. Das macht auch CI/CD-Pipelines sauberer, da die Build-Umgebung vollständig im Dockerfile definiert ist.

Building the Container Image

Ein Container-Image beginnt immer mit einem Basis-Image. Dafür verwenden wir Alpine Linux, da es klein ist (ungefähr 5MB) und genügend Werkzeuge für unser Projekt mitbringt.

Wir verwenden einen Multi-Stage-Build, eine Technik, die von modernen Container-Build-Tools unterstützt wird und es ermöglicht, ein Image zum Bauen und ein anderes zum Ausführen zu verwenden. Das hält unser finales Image klein, indem Bauteile weggelassen werden, die zur Laufzeit nicht benötigt werden.

Stage 1: Build

FROM alpine AS build
RUN apk add --no-cache hugo
WORKDIR /src
ADD . .
RUN hugo --minify

In diesem Schritt:

Die erstellte Seite landet in /src/public/.

Stage 2: Runtime

FROM alpine AS runner
RUN apk add --no-cache lighttpd
COPY --from=build /src/public /var/www/localhost/htdocs
EXPOSE 80
CMD ["lighttpd", "-D", "-f", "/etc/lighttpd/lighttpd.conf"]

Hier:

The Complete Dockerfile

Hier das vollständige Dockerfile, das beide Phasen kombiniert:

# Stage 1: Build the static site
FROM alpine AS build
RUN apk add --no-cache hugo
WORKDIR /src
ADD . .
RUN hugo --minify

# Stage 2: Serve with lighttpd
FROM alpine AS runner
RUN apk add --no-cache lighttpd
COPY --from=build /src/public /var/www/localhost/htdocs
EXPOSE 80
CMD ["lighttpd", "-D", "-f", "/etc/lighttpd/lighttpd.conf"]

Speichern Sie dies als Dockerfile im Stammverzeichnis Ihres Hugo-Projekts.

Building and Running Locally

Sie können jedes OCI-kompatible Container-Tool wie Podman oder Docker verwenden. Die Beispiele unten nutzen docker, aber podman funktioniert als Ersatz.

Um das Image zu bauen:

docker build -t my-website .

Zum lokalen Ausführen:

docker run -p 8080:80 --rm my-website

Ihre Seite ist nun unter http://localhost:8080 erreichbar.

Das -p 8080:80-Flag mappt Port 8080 auf Ihrem Rechner auf Port 80 im Container. Das --rm-Flag entfernt den Container automatisch, wenn er gestoppt wird.

Why This Approach Works Well

Diese Konfiguration hat mehrere Vorteile:

  1. Kleine Image-Größe: Das finale Image enthält nur Alpine (~5MB) + lighttpd (~1MB) + Ihre HTML-Dateien. Kein Node.js, kein Ruby, keine Build-Tools, die Ihr Produktionsimage aufblähen.

  2. Reproduzierbare Builds: Die exakt gleiche Hugo-Version wird in der CI wie lokal ausgeführt, was „funktioniert bei mir“-Probleme eliminiert.

  3. Schneller Start: lighttpd startet in Millisekunden, perfekt für scale-to-zero-Deployments auf DTZ.

  4. Sicherheit: Der Produktionscontainer hat eine minimale Angriffsfläche – nur ein statischer Dateiserver ohne dynamische Laufzeit.

Architecture Considerations

Wenn Sie Ihr Image auf einem Apple Silicon Mac (ARM64) oder einer anderen nicht standardmäßigen Architektur bauen, bedenken Sie, dass Server normalerweise auf AMD64 (x86_64) laufen. Um sicherzustellen, dass Ihr Container auf DTZ (und den meisten anderen Cloud-Anbietern) korrekt läuft, sollten Sie während des Baus explizit die Zielplattform angeben.

Ändern Sie Ihren Build-Befehl zu:

docker build --platform linux/amd64 -t my-website .

Dies weist Docker an, das Image für Standard-Linux-Server cross-zu-kompilieren, was die Kompatibilität unabhängig von Ihrer Bau-Maschine sicherstellt.

Deploying to DownToZero

Nachdem Ihr Image gebaut ist, können Sie es in ein Container-Registry pushen und auf DTZ bereitstellen. Wenn Sie unser Container-Registry nutzen:

# Tag für DTZ-Registry
docker tag my-website YOUR_CONTEXT_ID.cr.dtz.dev/my-website:latest

# Anmelden & pushen
docker login YOUR_CONTEXT_ID.cr.dtz.dev -u apikey
docker push YOUR_CONTEXT_ID.cr.dtz.dev/my-website:latest

Erstellen Sie dann einen Container-Service im DTZ-Dashboard, der auf Ihr Image zeigt. Der Service kümmert sich automatisch um TLS-Zertifikate, Skalierung und Routing.

Für automatisierte Deployments bei jedem Commit schauen Sie sich unsere GitHub Action für nahtlose Deployments an.

Adapting for Other Static Site Generators

Das gleiche Muster funktioniert auch für andere Generatoren. Hier die wichtigsten Änderungen:

Für Zola:

FROM alpine AS build
RUN apk add --no-cache zola
WORKDIR /src
ADD . .
RUN zola build

Für Jekyll:

FROM ruby:alpine AS build
RUN apk add --no-cache build-base
RUN gem install bundler jekyll
WORKDIR /src
ADD . .
RUN bundle install
RUN bundle exec jekyll build

Die Laufzeitphase bleibt gleich – einfach von /src/public (Zola) oder /src/_site (Jekyll) in lighttpds Dokumenten-Root kopieren.

Wrapping Up

Das Containerisieren statischer Seiten ist einfach, sobald man das Muster versteht: in einer Phase bauen, in einer anderen ausliefern. Das Ergebnis ist ein winziger, schneller und sicherer Container, perfekt für moderne Cloud-Deployments.

Dieser Ansatz passt gut zu unserer Philosophie bei DTZ – minimale Ressourcen, schnelle Kaltstarts und Infrastruktur, die auf null skaliert, wenn sie nicht benutzt wird. Eine statische Seite in einem ~10MB Container, der sofort startet, ist so effizient, wie Webhosting nur sein kann.