Grafik zum Meilenstein Deployment der heat-conduction-app. Clients greifen über das Internet per HTTPS auf einen VPS zu. Eine Firewall filtert eingehende Anfragen, die an einen containerisierten Reverse Proxy im Docker-Netzwerk weitergeleitet werden. Der Reverse Proxy terminiert TLS und leitet interne HTTP-Anfragen an mehrere Application Services weiter.

Deployment - vom lokalen Setup zum produktiven System

heat-conduction-app: Referenzprojekt für moderne Backend- und Webarchitektur

Deployment Containerisierung Docker Reverse Proxy Let's Encrypty Backend heat-conduction-app

← Zurück zur Übersicht | ← Meilenstein 4 - Datenbankintegration

Einleitung

Im vorangegangenen Artikel zur Datenbankintegration (Meilenstein 4) wurde die Persistenzschicht der heat-conduction-app umgesetzt. Kern ist eine SQLite-Datenbank mit Repository-Schicht zur gekapselten Anbindung. Materialien werden aus der Datenbank geladen und in der Oberfläche über ein Dropdown zur Auswahl bereitgestellt. Damit sind Backend-Logik, UI und Persistenz der ersten Phase vollständig integriert.

In diesem Meilenstein geht um das Deployment der Anwendung. Ich zeige, wie ich die Flask-Anwendung containerisiert mit Docker und Nginx zunächst lokal betreibe und das Setup anschließend auf einen VPS übertrage, inklusive HTTPS, Reverse Proxy und grundlegender Absicherung.

Ziel ist kein vollständiges Produktionssetup, sondern eine pragmatische, reproduzierbare Lösung für kleine bis mittlere Anwendungen.

Projektkontext

Dieser Artikel ist Teil zur Serie zur heat-conduction-app. Der aktuelle Stand ist im Repository verfügbar, die Anwendung läuft produktiv unter einer eigenen Subdomain.

Architektur und Deployment-Strategie

Vor der Umsetzung kläre ich die grundlegenden Architektur- und Deploymententscheidungen.

Die klassische Variante ist die direkte Installation der benötigten Dienste auf einem Server oder VPS. Für eine Flask-Anwendung ergibt sich typischerweise ein Setup wie:

Nginx → Supervisor → Gunicorn → Flask

Diese Lösung ist vergleichsweise einfach aufzusetzen und direkt zugänglich, da alle Komponenten über den Paketmanager installiert werden. Debugging erfolgt unmittelbar auf dem System.

Ich entscheide mich hier stattdessen für ein containerisiertes Deployment. Alle Dienste laufen isoliert in Containern innerhalb von Docker. Das erhöht zunächst die Komplexität, bietet aber Vorteile. Die Umgebung ist reproduzierbar, bietet versionierbare Konfigurationen und eine saubere Trennung der Komponenten. Änderungen lassen sich gezielt und kontrolliert ausrollen.

Die Orchestrierung erfolgt über eine zentrale docker-compose.yml. Damit wird nicht nur die Anwendung, sondern die gesamte Laufzeitumgebung konsistent beschrieben.

Der Einsatz eines Reverse-Proxys ist eine bewusste Architekturentscheidung. Alternativ könnte Gunicorn direkt Anfragen entgegennehmen, jedoch übernimmt der Reverse Proxy zentrale Aufgaben. Dazu gehören, das Routing eingehender Requests, die Terminierung von HTTPS und die Trennung zwischen öffentlichem Zugang und internen Services. Weitere Funktionen wie Lastverteilung oder zusätzliche Sicherheitsmechanismen sind damit vorbereitet.

Damit ergibt sich das folgende Zielbild:

[ Client ] Intern/Browser
  ↓
[ Nginx (Reverse Proxy, HTTPS)]
  ↓
[ Gunicorn / Flask ]
  ↓
[ SQLite (Volume)]

