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):
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
.
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.
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.
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
.
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.