Einleitung

Durch die fortschreitende Digitalisierung wird die IT-Infrastruktur von Unternehmen immer komplexer. Bei der Entwicklung einer einzigen Software oder Website lässt sich diese Komplexität durch Dinge wie bspw. Clean Code gut managen.

In den meisten Fällen wird heutzutage allerdings eine Vielzahl von Websites, Blogs und internen Tools eingesetzt. Um diese ordentlich zu trennen ohne dabei Unmengen für die Administration unterschiedliche Server auszugeben setzen immer mehr Unternehmen auf sogenannte container-basierte Anwendungen. Damit ist es (vereinfacht gesagt) möglich, auf nur einem Server viele kleine „Unterserver“ zu starten. Docker ist dabei eines der bekanntesten Tools für die Erstellung und Verwaltung von Containern. In diesem Artikel zeigen wir Ihnen, wie Sie Docker einsetzen können, um Ihre Anwendungen und/oder Websites zu hosten und zu skalieren.

Durch die zusätzliche Verwendung eines Reverse-Proxy können Sie dabei einfach und effizient beliebig viele Dienste unter einer Domain und auf nur einem Server laufen lassen.

Bei Service-Soft nutzen wir diese Technik übrigens auch selbst, der Blog auf dem Sie sich momentan befinden ist ein Docker-Container und läuft zusammen mit unserer Website, unserer Api, einem internen Tool, einem Datenbankserver und einem Cloudserver.

Docker Compose

Docker Compose ist ein Tool für Docker, mit dem Sie mehrere Dienste wie bspw. Websites, Groupware oder auch Datenbanken zentral in einer Datei verwalten können. Mit Docker Compose können Sie Ihre Anwendungen schnell und einfach starten und stoppen und Sie haben die Möglichkeit, die Dienste zu konfigurieren. Dadurch ist es möglich, komplette Systemlandschaften aus unterschiedlichsten Serveranwendungen mit nur einem einzigen Befehl zu starten.

Reverse Proxy

Ein Reverse Proxy ist ein Server, der dafür zuständig ist, eingehende Anfragen weiterzuleiten. Der Reverse Proxy ist dabei als einziger Server über das Internet erreichbar. Dadurch können Sie Ihre Anwendungen einfach auf mehrere Server verteilen. Oder Sie nutzen ihn auf einem Server, um nicht für jeden Dienst einen eigenen Server mieten und administrieren zu müssen.

Beispiel:
Sie haben einen Blog, eine Website und noch eine Api. Durch einen Reverse-Proxy könnten Sie dann alle Anfragen unter blog.meine-domain.de an den Blog, alle Anfragen unter meine-domain.de an Ihre Website und alle Anfragen unter api.meine-domain.de an Ihre Api weiterleiten.

Ein weiterer Vorteil des Reverse-Proxies ist die erhöhte Sicherheit:
Dadurch, dass alle Kommunikation nur durch den Reverse-Proxy stattfindet ist der Angriffsvektor automatisch auf das Minimum reduziert und bösartige Anfragen können abgewehrt werden, noch bevor sie einen der „echten“ Server erreichen.

Sie möchten Unterstützung oder haben Interesse an einer Zusammenarbeit?

Technische Umsetzung

Docker Compose

Zunächst wird eine docker-compose.yaml Datei benötigt. Hier werden sämtliche Dienste definiert, die von Docker verwaltet werden sollen. Eine grundlegende Datei nur mit einem Reverse-Proxy Container sieht dabei wie folgt aus:

version: '3.9'

services:

    reverse:
        image: nginx:1.23
        restart: unless-stopped
        volumes:
            - '/etc/letsencrypt/:/etc/nginx/ssl'
            - './reverse-proxy/nginx-custom.conf:/etc/nginx/conf.d/default.conf'
            - '/reverse-proxy'
        ports:
            - '80:80'
            - '443:443'
YAML

