Ein Backend mit Python, Flask und SSL

In diesem Beitrag beschreibe ich, wie wir in wenigen Schritten und zu geringen Kosten Informationen öffentlich abrufbar in Internet stellen können. Dazu starten wir zunächst mit statischen Webseiten, um die zu Grunde liegende Vorgehensweise zu erklären. In den nächsten Schritten wird die Bereistellung von dynamischen Information sowie die Umsetzung einer Backend-API beschrieben.

backend_static_page (Ordner)
├── docker-compose.prod.yml
├── Dockerfile-prod
├── requirements.txt
├── scripts (Ordner)
│   └── run_backend.py
└── static_pages (Ordner)
    └── index.html

Eine erste lokal erreichbare Webseite

Unser Backend soll die folgende Webseite einfachster Art als statische Antwort ausgeben. Wir erstellen dazu eine Datei im Ordner static_pages mit dem folgenden Inhalt.

<!DOCTYPE html>
<html>
    <head>
        <title>Text in der Titelzeile des Webbrowsers</title>
    </head>
    <body>
       Dieser Text erscheint im Webbrowser
    </body>
</html>

Es gibt viele unterschiedliche technische Möglichkeiten diese Webseite zu publizieren. In diesem Beispiel nutzen wir die Skriptsprache Python mit der Flask-Bibliothek, da diese auch für Einsteiger einfach zu verstehen und anzupassen sind. Die Datei run_backend.py beinhaltet den Quellcode der gebraucht wird, um den Webserver zu starten:

import os
from flask import Flask
current_dir = os.path.dirname(os.path.abspath(__file__))
project_dir = os.path.dirname(current_dir)
# local storage of static files
static_folder = os.path.join(project_dir, 'static_pages')
app = Flask(__name__, static_folder=static_folder)
@app.route("/")
def index_page_support():
    return app.send_static_file('index.html')
if __name__ == '__main__':
    print('This is standard output')
    app.run()

Diese Datei kann über die Kommandozeile gestartet werden, nachdem die Flask-Bibliothek mit dem Kommando pip3 install flask installiert wurde. In der darauffolgende Ausgabe ist zu erkennen, dass ein Webserver zu Entwicklungszwecken gestartet wurde und unter der Adresse http://127.0.0.1:5000 auf einen Aufruf wartet.

 * Serving Flask app 'run_backend'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit

Diese Adresse ist nur mittels des Web Browsers auf dem aktuell genutzten Computer aufrufbar und sieht unspektakulär aus:

Dieses Backend packen wir in einen Docker Container

Damit wir das im vorherigen Schritt erstelle Backend unabhängig von unserem Entwicklungssystem auf einem Server betreiben können, bietet es sich eine Containerisierung mit Docker an. Wir nutzen einen Standard Python-Container namens python:3.9-slim als Grundlage und installieren die beiden Python-Bibliotheken, deren Namen in der Datei requirements.txt gespeichert sind:

flask
gunicorn

Mit dem folgenden Inhalt der Datei Dockerfile-prod wird ein Container beschrieben, in dem das Backend gestartet werden kann. Da wir auch den produktiven Einsatz des Backends anstreben, nutzen gunicorn zum start des Backends.

# Use an official Python runtime as the base image
FROM python:3.9-slim
# Set the working directory in the container
WORKDIR /app
# upgrade
RUN pip install --upgrade pip
# Copy the requirements file into the container
COPY requirements.txt .
# Install the Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy the Python files into the container
COPY scripts/run_backend.py .
COPY static_pages ./static_pages 
# Expose the port on which the Flask app will run
EXPOSE 5000
# Define the command to run the Flask app with Gunicorn
CMD ["gunicorn", "-b", "0.0.0.0:80", "run_backend:app"]

Damit wir auf unübersichtliche Kommandozeilenbefehle zum Starten des Docker-Containers verzichten können, nutzen wir die folgende Datei docker-compose.prod.yml.

version: '3'
services:
  app:
    container_name: backend-staticpage-programmierkunst
    build:
      context: .             
      dockerfile: Dockerfile-prod
    volumes:
      - app:/var/www/html
      - /etc/localtime:/etc/localtime:ro
    environment:
      - VIRTUAL_HOST=staticpage.demo.programmierkunst.com
      - LETSENCRYPT_HOST=staticpage.demo.programmierkunst.com.
      - LETSENCRYPT_EMAIL=demo@programmierkunst.com
    restart: unless-stopped
    expose:
    - 80