Die Umsetzung erfolgt in zwei Schritten. Zunächst ein lokales Setup als reproduzierbare Basis und zur Validierung der Konfiguration. Anschließend wird die Anwendung auf einem VPS unter produktionsnahen Bedingungen betrieben. Dabei stehen HTTPSm Domain-Anbindung und grundlegende Sicherheitsaspekte im Fokus.

Lokales Setup / Deployment (Docker + nginx)

Beim lokalen Deployment wird das containerisierte Setup bereits weitgehend umgesetzt. Ziel ist eine reproduzierbare Basis, die sich später auf den VPS übertragen lässt. HTTPS mittels Let's Encrypt fehlt an dieser Stelle noch und wird im nächsten Abschnitt ergänzt.

Ausgangspunkt ist der aktuelle Projektstand aus dem Repository. Nach dem Klonen werden die Abhängigkeiten aktualisiert und eine .env-datei mit den notwendigen Umgebungsvariablen angelegt (SECRET_KEY, DATABASE_URI etc.). Damit ist die Anwendung lokal mit dem Flask-Development-Server lauffähig.

Für den Containerbetrieb kommen drei zusätzliche Bausteine hinzu, ein Dockerfile, Reverse Proxy Konfiguration (nginx) und eine zentrale docker-compose.yml.

Dockerisierung

Das Dockerfile beschreibt, wie aus der Flask-Anwendung ein lauffähiges Image entsteht. Dieses Image dient als unveränderliche Basis für den späteren Container und enthält die Anwendung, Abhängigkeiten und Laufzeitumgebung.

Ein mögliches Dockerfile für die heat-conduction-app sieht wie folgt aus:

FROM python:3.11-alpine

RUN apk add --no-cache sqlite

ENV PYTHONUNBUFFERED=1
WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip
RUN pip install --no-cache-dir -r requirements.txt gunicorn

COPY . .

RUN mkdir -p instance

COPY entrypoint.sh ./
RUN chmod +x entrypoint.sh

RUN adduser -D flaskuser
RUN chown -R flaskuser:flaskuser /app

USER flaskuser

EXPOSE 8000

ENTRYPOINT [ "./entrypoint.sh" ]

Drei Punkte möchte ich hervorheben:

  1. Basis-Image: python:3.11-alpine als schlanke, ausreichend vollständige Grundlage
  2. Kapselung der Anwendung: Quellcode wird ins Image kopiert, nicht benötigte Dateien über .dockerignore ausgeschlossen
  3. Startlogik: Auslagerung in ein Shellskript entrypoint.sh

Das Shellskript übernimmt initiale Schritte vor dem Start der Anwendung:

  • Datenbankmigration
  • Initiale Daten (Seeding)
  • Vorbereitung von Ressourcen (z.B. Übersetzungen)
  • Start von Gunicorn

Damit ist sichergestellt, dass der Container in einem definierten Zustand startet. Die Anwendung selbst läuft hinter Gunicorn und ist innerhalb des Containers auf Port 8000 erreichbar.

Reverse Proxy mit nginx

Vor die Flask-Anwendung wird ein Reverse Proxy geschaltet. Zum Einsatz kommt nginx als schlanker Webserver mit Proxy-Funktionalität, seine zentrale Rolle ist:

  • Annahme aller externen Requests über Port 80/443
  • Weiterleitung an interne Services (hier: Flask/Gunicorn)
  • Trennung von öffentlichem Zugriff und interner Service-Kommunikation
  • Zentrale Verwaltung von HTTPS (TLS-Termination)

Innerhalb des Docker-Netzwerks kommunizieren die Container unverschlüsselt. Die TLS-Verschlüsselung endet am Proxy, wodurch die Anwendung selbst keine Zertifikate verwalten muss.

Für das lokale Setup werden selbstsignierte Zertifikate verwendet. Diese sind für den Browser nicht vertrauenswürdig, reichen aber aus, um HTTPS-Verhalten und Proxy-Konfiguration im lokalen Deployment zu testen.