Der Abschnitt beginnt dabei mit dem Namen des Dienstes, in diesem Fall „reverse“.
Das image gibt an, dass ein nginx-Server gebaut werden soll.
Durch das „restart: unless-stopped“ startet der Dienst automatisch neu.
Über den volumes Abschnitt lassen sich Daten des Containers verwalten. Die ersten beiden Stichpunkte dienen dazu, SSL-Zertifikate und eine angepasste nginx Konfiguration in den Server zu laden (Mehr dazu später). Der letzte Stichpunkt ist der Ort, wo die Daten des Containers gespeichert werden. Dieser ist dabei kein lokales Verzeichnis sondern wird auch komplett von Docker verwaltet.
Über den Abschnitt ports wird angegeben, dass der reverse-proxy über die Standardports 443 (https) und 80 (http) erreichbar ist.

Als nächstes können einfach und flexibel neue Dienste definiert werden, auf die der Reverse-Proxy weiterleiten soll. Als Beispiel wurden hier ein Datenbankserver, eine Api und eine Website hinterlegt:

version: '3.9'

services:

    reverse:
        image: nginx:1.23
        restart: unless-stopped
        volumes:
            - '/etc/letsencrypt/:/etc/nginx/ssl'
            - './reverse-proxy/nginx-custom.conf:/etc/nginx/conf.d/default.conf'
            - '/reverse-proxy'
        ports:
            - '80:80'
            - '443:443'

    db:
        image: mariadb:10
        restart: unless-stopped
        volumes:
            - db-data:/var/lib/mysql
        environment:
            MARIADB_ROOT_PASSWORD_HASH: root_password_hash
            MARIADB_DATABASE: my_db
            MARIADB_USER: my_db_user
            MARIADB_PASSWORD_HASH: password_hash

    api:
        build: './api'
        restart: unless-stopped
        volumes:
            - /api

    website:
        build: './website'
        restart: unless-stopped
        volumes:
            - '/website'
            - './website/nginx-custom.conf:/etc/nginx/conf.d/default.conf'

volumes:
    db-data:
YAML

Im Gegensatz zum Reverse-Proxy und der Datenbank wird bei Website und Api kein fertiges Image verwendet sondern ein eigenes gebaut.
Dazu wird eine Dockerfile in den angegebenen Verzeichnissen ./website und ./api benötigt (Mehr).

Reverse-Proxy

Wie oben ersichtlich ist der einzige Dienst mit offenen Ports der reverse-proxy. Wie genau nun die Weiterleitung funktioniert wird in der Konfigurationsdatei nginx-custom.conf definiert.

Am Anfang müssen die einzelnen Server an die weitergeleitet werden soll als upstream definiert werden. Allerdings sind diese nur im Netzwerk von Docker verfügbar und nicht direkt auf dem Server.
Wie können diese also erreicht werden?

Docker erstellt hier praktischerweise ein Netzwerk, in welchem alle Dienste innerhalb derselben Docker Compose Datei ganz simpel über ihren Namen erreichbar sind.

Die folgende Datei sorgt dafür, dass der Dienst website über www.meine-domain.de und meine-domain.de erreichbar ist und Anfragen an api.meine-domain.de an die Api weitergeleitet werden.
Für den Datenbankserver gibt es keine Weiterleitung, da dieser nur von der Api verwendet wird und aus Sicherheitsgründen auch nicht über das Internet erreichbar sein sollte.

# ddos
limit_conn_zone $binary_remote_addr zone=addr:10m;

# Defines all Subdomains
upstream website {
    server website:4200;
}
upstream api {
    server api:3000;
}

# unknown subdomains
server {
    listen 80 default_server;
    return 301 http://www.meine-domain.de;
}
server {
    listen 443 default_server ssl http2;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_certificate /etc/nginx/ssl/live/www.meine-domain.de/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/live/www.meine-domain.de/privkey.pem;
    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload" always;

    return 301 https://www.meine-domain.de;
}