volumes:
  app:
  db:
networks:
  default:
    external:
      name: nginx-proxy

Dort ist auch die Internetadresse notiert, unter der das Backend später öffentlich erreichbar sein wird staticpage.demo.programmierkunst.com

Wir öffnen uns zur Welt oder nichts ist umsonst

Im nächsten Schritt kümmern wir uns darum, dass die Webseite auch von anderen Computern aus erreichbar gemacht wird. Erneut gibt es viele technische Möglichkeiten dieses Ziel zu erreichen. Wir entscheinden uns für einen virtuellen privaten Server (VPS), um die volle Kontrolle zu haben. Z.B. Mit dem Hoster noez.de habe ich über viele Jahre gute Erfahrungen gemacht, aber es gibt zahlreiche Alternativen. Wir vermerken als einmalige Anschaffung einen VPS (KVM als Virtualisierungslösung) mit 1 CPU, 1 GB RAM, 10 GB HDD und dem Debian Betriebssystem für unter 4 Euro im Monat. Die Leistung des Servers ist vollkommen ausreichend, solange noch unklar ist, wie viele Aufrufe stattfinden werden. Sicher müssen leistungsstärkere Komponenten verwendet werden, wenn die Nutzerzahlen steigen.

Der frisch gekaufte Server ist jetzt nur über seine IP-Adresse erreichbar. Menschen können sich diese Adressen eher schlecht im Gedächtnis behalten, deshalb werden üblicherweise einer IP-Adresse ein Domainname (Internetadressen) zugeorndet. Domainnamen müssen ebenfalls erworben werden. Wir tun dies über den Anbieter Namecheap.com, da hier zu angemessenen Preisen eine brauchbare Leistung angeboten wird., z.B. die (jetzt nicht mehr verfügbare) Adresse programmierkunst.com für grob 10 Euro im Jahr).

In den DNS-Einstellungen der Adresse vermerken wir, dass alle Aufrufe der Adresse demo.programmierkunst.com an die IP-Adresse des eben gekauften Servers (hier 5.230.68.35) weitergereicht werden. Dazu wird ein A-Record angelegt, wie im folgenden Bild zu sehen ist.

Selbstverständlich wollen wir den nur einen Server und die Internetadresse für alle Beispiele auf diesem Blog genutzen.

Aufrufe mit dem Domainnamen werden nun an unseren Server weitergereicht. Bisher antwortet jedoch niemand. Deshalb gehen wir jetzt dazu über den VPS einzurichten und verbinden uns mit SSH zur IP-Adresse unserers Servers z.B. mit dem Kommandozeilenbefehl ssh root@5.230.68.35

Alle Abhängigkeiten unseres Backends sind innerhalb des Docker-Containers definiert, daher ist der nächste Schritt Docker und Docker-Compose auf unserem Debian VPS zu installieren. Hier gibt es eine Anleitung https://docs.docker.com/engine/install/debian/ Die relevanten Schritte sind 1. Die offiziellen Schlüssel für die offziellen Software-Paketquellen freizugeben

# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

und 2. die entsprechenden Software-Pakete für docker und docker-compose zu installieren, grundlegend zu konfigurieren sowie den Systemdienst docker zu starten:

sudo apt update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
newgrp docker
# install docker-compose
sudo apt install docker-compose
#start docker
systemctl start docker

Im Anschluss sorgen wir dafür, dass wir mehrere Backends auf einem Server bereitsstellen können, indem wir einen Reverse-Proxy installieren, der auch in einem Docker-Container gespeichert ist. Wir erstellen einen neuen Ordner nginx_proxy mit den folgenden beiden Dateien

nginx_proxy
├── docker-compose.yml
└── nginx.tmpl

