Statische Webseite & Webserver im Docker-Image

Veröffentlicht am 10. September 2023 • 6 Min. Lesezeit • 1.260 Wörter

Wie packt man eine Hugo Webseite zusammen mit einem HTTP-Webserver in ein Docker Image?

Statische Webseite & Webserver im Docker-Image
Foto von frank mckenna  auf Unsplash 

Obwohl dieser Beitrag im Kontext des vorherigen “Hugo Webseite erstellen ohne Installation” entstanden ist, lässt sich das Prinzip auf jedes Projekt aus statischen HTML-Seiten anwenden, von Under Construction- über Coming soon-Landing Seiten bis eben hin zu ganzen (Hugo)-Blog-Projekten.

Und wie bekommt man die statischen Websiten nun serviert? 🧐

Am einfachsten geht das, indem man die Webseite zusammen mit einem Webserver in ein Docker-Image steckt; aber bitte automatisiert!

Da die Webseiten bereits vorliegen, werden wir lediglich ein Skript und ein paar config-Files erstellen, um anschließend einen Docker-Image zu erstellen, das neben den eigentlichen Seiten auch einen Webserver für die Auslieferung enthält.

Das Erzeugen des Images dauert jeweils nur ein paar Minuten und läuft völlig automatisiert ab.

Das fehlende sichere Protokoll (https) lässt sich jedoch einfach in Form eines Reverse Proxys nachrüsten. In einem späteren Artikel zeige ich wie man Traefik Proxy und Let’s Encrypt als Mittler verwenden kann, um einen einfachen http-Endpunkt, wie unseren Webserver-Container, für die Verwendung von https fit zu machen.

Verwendete Technologien

Alle Tools sind komplett frei und OpenSource.

Ich nehme an ihr habt bereits Docker installiert. Wenn nicht gibt es da draußen gute Einführungen zum Thema Containerisierung und wie man damit am einfachsten anfängt (z.B. Docker Docs ).

Erstellen des Docker-Images

Im einzelnen benötigt man

  • ein Basis-Nginx Image, als Webserver
    • ein Nginx-Config-File, zur Konfiguration
  • das Verzeichnis in welchem die statische Webseite abgelegt ist; hier ./public

Das Docker-Image wird im Wesentlichen mit dem Kommando docker image build und Parametern erzeugt

  • -f zeigt dabei auf das Konfigurations-File (dockerfile) und
  • -t setzt ein optionales Docker-Tag eigener Wahl

Um das Image später bequem auf einen anderen Rechner verschieben zu können, exportieren wir es gleich via docker save <Docker-Tag> als .tar.gz File.

Wer den Artikel “Hugo Webseite erstellen ohne Installation” gelesen hat ahnt schon, dass ich das Skript im Hugo-Projekt-Ordner unter ./tools speichern werde. Davon lässt sich natürlich jederzeit abweichen, solange man die wesentlichen Pfade anpasst.

#!/bin/bash
set -e
set -o pipefail

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

docker image build -f ${DIR}/dockerfile -t hugo-nginx .
docker save hugo-nginx | gzip > ${DIR}/hugo-nginx.tar.gz

Um das Docker Image bauen zu können, fehlt noch ein Bauplan für die Erstellung, der im sogenannten dockerfile beschrieben wird. Als Grundlage wird ein minimales Image referenziert (Nginx:alpine). Zur Konfiguration des Web-Servers wird das default-Konfigurations-File gegen ein eigenes ausgetauscht.

In das Image wird ebenfalls das final veröffentlichte Hugo-Projekt aus dem lokalen Ordner ./public kopiert, das dort im Web-Serverbereich von Nginx (/usr/share/nginx/html) zu liegen kommt.

# Minimales Nginx Image als Basis Image
FROM nginx:alpine

# Lösche das Nginx default Config-File
RUN rm /etc/nginx/conf.d/default.conf

# Kopiere das neue Nginx Config-File
COPY ./tools/nginx.conf /etc/nginx/nginx.conf

# kopiere das gebaute Hugo-Projekt
# in das Webverzeichnis von Nginx
COPY ./public /usr/share/nginx/html

Der Web-Server läuft zu Testzwecken auch leidlich ohne eigenes Konfigurations-File. Möchte man jedoch einen Schritt weiter gehen und das Image zusammen mit einer https Terminierung (wie z.B. Traefik Proxy und Let’s Encrypt) ins Internet stellen, so läuft man unmittelbar in Cross-Origin Probleme. Also machen wir es gleich richtig.

worker_processes auto;

events {
  worker_connections  1024;
}