###########
# website #
###########
# redirect to https
server {
    server_name meine-domain.de;
    listen 80;
    return 301 https://$host$request_uri;
}
server {
    server_name www.meine-domain.de;
    listen 80;
    return 301 https://$host$request_uri;
}
# redirect to www.meine-domain.de
server {
    server_name meine-domain.de;
    listen 443 ssl http2;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_certificate /etc/nginx/ssl/live/www.meine-domain.de/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/live/www.meine-domain.de/privkey.pem;
    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload" always;

    return 301 https://www.meine-domain.de$request_uri;
}
# the "real" website server
server {
    server_name www.meine-domain.de;
    listen 443 ssl http2;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_certificate /etc/nginx/ssl/live/www.meine-domain.de/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/live/www.meine-domain.de/privkey.pem;

    location /  {
        proxy_pass http://website;
    }

    proxy_set_header    X-Forwarded-Host   $host;
    proxy_set_header    X-Forwarded-Server $host;
    proxy_set_header    X-Forwarded-For    $proxy_add_x_forwarded_for;
    proxy_set_header    X-Forwarded-Proto  $scheme;
    proxy_set_header    X-Real-IP          $remote_addr;
    proxy_set_header    Host               $host;
    # security
    fastcgi_hide_header X-Powered-By;
    server_tokens off;
    add_header X-Content-Type-Options nosniff;
    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload" always;
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    # ddos
    limit_conn addr 20;
    client_body_timeout 20s;
    client_header_timeout 20s;
    # proxy_cache_use_stale updating;
    # caching and gzip
    # expires $expires;
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_proxied expired no-cache no-store private auth;
    gzip_types
        text/plain
        text/css
        text/js
        text/xml
        text/javascript
        application/javascript
        application/x-javascript
        application/json
        application/xml
        application/rss+xml
        image/svg+xml;
    gzip_disable "MSIE [1-6]\.";
}

############
# api #
############
# redirect to https
server {
    server_name api.meine-domain.de;
    listen 80;
    return 301 https://$host$request_uri;
}
# the "real" api server
server {
    server_name api.meine-domain.de;
    listen 443 ssl http2;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_certificate /etc/nginx/ssl/live/www.meine-domain.de/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/live/www.meine-domain.de/privkey.pem;

    location /  {
        proxy_pass http://api;
    }

    proxy_set_header    X-Forwarded-Host   $host;
    proxy_set_header    X-Forwarded-Server $host;
    proxy_set_header    X-Forwarded-For    $proxy_add_x_forwarded_for;
    proxy_set_header    X-Forwarded-Proto  $scheme;
    proxy_set_header    X-Real-IP          $remote_addr;
    proxy_set_header    Host               $host;
    # security
    fastcgi_hide_header X-Powered-By;
    server_tokens off;
    add_header X-Content-Type-Options nosniff;
    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload" always;
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    # ddos
    limit_conn addr 20;
    client_body_timeout 20s;
    client_header_timeout 20s;
    # proxy_cache_use_stale updating;
    # caching and gzip
    # expires $expires;
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_proxied expired no-cache no-store private auth;
    gzip_types
        text/plain
        text/css
        text/js
        text/xml
        text/javascript
        application/javascript
        application/x-javascript
        application/json
        application/xml
        application/rss+xml
        image/svg+xml;
    gzip_disable "MSIE [1-6]\.";
}
Nginx

SSL Verschlüsselung

Die Konfigurationsdatei kümmert sich außerdem darum, dass sämtliche Anfragen von http (unverschlüsselt) an https (verschlüsselt) weitergeleitet werden.

Für die Verschlüsselung werden SSL-Zertifikate verwendet, die über die Docker Compose Datei vom lokalen Ordner /etc/letsencrypt/ in den Reverse Container hineingereicht werden:

reverse:
    image: nginx:1.23
    restart: unless-stopped
    volumes:
        - '/etc/letsencrypt/:/etc/nginx/ssl'
        - './reverse-proxy/nginx-custom.conf:/etc/nginx/conf.d/default.conf'
        - '/reverse-proxy'
    ports:
        - '80:80'
        - '443:443'
YAML

Mit certbot bzw. letsencrypt lassen sich solche SSL-Zertifikate komplett kostenlos erstellen. Dazu muss certbot auf dem Server installiert und anschließend der Befehl sudo certbot certonly --standalone ausgeführt werden. Hier ist darauf zu achten, Zertifikate für alle Subdomains zu erstellen, also bspw. www.meine-domain.de, meine-domain.de, api.meine-domain.de etc.

Abschließend können sämtliche Dienste mit dem Befehl docker compose up --build -d gestartet werden. Mit docker compose down lassen sich die Dienste wieder stoppen.