Auch nginx läuft als Container. Damit ist die gesamte Infrastruktur, Anwendung und Webserver, als Code definiert und reproduzierbar.

Infrastruktur Orchestrierung - docker-compose

Die einzelnen Container werden nicht isoliert gestartet, sondern über docker compose orchestriert. Ziel ist eine deklarative Beschreibung aller Services und ihrer Abhängigkeiten in einer zentralen Konfigurationsdatei.

services:
  nginx:
    image: nginx:latest
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./nginx/certs:/etc/nginx/certs

  heat-conduction-app:
    build: ./services/heat-conduction-app
    restart: always

    env_file:
      - .env
    volumes:
      - heat-instance:/app/instance
    expose:
      - "8000"

volumes:
  heat-instance:

Das minimale Setup besteht aus zwei Services.

  • nginx ist öffentlich erreichbar über Port 80/443 und bindet Konfigurations- und Zertifikatsverzeichnisse mit ein.
  • heat-conduction-app wird aus dem lokalen Dockerfile gebaut, erhält Umgebungsvariablen aus .env und ist intern über Port 8000 erreichbar.

Wichtige Aspekte:

  • Netzwerk: Beide Container laufen im gleichen Docker-Netzwerk und können sich über ihre Servicenamen erreichen.
  • Isolation: Nur nginx ist von aussen erreichbar, die heat-conduction-app bleibt intern.
  • Persistenz: Daten (z.B. SQlite, Konfigurationen, Zertifikate) werden über Volumes ausserhalb des Containers gespeichert.

Der Compose-Ansatz ersetzt komplexe docker run-Aufrufe durch eine versionierbare und reproduzierbare Konfiguration.

(finale) Projektstruktur

Das Ergebnis ist eine Projektstruktur, die klar zwischen Infrastruktur und Anwendung trennt und sich einfach erweitern lässt, z.B. um weitere Services.

project-server
├── docker-compose.yml      → zentrale Steuerung
├── nginx                   → Proxy-Konfiguration und Zertifikate
│    ├── conf.d
│    └── certs
└── services                → Anwendungscontainer
    └── heat-conduction-app

Ergebnis

Nach dem Build und Start über docker compose up -d --build läuft die Anwendung vollständig containerisiert. Nginx nimmt Requests entgegen und leitet sie über Gunicron and die Flask-Anwendung weiter. Die Datenhaltung geschieht über ein persistentes Volume. Das lokale Setup bildet eine stabile, reproduzierbare Grundlage für das spätere Deployment auf einem VPS.

Produktion (VPS + HTTPS + Security)

Im zweiten Schritt wird die Anwendung auf einem öffentlich erreichbaren VPS betrieben und unter einer eigenen Domain bereitgestellt. Das lokale Setup dient dabei als Grundlage, wird jedoch um zwei Aspekte erweitert, das gültige HTTPS-Zertifikat und grundlegende Absicherung des Systems.

Ziel ist kein vollständig gehärtetes Produktionssystem, sondern eine stabile, nachvollziehbare Umgebung für den Betrieb containerisierter Anwendungen.

Minimales Setup (Server, DNS, Docker)

Als Laufzeitumgebung dient ein einfacher VPS bei einem deutschen Hoster. Für die Anwendung ist kein dezidierter Server notwendig, ein Einstiegsmodell ist ausreichend.

Die Basis bildet ein minimal gehärtetes System:

  • SSH-Zugang absichern (kein Root-Login, Key-basierte Authentifizierung)
  • Firewall aktivieren (Freigabe SSH-Port und Port 80, 443)
  • optional: fail2ban zur automatischen Sperrung auffälliger IPs

Diese Maßnahmen reduzieren die Angriffsfläche, ohne den Fokus auf Infrastruktur zu verschieben.

Für die Erreichbarkeit wird eine Subdomain auf die öffentliche IP des VPS gelegt. Die Zuordnung erfolgt über einen DNS-Eintrag beim Domain-Registrar. Sobald die Domain korrekt auflöst, ist der Server unter dieser Adresse erreichbar. Dies lässt sich mit