http {
  include mime.types;
  map $http_origin $allow_origin {
    default "*";
    "~^https?://(frankschmidt-bruecken\.de|localhost:8080)$" "$http_origin";  # <<< ersetze Domain (kein www.)
  }

  map $request_method $cors_method {
    default "allowed";
    "OPTIONS" "preflight";
  }

  map $cors_method $cors_max_age {
    default "";
    "preflight" 3600;
  }

  map $cors_method $cors_allow_methods {
    default "";
    "preflight" "GET, POST, OPTIONS";
  }

  map $cors_method $cors_allow_headers {
    default "";
    "preflight" "Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since";
  }

  map $cors_method $cors_content_length {
    default $initial_content_length;
    "preflight" 0;
  }

  map $cors_method $cors_content_type {
    default $initial_content_type;
    "preflight" "text/plain charset=UTF-8";
  }

  server {
      gzip            on;
      gzip_vary       on;
      gzip_proxied    any;
      gzip_comp_level 6;
      gzip_types      text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;

      listen       80;
      #listen  [::]:80;
      server_name  frankschmidt-bruecken.com;                           # <<< ersetze Domain (kein www.)

      #access_log  /var/log/nginx/host.access.log  main;

      add_header Access-Control-Allow-Origin $allow_origin;
      add_header Access-Control-Allow-Credentials 'true';
      add_header Access-Control-Max-Age $cors_max_age;
      add_header Access-Control-Allow-Methods $cors_allow_methods;
      add_header Access-Control-Allow-Headers $cors_allow_headers;

      set $initial_content_length $sent_http_content_length;
      add_header 'Content-Length' "";
      add_header 'Content-Length' $cors_content_length;

      set $initial_content_type $sent_http_content_type;
      add_header Content-Type "";
      add_header Content-Type $cors_content_type;

      if ($request_method = 'OPTIONS') {
        return 204;
      }

      location / {
          add_header Access-Control-Allow-Origin https://www.frankschmidt-bruecken.com;   # <<< ersetze www.Domain
          add_header Cache-Control "public, max-age=3600";
          root   /usr/share/nginx/html;
          index  index.html index.htm;
      }

      error_page  404              /404.html;

      # redirect server error pages to the static page /50x.html
      #
      error_page   500 502 503 504  /50x.html;
      location = /50x.html {
          root   /usr/share/nginx/html;
      }
  }
}

Nachdem das Skript einmalig ausführbar gemacht wurde (chmod +x),

chmod +x ./tools/erstelle-docker-image.sh

lässt sich das finale Image nun erzeugen.

./tools/erstelle-docker-image.sh

Wenn alles richtig gelaufen ist, befindet sich im Ordner ./tools das erzeugte Docker-Image (/hugo-nginx.tar.gz) unseres Projektes.

Lokales Testen des Hugo-Nginx-Images

#!/bin/bash
set -e
set -o pipefail

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
IMAGE="hugo-nginx"

echo "Der Web Server steht unter http://localhost/ zur Verfügung"

docker run --rm \
    -p 80:80 \
    ${IMAGE}
chmod +x ./tools/test-image-lokal.sh

Da der Container jetzt auf Port 80 lauscht, startet man den lokalen Browser einfach mit http://localhost/.

Der Container kann wieder mit CTRL+C beendet werden.

Kopieren des Images auf das Zielsystem

Oft möchte man nach der lokalen Erstellung das finale Image auf einen anderen Server kopieren. Hier ein Beispiel wie das per ssh gehen kann. Ich habe gerne den Namen des Zielsystems bereits im Skriptnamen ersichtlich, um Missverständnisse zu vermeiden.

Innerhalb des Skriptes müssen noch die Variablen an die eigenen Bedürfnisse angepasst werden: Eine Beschreibung des Images, der Name des Image Files, die Adresse des Zielsystems, der Benutzername für den SSH-Transfer sowie der Ordner in dem das Image dort abgelegt werden soll.

Bei der Ausführung wird dann das Passwort erfragt (es sei denn man hat den lokalen ssh-Schlüssel auf das Zielsystem übertagen).

#!/bin/bash
set -e
set -o pipefail

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

IMAGEBESCHREIBUNG="MeinHugoBlog"
IMAGEFILE="${DIR}/hugo-nginx.tar.gz"
ZIELSYSTEM="meine-domain.de oder IP"
BENUTZERNAME="fritzchen"
ZIELORDNER="/srv/dockerimages/

echo ">>> Kopiere ${IMAGEBESCHREIBUNG} nach ${ZIELSYSTEM}"
scp ${DIR}/tools/${IMAGEFILE} ${BENUTZERNAME}@${ZIELSYSTEM}:${ZIELORDNER}
echo "\n>>>Fertig"
echo ">>> Das Image wurde auf ${ZIELSYSTEM} im Ordner ${ZIELORDNER} abgelegt."

Anschließend loggt man sich auf dem Zielsystem ein und startet einen Container des Images oder startet einen vorhandenen neu. Wie das zu tun ist hängt im Wesentlichen davon ab, welche Art von Reverse Proxy dort verwendet wird (siehe nächste Schritte).

Fazit

In wenigen Schritten lassen sich statische Webseiten zusammen mit einem Webserver in ein Docker Image packen und auf das Zielsystem kopieren.

Aus meiner Sicht ist es immer hilfreich zu Beginn eines Projektes ein wenig Zeit in die Erstellung von Helferlein in Form von Skripten zu investieren. Diese nehmen einem nicht nur immer wiederkehrende und oft stupide Arbeiten ab, sonder beugen vor allem Leichtsinnsfehler vor.

Nächster Schritt

Um nun das Webprojekt sicher online bringen zu können, müssen wir noch dafür sorgen, dass die Seiten sicher per HTTPS ausgeliefert werden können. Dazu gibt es mehrere Möglichkeiten, z.B.