Als Folgeartikel von Webserver mit SSL: vServer + Ubuntu + NGINX + Let’s Encrypt beschreibe ich hier, wie sich der Nginx Webserver mit Let’s Encrypt SSL-Zertifikaten und anonymisierten Log-Files in einen Docker Container verbannen lässt, was im Wesentlichen deutlich mehr Komfort und zudem etwas mehr Sicherheit bedeutet.

Der Komfort besteht darin, dass alle Abhängigkeiten im Docker Container vorhanden sind und das umgebende System (Host) nicht speziell dahingehend eingerichtet werden muss. Lediglich eine Docker-Installation ist notwendig. Etwas mehr Sicherheit ist durch die Kapselung und einem möglichst minimal ausgestatteten Docker-Image gegeben.

Ich nutze hier der Einfachheit halber als Basis ein Ubuntu Docker-Image, da Ubuntu auch im ursprünglichen Artikel das zugrunde liegende System war. Sicherlich ist es möglich ein kleineres Image zu wählen oder zu versuchen direkt das Nginx Docker-Image zu verwenden.

Umsetzung

Vorab nun eine Übersicht der Dateien, die erstellt werden (- in nachfolgend besprochener Reihenfolge):

Datei- und Ordner-Struktur
1
2
3
4
5
├── app.sh
├── dockerfile
├── docker-compose.yml
├── webserver.conf
└── .env -> webserver.conf

Der Webserver als Applikation: app.sh

Das folgende app.sh bash Script wird später als Applikation im Docker-Container ausgeführt. Es konfiguriert Nginx für via Environment definierte Domains, fragt Let’s Encrypt SSL-Zertifikate für diese Domains an, erneuert sie alle 30 Tage und startet schließlich den Nginx Webserver im Nicht-Daemon-Modus. Die Website-Dateien erwartet der Webserver unterhalb von /var/www/site.

Datei: app.sh - Der Webserver als Applikation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#!/bin/bash

##############################################################################################
# App to be started in the container
# - Requires ${DOMAINS} to be set, which must contain one or more comma separated domains
#   (without whitespace), e.g. DOMAINS=drstefankuhn.de,www.drstefankuhn.de,smrtptr.com
# - Retrieves SSL certificates for all ${DOMAINS} using certbot (https://letsencrypt.org/) and
#   renews these every 30 days
# - Configures nginx web server for all ${DOMAINS} that also anynomizes IPs in the logs; and
#   starts the nginx web server in non-daemon mode.
##############################################################################################

# Immediately exit script on error and treat unset variables as error

set -e -u
echo "Starting web server for ${DOMAINS} with SSL certificates and anonymized logs"


###### SSL Certificates ######################################################################

# Create and start script '/certbot.sh' in background for initializing and renewing the SSL
# certificates for all ${DOMAINS} after nginx web server has started.

CERTBOT_DOMAINS=$(echo ${DOMAINS} | awk -v RS=, -v ORS=" " '{print "-d " $0}' | sed 's/ $//')

cat >/certbot.sh <<EOL
#!/bin/bash
until pidof nginx > /dev/null; do sleep 1; done
sleep 5
certbot -n ${CERTBOT_DOMAINS} --nginx --register-unsafely-without-email --agree-tos --redirect
while pidof nginx > /dev/null; do sleep 30d && /usr/bin/certbot renew --quiet; done
EOL

chmod 755 /certbot.sh
/certbot.sh &


###### Web Server ############################################################################

# Configure nginx web server to anonymize IPs by replacing the last octet by 0

cat >/etc/nginx/conf.d/anonymized_logging.conf <<EOL
map \$remote_addr \$remote_addr_anonymized {
    ~(?P<ip>\\d+\\.\\d+\\.\\d+)\\. \$ip.0;
    ~(?P<ip>[^:]+:[^:]+):      \$ip::;
    127.0.0.1                  \$remote_addr;
    ::1                        \$remote_addr;
    default                    0.0.0.0;
}
log_format anonymized '\$remote_addr_anonymized - \$remote_user [\$time_local] "" '
                      '"\$request" \$status \$body_bytes_sent '
                      '"\$http_referer" "\$http_user_agent" "\$http_x_forwarded_for"';
access_log off;
error_log off;
EOL

# Create nginx web server entry for all ${DOMAINS}

SERVER_DOMAINS=$(echo ${DOMAINS} | awk -v RS=, -v ORS=" " '{print " " $0}' | sed 's/ $//')

cat >/etc/nginx/sites-available/site <<EOL
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    root /var/www/site;
    server_name ${SERVER_DOMAINS};
    index index.html index.htm;
    location / {
        try_files \$uri \$uri/ =404;
    }
    access_log /var/log/nginx/access.log anonymized;
    error_log /var/log/nginx/error.log;
}
EOL

# Enable only the new nginx web server entry and start nginx in non-daemon mode