$ dig heat-conduction.app.meshing-bytes.de

testen.

Als Laufzeitumgebung wird Docker installiert. Die Container entsprechen dem lokalen Setup und können übernommen werden. Ziel ist eine konsistente Umgebung zwischen Entwicklung und Produktion.

HTTPS mit Let's Encrypt (Prinzip + Umsetzung)

Für den öffentlichen Betrieb ist ein gültiges HTTPS-Zertifikat erforderlich. Zum Einsatz kommt Let's Encrypt in Kombination mit certbot.

Das zugrundeliegende Prinzip ist die ACME-Challenge (Automatic Certificate Management Environment):

  • certbot fordert ein Zertifikat für eine Domain an
  • Let`s Encrypt ptüft über HTTP, ob die Domain auf den Server zeigt
  • nginx stellt eine temporäre Challenge-Datei bereit
  • bei erfolgreicher Prüfung wird das Zertifikat ausgestellt

Vereinfachter Ablauf:

Certbot → fordert Zertifikat an
    ↓
Let's Encrypt → prüft Domain über HTTP
    ↓
nginx → liefert Challenge aus
    ↓
Certbot → speichert Zertifikat
    ↓
nginx → nutzt Zertifikat

Die Integration erfolgt ebenfalls über docker compose. Neben nginx und Anwendung wird ein zusätzlicher certbot-Service definiert. Zertifikate und Challenge-Dateien werden über gemeinsame Volumes zwischen nginx und certbot bereitgestellt.

Für die initiale Zertifikatserstellung wird nginx zunächst ohne HTTPS gestartet, sodass ACME -Challenge erreichbar ist. Anschließend erzeugt certbot einmalig das Zertifikat. Danach wird nginx auf HTTPS umgestellt und leitet Requests an die Anwendung weiter.

Wesentliche Punkte:

  • HTTPS wird ausschließlich im Reverse Proxy terminiert
  • die Anwendung selbst bleibt unverändert
  • Zertifikate liegen zentral und werden von nginx genutzt

Zertifikate sind zeitlich begrenzt gültig und müssen regelmäßig erneuert werden. Das Renewal erfolgt automatisert, hier verwende ich einen täglichen Cronjob, der certbot renew ausführt und nginx neu lädt. An dieser Stelle eine pragmatische Lösung, certbot ist in docker-compose integriert, das Renewal erfolgt jedoch als einfacher externer Prozess.

Betrieb (Security, Logging)

Mit der öffentlichen Erreichbarkeit verschiebt sich der Fokus auf Betrieb und Beobachtbarkeit, mit den Themen Security und Logging.

Security

Neben Netzwerk (Firewall) und Zugriff (SSH) habe ich noch folgende Maßnahmen auf Proxy- und Anwendungsebene umgesetzt:

  • HTTPS only (keine unverschlüsselten Zugriffe)
  • Security Header im Reverse Proxy
  • optional: Rate Limiting
  • Input-Validierung auf Anwendungsebene

Dabei schützt der Reverse-Proxy die Anwendung nach aussen, leitet aber alle HTTPS-Requests an die Anwendung weiter, die saubere Verarbeitung von Requests muss daher innerhalb der App durchgeführt werden.

Logging

Die Security-Massnahmen bieten einen Basisschutz, dennoch ist es wichtig die Laufzeitumgebung im Blick zu behalten, hier sind Logs die zentrale Informationsquelle im Betrieb. Mit den drei Ebenen:

  • Web → nginx-Logs: zeigen eingehende Requests und Statuscodes
  • App → Gunicorn/Flask-Logs: liefern Details zu Fehlern innerhalb der Anwendung
  • Zugriff → SSH-Logs: geben Hinweise auf Zugriffsversuche

Die Auswertung erfolgt über docker compose logs und journalctl -u sshd in Verbindung mit grep zur Filterung nach relevanten Ereignissen. Auf der Web und App-Ebene zählen dazu insbesondere:

  • Status Code 200 → gültige Requests
  • Status Code 400 → fehlerhafte Anfragesyntax
  • Status Code 404 → häufig automatisierte Scans (Bots)
  • Status Code 500 → Fehler in der Anwendung, hohe Priorität

Ein kurzer regelmäßiger Blick auf die Logs ist sinnvoll, um Fehler und auffällige Abweichungen früh zu erkennen. Dabei ist ein hoher Anteil an automatisierten Requests und Scanversuchen nicht ungewöhnlich für einen öffentlich erreichbaren Server.

Die resultierende Struktur ergänzt das lokale Setup um produktionsspezifische Komponenten:

project-server/
├── docker-compose.yml
├── logs
│   └── certbot-renew.log
├── nginx
│   ├── conf.d
│   ├── ssl
│   └── www
├── renew_certs.sh
└── services
    └── heat-conduction-app

Fazit & Ausblick

Screenshot der laufenden heat-conduction-app im Browser mit HTTPS und Subdomain auf einem VPS.
Die Anwendung läuft produktiv unter HTTPS auf einem VPS und ist öffentlich erreichbar.

Im Mittelpunkt dieses Artikels stand das Deployment der Anwendung, zunächst lokal mit Fokus auf Containerisierung, anschließend auf einem öffentlich erreichbaren VPS mit HTTPS und grundlegenden Betriebsaspekten.

Zentral ist der Einsatz von Docker bzw. docker-compose zur Orchestrierung. Der führt initial zu mehr Aufwand, Dockerfiles, Compose-Konfiguration, Volumes und das Zusammenspiel der Services müssen sauber aufgebaut und verstanden werden. Dafür erhält man ein gekapseltes, reproduzierbares und wartbares System. Änderungen lassen sich kontrolliert ausrollen, Services gezielt neustarten. Gleichzeitig wird die Infrastruktur als Code dokumentiert (docker-compose.yml, Dockerfile).

Das Deployment erweitert den Blick über die klassische Webentwicklung hinaus. Grundlegende Linux-Kenntnisse (SSH, Logs, Prozesse, Cron) sind notwendig, insbesondere beim Debugging. An dieser Stelle beginnt eine eigene Disziplin.

Ich bin hier kein Server-Administrator, sondern verfolge den Ansatz funktionierende, stabile Systeme auf Basis etablierter Best-Practices. Dazu gehört auch die Erkenntnis, dass öffentlich erreichbare Server permanent automatisiert gescannt werden. Das ist Normalzustand, kein Sonderfall.

Mit der öffentlich erreichbaren Anwendung ist Phase 1 des Referenzprojekts abgeschlossen. Der Prototyp läuft stabil und bildet eine belastbare Grundlage.

Die Umsetzungsschritte zeigen die Bandbreite:

Projektdefinition
    ↓
Projektgerüst
    ↓
Backend + Businesslogik
    ↓
Frontend
    ↓
Persistenzschicht
    ↓
Deployment

Eine zentrale Erkenntnis dieser Umsetzungsschritte, Erweiterbarkeit entsteht durch saubere Trennung der Verantwortlichkeiten. Das erhöht den initialen Aufwand, zahlt sich aber mit wachsender Komplexität aus.

Ausblick

Phase 1 endet mit einem funktionalen Prototyp. Für den produktiven Einsatz fehlen noch zentrale Bausteine:

  • Authentifizierung und Benutzerverwaltung
  • Umstieg auf eine produktive Datenbank
  • API-Schnittstelle
  • Ausbau der Fachlogik und Features

Mit diesen Schritten entwickelt sich die Anwendung von einem Prototypen zu einer produktionsnahen Webanwendung.

Phase 2 und 3 verschieben den Fokus entsprechend weg von Infrastruktur, hin zu Anwendung, Daten und Nutzerinteraktion.

Feedback, Fragen oder Anregungen sind jederzeit willkommen, gerne per Email oder auch auf LinkedIn.