# Inhalt der Datei docker-compose.yml
# Quelle https://blog.ssdnodes.com/blog/host-multiple-ssl-websites-docker-nginx/  
version: '3'
services:
  nginx:
    image: nginx:1.13.1
    container_name: nginx-proxy
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - conf:/etc/nginx/conf.d
      - vhost:/etc/nginx/vhost.d
      - html:/usr/share/nginx/html
      - certs:/etc/nginx/certs
    labels:
      - "com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy=true"
  dockergen:
    image: jwilder/docker-gen:0.7.3
    container_name: nginx-proxy-gen
    restart: always
    depends_on:
      - nginx
    command: -notify-sighup nginx-proxy -watch -wait 5s:30s /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
    volumes:
      - conf:/etc/nginx/conf.d
      - vhost:/etc/nginx/vhost.d
      - html:/usr/share/nginx/html
      - certs:/etc/nginx/certs
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - ./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro
  letsencrypt:
    image: jrcs/letsencrypt-nginx-proxy-companion
    container_name: nginx-proxy-le
    restart: always
    depends_on:
      - nginx
      - dockergen
    environment:
      NGINX_PROXY_CONTAINER: nginx-proxy
      NGINX_DOCKER_GEN_CONTAINER: nginx-proxy-gen
    volumes:
      - conf:/etc/nginx/conf.d
      - vhost:/etc/nginx/vhost.d
      - html:/usr/share/nginx/html
      - certs:/etc/nginx/certs
      - /var/run/docker.sock:/var/run/docker.sock:ro
volumes:
  conf:
  vhost:
  html:
  certs:
# Do not forget to 'docker network create nginx-proxy' before launch, and to add '--network nginx-proxy' to proxied containers. 
networks:
  default:
    external:
      name: nginx-proxy
# Inhalt der Datei nginx.tmpl
# Quelle https://github.com/nginx-proxy/nginx-proxy
{{ $CurrentContainer := where $ "ID" .Docker.CurrentContainerID | first }}
{{ $external_http_port := coalesce $.Env.HTTP_PORT "80" }}
{{ $external_https_port := coalesce $.Env.HTTPS_PORT "443" }}
{{ $debug_all := $.Env.DEBUG }}
{{ define "ssl_policy" }}
	{{ if eq .ssl_policy "Mozilla-Modern" }}
		ssl_protocols TLSv1.3;
		{{/* nginx currently lacks ability to choose ciphers in TLS 1.3 in configuration, see https://trac.nginx.org/nginx/ticket/1529 /*}}
		{{/* a possible workaround can be modify /etc/ssl/openssl.cnf to change it globally (see https://trac.nginx.org/nginx/ticket/1529#comment:12 ) /*}}
		{{/* explicitly set ngnix default value in order to allow single servers to override the global http value */}}
		ssl_ciphers HIGH:!aNULL:!MD5;
		ssl_prefer_server_ciphers off;
	{{ else if eq .ssl_policy "Mozilla-Intermediate" }}
		ssl_protocols TLSv1.2 TLSv1.3;
		ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
		ssl_prefer_server_ciphers off;
	{{ else if eq .ssl_policy "Mozilla-Old" }}
		ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
		ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA';
		ssl_prefer_server_ciphers on;
	{{ else if eq .ssl_policy "AWS-TLS-1-2-2017-01" }}
		ssl_protocols TLSv1.2 TLSv1.3;
		ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES128-SHA256:AES256-GCM-SHA384:AES256-SHA256';
		ssl_prefer_server_ciphers on;
	{{ else if eq .ssl_policy "AWS-TLS-1-1-2017-01" }}
		ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
		ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA';
		ssl_prefer_server_ciphers on;
	{{ else if eq .ssl_policy "AWS-2016-08" }}
		ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
		ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA';
		ssl_prefer_server_ciphers on;
	{{ else if eq .ssl_policy "AWS-2015-05" }}
		ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
		ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA:DES-CBC3-SHA';
		ssl_prefer_server_ciphers on;
	{{ else if eq .ssl_policy "AWS-2015-03" }}
		ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
		ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA:DHE-DSS-AES128-SHA:DES-CBC3-SHA';
		ssl_prefer_server_ciphers on;
	{{ else if eq .ssl_policy "AWS-2015-02" }}
		ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
		ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA:DHE-DSS-AES128-SHA';
		ssl_prefer_server_ciphers on;
	{{ end }}
{{ end }}
# If we receive X-Forwarded-Proto, pass it through; otherwise, pass along the
# scheme used to connect to this server
map $http_x_forwarded_proto $proxy_x_forwarded_proto {
  default $http_x_forwarded_proto;
  ''      $scheme;
}
# If we receive X-Forwarded-Port, pass it through; otherwise, pass along the
# server port the client connected to
map $http_x_forwarded_port $proxy_x_forwarded_port {
  default $http_x_forwarded_port;
  ''      $server_port;
}
# If we receive Upgrade, set Connection to "upgrade"; otherwise, delete any
# Connection header that may have been passed to this server
map $http_upgrade $proxy_connection {
  default upgrade;
  '' close;
}
# Apply fix for very long server names
server_names_hash_bucket_size 128;
# Default dhparam
{{ if (exists "/etc/nginx/dhparam/dhparam.pem") }}
ssl_dhparam /etc/nginx/dhparam/dhparam.pem;
{{ end }}
# Set appropriate X-Forwarded-Ssl header based on $proxy_x_forwarded_proto
map $proxy_x_forwarded_proto $proxy_x_forwarded_ssl {
  default off;
  https on;
}
gzip_types text/plain text/css application/javascript application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
log_format vhost '$host $remote_addr - $remote_user [$time_local] '
                 '"$request" $status $body_bytes_sent '
                 '"$http_referer" "$http_user_agent" '
                 '"$upstream_addr"';