rm /etc/nginx/sites-enabled/*
ln -s /etc/nginx/sites-available/site /etc/nginx/sites-enabled/site
nginx -g 'daemon off;'

Dieses Script unterscheidet sich nicht allzu stark von dem im ursprünglichen Artikel. Die Struktur und die Art der Aufrufe haben sich etwas geändert und die Instruktionen zur Anonymisierung von IPs stecken jetzt auch schon drin.

Docker erwartet eine Applikation an deren Lebenszeit der Container gebunden ist. Daher wird Nginx am Ende des Scripts im Nicht-Daemon-Modus aufgerufen.

Das Anfragen und Erneuern der Zertifikate ist in ein im Hintergrund laufendes Script ausgelagert, welches sich hauptsächlich im sleep-Modus befindet und nur alle 30 Tage aufwacht um die Zertifikate zu erneuern. Diese Art der Umsetzung ist ebenfalls der Einfachheit geschuldet und widerspricht etwas dem vorherigen Abschnitt. Außerhalb von Docker würde man beispielsweise cron verwenden. Innerhalb von Docker böte sich evtl. das ofelia Docker-Image an.

Das angepasste Linux-System: dockerfile

Mit dem dockerfile lässt sich wie folgt ein angepasstes Linux-System Image erstellen. Es basiert auf dem Ubuntu Docker-Image, installiert den Nginx Webserver sowie den Client zur Anfrage/Erneuerung von SSL-Zertifikaten. Außerdem wird die Applikation app.sh als Script in das Image kopiert und als Entrypoint definiert. Desweiteren wird angegeben, dass im Container auf den Ports 80 (HTTP) und 443 (HTTPS) gelauscht wird.

Datei: dockerfile - Das angepasste Linux-System
1
2
3
4
5
6
7
8
9
10
11
12
13
14
FROM ubuntu:latest

RUN apt-get update && apt-get install -y \
    nginx-light \
    certbot \
    python3-certbot-nginx \
 && rm -rf /var/lib/apt/lists/*

COPY app.sh /app.sh
RUN chmod 755 /app.sh

EXPOSE 80 443

ENTRYPOINT /app.sh

Die Startkonfiguration: docker-compose.yml

Die docker-compose.yml-Datei beschreibt, wie der Container gestartet werden soll; Es kann daher als Startkonfiguration betrachtet werden. Environment-Variablen werden genutzt um individuelle DOMAINS, HTTP(S)_PORT und den DOCUMENT_ROOT festlegen zu können.

Datei: docker-compose.yml - Die Startkonfiguration
1
2
3
4
5
6
7
8
9
10
11
12
version: '3'

services:
  webserver:
    environment:
      - DOMAINS=${DOMAINS}
    build: .
    ports:
      - "${HTTP_PORT}:80"
      - "${HTTPS_PORT}:443"
    volumes:
      - ${DOCUMENT_ROOT}:/var/www/site/

Die individuellen Startparameter: webserver.conf

Die individuellen Startparameter lassen sich in Form von Environment-Variablen komfortabel in einer Konfigurationsdatei ablegen, hier webserver.conf.

Datei: webserver.conf - Die individuellen Startparameter
1
2
3
4
5
6
7
8
9
10
11
12
# Comma separated domains (without whitespace).
# The first domain is the main one.
DOMAINS=some_example_domain.de,www.some_example_domain.de

# Document root on host
DOCUMENT_ROOT=/var/www/default_site

# Http port on host
HTTP_PORT=80

# Https port on host
HTTPS_PORT=443

Die standardmäßig verwendeten Startparameter: .env -> webserver.conf

Damit die zuvor definierten Parameter automatisch beim Starten des Docker-Containers verwendet werden, legt man einen symbolischen Link von .env auf webserver.conf an: ln -s webserver.conf .env.

Verwendung

Der Webserver wird im Docker Container wie folgt gestartet und gestoppt. Die Website-Dateien werden auf dem Host abgelegt und können daher auch außerhalb des Containers bearbeitet werden.

Starten des Webservers im Docker Container

docker compose up -d

  • Baut ggf. das Webserver Image
  • Startet den Container mit Berücksichtigung von .env
  • Startet den Nginx Webserver im Docker Container
  • Fragt dort Let’s Encrypt SSL-Zertifikate an und erneuert diese alle 30 Tage
  • Liefert Webseiten aus dem spezifizierten DOCUMENT_ROOT an allen angegebenen Domains aus (- entsprechende DNS-Konfiguration vorausgesetzt)
  • Anonymisiert die IPs in den Log-Dateien

Stoppen des Docker Containers mit Webserver

docker compose down

  • Stoppt den Container mit dem Nginx Webserver

Repository

Der Code ist ebenfalls unter github.com/drqhn/dockerized_webserver zu finden. Tag v1.0.0 entspricht dem hier beschriebenen Stand. Eine Weiterentwicklung erfolgt nach persönlichem Bedarf.