access_log off;
{{/* Get the SSL_POLICY defined by this container, falling back to "Mozilla-Intermediate" */}}
{{ $ssl_policy := or ($.Env.SSL_POLICY) "Mozilla-Intermediate" }}
{{ template "ssl_policy" (dict "ssl_policy" $ssl_policy) }}
{{ if $.Env.RESOLVERS }}
resolver {{ $.Env.RESOLVERS }};
{{ end }}
{{ if (exists "/etc/nginx/proxy.conf") }}
include /etc/nginx/proxy.conf;
{{ else }}
# HTTP 1.1 support
proxy_http_version 1.1;
proxy_buffering off;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $proxy_connection;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
proxy_set_header X-Forwarded-Ssl $proxy_x_forwarded_ssl;
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
# Mitigate httpoxy attack (see README for details)
proxy_set_header Proxy "";
{{ end }}
{{ $access_log := (or (and (not $.Env.DISABLE_ACCESS_LOGS) "access_log /var/log/nginx/access.log vhost;") "") }}
{{ $enable_ipv6 := eq (or ($.Env.ENABLE_IPV6) "") "true" }}
server {
	server_name _; # This is just an invalid value which will never trigger on a real hostname.
	server_tokens off;
	listen {{ $external_http_port }};
	{{ if $enable_ipv6 }}
	listen [::]:{{ $external_http_port }};
	{{ end }}
	{{ $access_log }}
	return 503;
}
{{ if (and (exists "/etc/nginx/certs/default.crt") (exists "/etc/nginx/certs/default.key")) }}
server {
	server_name _; # This is just an invalid value which will never trigger on a real hostname.
	server_tokens off;
	listen {{ $external_https_port }} ssl http2;
	{{ if $enable_ipv6 }}
	listen [::]:{{ $external_https_port }} ssl http2;
	{{ end }}
	{{ $access_log }}
	return 503;
	ssl_session_cache shared:SSL:50m;
	ssl_session_tickets off;
	ssl_certificate /etc/nginx/certs/default.crt;
	ssl_certificate_key /etc/nginx/certs/default.key;
}
{{ end }}
{{ range $host, $containers := groupByMulti $ "Env.VIRTUAL_HOST" "," }}
{{ $host := trim $host }}
{{ $is_regexp := hasPrefix "~" $host }}
{{ $upstream_name := (print (when $is_regexp (sha1 $host) $host) "-upstream") }}
# {{ $host }}
upstream {{ $upstream_name }} {
{{ $server_found := "false" }}
{{ range $container := $containers }}
	{{ $debug := (eq (coalesce $container.Env.DEBUG $debug_all "false") "true") }}
	{{/* If only 1 port exposed, use that as a default, else 80 */}}
	{{ $defaultPort := (when (eq (len $container.Addresses) 1) (first $container.Addresses) (dict "Port" "80")).Port }}
	{{ $port := (coalesce $container.Env.VIRTUAL_PORT $defaultPort) }}
	{{ $address := where $container.Addresses "Port" $port | first }}
	{{ if $debug }}
	# Exposed ports: {{ $container.Addresses }}
	# Default virtual port: {{ $defaultPort }}
	# VIRTUAL_PORT: {{ $container.Env.VIRTUAL_PORT }}
		{{ if not $address }}
	# /!\ Virtual port not exposed
		{{ end }}
	{{ end }}
	{{ range $knownNetwork := $CurrentContainer.Networks }}
		{{ range $containerNetwork := $container.Networks }}
			{{ if (and (ne $containerNetwork.Name "ingress") (or (eq $knownNetwork.Name $containerNetwork.Name) (eq $knownNetwork.Name "host"))) }}
	## Can be connected with "{{ $containerNetwork.Name }}" network
				{{ if $address }}
					{{/* If we got the containers from swarm and this container's port is published to host, use host IP:PORT */}}
					{{ if and $container.Node.ID $address.HostPort }}
						{{ $server_found = "true" }}
	# {{ $container.Node.Name }}/{{ $container.Name }}
	server {{ $container.Node.Address.IP }}:{{ $address.HostPort }};
					{{/* If there is no swarm node or the port is not published on host, use container's IP:PORT */}}
					{{ else if $containerNetwork }}
						{{ $server_found = "true" }}
	# {{ $container.Name }}
	server {{ $containerNetwork.IP }}:{{ $address.Port }};
					{{ end }}
				{{ else if $containerNetwork }}
	# {{ $container.Name }}
					{{ if $containerNetwork.IP }}
						{{ $server_found = "true" }}
	server {{ $containerNetwork.IP }}:{{ $port }};
					{{ else }}
	# /!\ No IP for this network!
					{{ end }}
				{{ end }}
			{{ else }}
	# Cannot connect to network '{{ $containerNetwork.Name }}' of this container
			{{ end }}
		{{ end }}
	{{ end }}
{{ end }}
{{/* nginx-proxy/nginx-proxy#1105 */}}
{{ if (eq $server_found "false") }}
	# Fallback entry
	server 127.0.0.1 down;
{{ end }}
}
{{ $default_host := or ($.Env.DEFAULT_HOST) "" }}
{{ $default_server := index (dict $host "" $default_host "default_server") $host }}
{{/* Get the VIRTUAL_PROTO defined by containers w/ the same vhost, falling back to "http" */}}
{{ $proto := trim (or (first (groupByKeys $containers "Env.VIRTUAL_PROTO")) "http") }}
{{/* Get the SERVER_TOKENS defined by containers w/ the same vhost, falling back to "" */}}
{{ $server_tokens := trim (or (first (groupByKeys $containers "Env.SERVER_TOKENS")) "") }}
{{/* Get the NETWORK_ACCESS defined by containers w/ the same vhost, falling back to "external" */}}
{{ $network_tag := or (first (groupByKeys $containers "Env.NETWORK_ACCESS")) "external" }}
{{/* Get the HTTPS_METHOD defined by containers w/ the same vhost, falling back to "redirect" */}}
{{ $https_method := or (first (groupByKeys $containers "Env.HTTPS_METHOD")) (or $.Env.HTTPS_METHOD "redirect") }}
{{/* Get the SSL_POLICY defined by containers w/ the same vhost, falling back to empty string (use default) */}}
{{ $ssl_policy := or (first (groupByKeys $containers "Env.SSL_POLICY")) "" }}
{{/* Get the HSTS defined by containers w/ the same vhost, falling back to "max-age=31536000" */}}
{{ $hsts := or (first (groupByKeys $containers "Env.HSTS")) (or $.Env.HSTS "max-age=31536000") }}
{{/* Get the VIRTUAL_ROOT By containers w/ use fastcgi root */}}
{{ $vhost_root := or (first (groupByKeys $containers "Env.VIRTUAL_ROOT")) "/var/www/public" }}
{{/* Get the first cert name defined by containers w/ the same vhost */}}
{{ $certName := (first (groupByKeys $containers "Env.CERT_NAME")) }}
{{/* Get the best matching cert  by name for the vhost. */}}
{{ $vhostCert := (closest (dir "/etc/nginx/certs") (printf "%s.crt" $host))}}
{{/* vhostCert is actually a filename so remove any suffixes since they are added later */}}
{{ $vhostCert := trimSuffix ".crt" $vhostCert }}
{{ $vhostCert := trimSuffix ".key" $vhostCert }}
{{/* Use the cert specified on the container or fallback to the best vhost match */}}
{{ $cert := (coalesce $certName $vhostCert) }}
{{ $is_https := (and (ne $https_method "nohttps") (ne $cert "") (exists (printf "/etc/nginx/certs/%s.crt" $cert)) (exists (printf "/etc/nginx/certs/%s.key" $cert))) }}
{{ if $is_https }}
{{ if eq $https_method "redirect" }}
server {
	server_name {{ $host }};
	{{ if $server_tokens }}
	server_tokens {{ $server_tokens }};
	{{ end }}
	listen {{ $external_http_port }} {{ $default_server }};
	{{ if $enable_ipv6 }}
	listen [::]:{{ $external_http_port }} {{ $default_server }};
	{{ end }}
	{{ $access_log }}
	
	# Do not HTTPS redirect Let'sEncrypt ACME challenge
	location ^~ /.well-known/acme-challenge/ {
		auth_basic off;
		auth_request off;
		allow all;
		root /usr/share/nginx/html;
		try_files $uri =404;
		break;
	}
	
	location / {
		{{ if eq $external_https_port "443" }}
		return 301 https://$host$request_uri;
		{{ else }}
		return 301 https://$host:{{ $external_https_port }}$request_uri;
		{{ end }}
	}
}
{{ end }}
server {
	server_name {{ $host }};
	{{ if $server_tokens }}
	server_tokens {{ $server_tokens }};
	{{ end }}
	listen {{ $external_https_port }} ssl http2 {{ $default_server }};
	{{ if $enable_ipv6 }}
	listen [::]:{{ $external_https_port }} ssl http2 {{ $default_server }};
	{{ end }}
	{{ $access_log }}
	{{ if eq $network_tag "internal" }}
	# Only allow traffic from internal clients
	include /etc/nginx/network_internal.conf;
	{{ end }}
	{{ template "ssl_policy" (dict "ssl_policy" $ssl_policy) }}
	client_max_body_size 10M; ##MANUAL EDIT
	ssl_session_timeout 5m;
	ssl_session_cache shared:SSL:50m;
	ssl_session_tickets off;
	ssl_certificate /etc/nginx/certs/{{ (printf "%s.crt" $cert) }};
	ssl_certificate_key /etc/nginx/certs/{{ (printf "%s.key" $cert) }};
	{{ if (exists (printf "/etc/nginx/certs/%s.dhparam.pem" $cert)) }}
	ssl_dhparam {{ printf "/etc/nginx/certs/%s.dhparam.pem" $cert }};
	{{ end }}
	{{ if (exists (printf "/etc/nginx/certs/%s.chain.pem" $cert)) }}
	ssl_stapling on;
	ssl_stapling_verify on;
	ssl_trusted_certificate {{ printf "/etc/nginx/certs/%s.chain.pem" $cert }};
	{{ end }}
	{{ if (not (or (eq $https_method "noredirect") (eq $hsts "off"))) }}
	add_header Strict-Transport-Security "{{ trim $hsts }}" always;
	{{ end }}
	{{ if (exists (printf "/etc/nginx/vhost.d/%s" $host)) }}
	include {{ printf "/etc/nginx/vhost.d/%s" $host }};
	{{ else if (exists "/etc/nginx/vhost.d/default") }}
	include /etc/nginx/vhost.d/default;
	{{ end }}
	location / {
		{{ if eq $proto "uwsgi" }}
		include uwsgi_params;
		uwsgi_pass {{ trim $proto }}://{{ trim $upstream_name }};
		{{ else if eq $proto "fastcgi" }}
		root   {{ trim $vhost_root }};
		include fastcgi_params;
		fastcgi_pass {{ trim $upstream_name }};
		{{ else if eq $proto "grpc" }}
		grpc_pass {{ trim $proto }}://{{ trim $upstream_name }};
		{{ else }}
		proxy_pass {{ trim $proto }}://{{ trim $upstream_name }};
		{{ end }}
		{{ if (exists (printf "/etc/nginx/htpasswd/%s" $host)) }}
		auth_basic	"Restricted {{ $host }}";
		auth_basic_user_file	{{ (printf "/etc/nginx/htpasswd/%s" $host) }};
		{{ end }}
		{{ if (exists (printf "/etc/nginx/vhost.d/%s_location" $host)) }}
		include {{ printf "/etc/nginx/vhost.d/%s_location" $host}};
		{{ else if (exists "/etc/nginx/vhost.d/default_location") }}
		include /etc/nginx/vhost.d/default_location;
		{{ end }}
	}
}
{{ end }}
{{ if or (not $is_https) (eq $https_method "noredirect") }}
server {
	server_name {{ $host }};
	{{ if $server_tokens }}
	server_tokens {{ $server_tokens }};
	{{ end }}
	listen {{ $external_http_port }} {{ $default_server }};
	{{ if $enable_ipv6 }}
	listen [::]:80 {{ $default_server }};
	{{ end }}
	{{ $access_log }}
	{{ if eq $network_tag "internal" }}
	# Only allow traffic from internal clients
	include /etc/nginx/network_internal.conf;
	{{ end }}
	{{ if (exists (printf "/etc/nginx/vhost.d/%s" $host)) }}
	include {{ printf "/etc/nginx/vhost.d/%s" $host }};
	{{ else if (exists "/etc/nginx/vhost.d/default") }}
	include /etc/nginx/vhost.d/default;
	{{ end }}
	location / {
		{{ if eq $proto "uwsgi" }}
		include uwsgi_params;
		uwsgi_pass {{ trim $proto }}://{{ trim $upstream_name }};
		{{ else if eq $proto "fastcgi" }}
		root   {{ trim $vhost_root }};
		include fastcgi_params;
		fastcgi_pass {{ trim $upstream_name }};
		{{ else if eq $proto "grpc" }}
		grpc_pass {{ trim $proto }}://{{ trim $upstream_name }};
		{{ else }}
		proxy_pass {{ trim $proto }}://{{ trim $upstream_name }};
		{{ end }}
		{{ if (exists (printf "/etc/nginx/htpasswd/%s" $host)) }}
		auth_basic	"Restricted {{ $host }}";
		auth_basic_user_file	{{ (printf "/etc/nginx/htpasswd/%s" $host) }};
		{{ end }}
		{{ if (exists (printf "/etc/nginx/vhost.d/%s_location" $host)) }}
		include {{ printf "/etc/nginx/vhost.d/%s_location" $host}};
		{{ else if (exists "/etc/nginx/vhost.d/default_location") }}
		include /etc/nginx/vhost.d/default_location;
		{{ end }}
	}
}
{{ if (and (not $is_https) (exists "/etc/nginx/certs/default.crt") (exists "/etc/nginx/certs/default.key")) }}
server {
	server_name {{ $host }};
	{{ if $server_tokens }}
	server_tokens {{ $server_tokens }};
	{{ end }}
	listen {{ $external_https_port }} ssl http2 {{ $default_server }};
	{{ if $enable_ipv6 }}
	listen [::]:{{ $external_https_port }} ssl http2 {{ $default_server }};
	{{ end }}
	{{ $access_log }}
	return 500;
	ssl_certificate /etc/nginx/certs/default.crt;
	ssl_certificate_key /etc/nginx/certs/default.key;
}
{{ end }}
{{ end }}
{{ end }}

Den neu erstellten Ordner inkl. der beiden Dateien kopieren wir mit ssh auf unseren Server. Dann installieren wir noch das virtuelle Docker-Netzwerk und starten den Reverse-Proxy

scp -r nginx-proxy root@5.230.68.35:
docker network create nginx-proxy
cd nginx_proxy/
docker-compose up -d

Als finalen Schritt kopieren wir den oben erstellen Order backend_static_page auf den Server und starten auch diesen Docker-Container

scp -r nginx-proxy root@5.230.68.35:
cd backend_static_page/
docker-compose up -d

Als Ergebnis können wir unsere Webseite unter Adresse staticpage.demo.programmierkunst.com abrufen.

Wichtig ist, dass auch das automatisch erstellte SSL-Zertifikat (über LetsEncrypt) vom Web-Browser als gültig akzeptiert wird.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *