Docker Containerization & Deployment
Überblick
Unsere Infrastruktur nutzt Docker Compose für das Deployment von Applikationen. Die Architektur basiert auf dedizierten Kundenhosts mit Traefik als zentralem Reverse Proxy für moderne containerisierte Anwendungen sowie Legacy-Applikationen.
Infrastruktur-Architektur
Host-Struktur
Kundenhost: Dedizierter Host pro Kunde
- Gehostet bei Styrion oder auf Kunden-Proxmox Cluster
- Läuft auf Proxmox-Virtualisierung
Öffentliche IP: Eine öffentliche IP-Adresse pro Kunde
- Alle Services laufen über diese IP
- DNS-Routing über verschiedene Hostnames
Traefik als zentraler Proxy:
- Läuft in separatem Docker Container
- Managed sowohl Legacy- als auch Container-Applikationen
- Automatisches TLS-Zertifikat-Management via Let's Encrypt
Traefik-Architektur
Traefik fungiert als intelligenter Reverse Proxy mit zwei Hauptfunktionen:
- Legacy-Support: TLS SNI-basierte Weiterleitung an Apache auf dem Host
- Container-Routing: Automatisches Service Discovery für Docker Container
┌─────────────────┐
│ Öffentliche │
│ IP │
└────────┬────────┘
│
│
┌────────▼────────┐
│ Traefik │
│ (Container) │
└────┬───────┬────┘
│ │
┌────────────┘ └────────────┐
│ │
┌────────▼─────────┐ ┌──────────▼──────────┐
│ Legacy Apache │ │ Docker Container │
│ (TLS SNI) │ │ Applications │
│ auf Host │ │ (Service Disc.) │
└──────────────────┘ └─────────────────────┘Traefik-Konfiguration
Docker Compose Setup
Die Traefik-Instanz wird mit folgendem docker-compose.yml deployed:
name: 'traefik'
services:
traefik:
image: traefik:v3.5
command:
# API & Dashboard
- "--api.insecure=false"
- "--api.dashboard=true"
# Provider Configuration
- "--providers.docker=true"
- "--providers.docker.network=proxy"
- "--providers.docker.exposedbydefault=false"
- "--providers.file.directory=/etc/traefik/dynamic"
- "--providers.file.watch=true"
# Logging
- "--log.level=INFO"
- "--accessLog=true"
# Entrypoints
- "--entrypoints.http.address=:80"
- "--entryPoints.http.http.redirections.entryPoint.to=https"
- "--entryPoints.http.http.redirections.entryPoint.scheme=https"
- "--entrypoints.https.address=:443"
# Let's Encrypt
- "--certificatesResolvers.default.acme.email=hostmaster@iteas.at"
- "--certificatesResolvers.default.acme.storage=/letsencrypt/acme.json"
- "--certificatesResolvers.default.acme.httpChallenge.entryPoint=http"
ports:
- "80:80"
- "443:443"
- "8080:8080"
networks:
- proxy
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- traefik_acme:/letsencrypt
- "/opt/traefik/dynamic:/etc/traefik/dynamic"
labels:
- "traefik.enable=true"
# Basic Auth für Dashboard
# WICHTIG: $ muss als $$ escaped werden in docker-compose.yml
- 'traefik.http.middlewares.api-auth.basicauth.users=<%=@traefik_dashboard_auth%>'
# Dashboard Router
- "traefik.http.routers.api.rule=Host(`<%=@traefik_url%>`)"
- "traefik.http.routers.api.service=api@internal"
- "traefik.http.routers.api.middlewares=api-auth"
- "traefik.http.routers.api.tls=true"
- "traefik.http.routers.api.tls.certresolver=default"
networks:
proxy:
name: proxy
volumes:
traefik_acme:Wichtige Konfigurationsdetails
Provider
- Docker Provider: Automatisches Service Discovery via Docker Socket
- File Provider: Dynamische Konfiguration für Legacy-Services (TLS SNI)
exposedbydefault=false: Services müssen explizit mittraefik.enable=trueLabel aktiviert werden
Entrypoints
- HTTP (Port 80): Automatische Weiterleitung zu HTTPS
- HTTPS (Port 443): Hauptentrypoint für alle Services + Traefik Web UI (passwortgeschützt)
TLS-Zertifikate
- Automatische Let's Encrypt-Zertifikate via HTTP Challenge
- Storage in
/letsencrypt/acme.jsonVolume - Certificate Resolver Name:
default
Dashboard Security
Basic Authentication für das Traefik Dashboard:
# Passwort-Hash generieren ($ als $$ escapen für docker-compose.yml)
htpasswd -nb admin password
# Output: admin:$apr1$xyz...
# In Vault eintragenApplikations-Deployment
Network-Architektur
Jede Applikation verwendet zwei Netzwerk-Typen:
proxyNetwork (external):- Verbindet Services mit Traefik
- Shared zwischen allen Applikationen
- Muss vor dem ersten App-Deployment existieren
Interne Networks:
- App-spezifisch (z.B.
edbsdbfür Datenbank) - Isolieren Backend-Services
- Nicht von außen erreichbar
- App-spezifisch (z.B.
Beispiel-Applikation: EDBS NG
Eine Multi-Service-Applikation mit Web-Frontend, API, Dokumentation, Datenbank und Administration:
name: 'edbs-ng-debug'
services:
# Web-Applikation
webapp:
image: git.styrion.net:4567/iteas/edbs-ng/edbs-ng-webapp-dev
pull_policy: always
restart: unless-stopped
environment:
ASPNETCORE_ENVIRONMENT: Production
ports:
- '8081:8080'
networks:
- proxy
- edbsdb
depends_on:
db:
condition: service_healthy
labels:
- "traefik.enable=true"
- "traefik.http.routers.edbs_ng_webapp.rule=Host(`<%=@hostname_webapp%>`)"
- "traefik.http.routers.edbs_ng_webapp.tls.certresolver=default"
- "traefik.http.services.edbs_ng_webapp.loadbalancer.server.port=8080"
entrypoint: /bin/sh
command:
- -c
- |
./efbundle-migrate-core
./efbundle-migrate-protect
dotnet EDBS_NG.WebApp.dll
# API-Service
api:
image: git.styrion.net:4567/iteas/edbs-ng/edbs-ng-api-dev
pull_policy: always
restart: unless-stopped
environment:
ASPNETCORE_ENVIRONMENT: Production
ports:
- '8082:8080'
networks:
- edbsdb
- proxy
depends_on:
db:
condition: service_healthy
labels:
- "traefik.enable=true"
- "traefik.http.routers.edbs_ng_api.rule=Host(`<%=@hostname_api%>`)"
- "traefik.http.routers.edbs_ng_api.tls.certresolver=default"
- "traefik.http.services.edbs_ng_api.loadbalancer.server.port=8080"
entrypoint: /bin/sh
command:
- -c
- |
dotnet EDBS_NG.Api.dll
# Dokumentation
docs:
image: git.styrion.net:4567/iteas/edbs-ng/edbs-ng-docs-dev
pull_policy: always
restart: unless-stopped
environment:
ASPNETCORE_ENVIRONMENT: Production
ports:
- '8083:80'
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.edbs_ng_docs.rule=Host(`<%=@hostname_docs%>`)"
- "traefik.http.routers.edbs_ng_docs.tls.certresolver=default"
- "traefik.http.services.edbs_ng_docs.loadbalancer.server.port=80"
# PostgreSQL Datenbank
db:
image: postgres:18
restart: unless-stopped
ports:
- '5432:5432'
networks:
- edbsdb
volumes:
- db_data:/var/lib/postgresql
environment:
POSTGRES_PASSWORD: hftsWqylvDUV7Df9
POSTGRES_DB: edbs_ng
POSTGRES_USER: edbs_ng
healthcheck:
test: ["CMD-SHELL", "pg_isready", "-d", "edbs_ng"]
interval: 2s
timeout: 20s
retries: 10
# pgAdmin für Datenbank-Administration
pgadmin:
image: dpage/pgadmin4:9.9
restart: unless-stopped
environment:
PGADMIN_DEFAULT_EMAIL: pgadmin@iteas.at
PGADMIN_DEFAULT_PASSWORD: PbdByEUy65uCQnfnZmV1
networks:
- proxy
- edbsdb
labels:
- "traefik.enable=true"
- "traefik.http.routers.edbs_ng_pgadmin.rule=Host(`db.app.iteas.cloud`)"
- "traefik.http.routers.edbs_ng_pgadmin.tls.certresolver=default"
- "traefik.http.services.edbs_ng_pgadmin.loadbalancer.server.port=80"
# Automatische Datenbank-Backups
pgbackups:
image: prodrigestivill/postgres-backup-local
restart: unless-stopped
volumes:
- db_backup:/backups
depends_on:
db:
condition: service_healthy
networks:
- edbsdb
environment:
- POSTGRES_HOST=db.edbsdb
- POSTGRES_DB=edbs_ng
- POSTGRES_USER=edbs_ng
- POSTGRES_PASSWORD=hftsWqylvDUV7Df9
- POSTGRES_EXTRA_OPTS=-Z1 --schema=public --blobs
- SCHEDULE=@daily
- BACKUP_ON_START=TRUE
- BACKUP_KEEP_DAYS=7
- BACKUP_KEEP_WEEKS=4
- BACKUP_KEEP_MONTHS=6
- HEALTHCHECK_PORT=8080
networks:
proxy:
name: proxy
external: true
edbsdb:
name: edbsdb
volumes:
db_data:
db_backup:Traefik-Labels Erklärung
Jeder Service, der über Traefik erreichbar sein soll, benötigt folgende Labels:
labels:
# Service für Traefik aktivieren
- "traefik.enable=true"
# Routing-Regel (Hostname-basiert)
- "traefik.http.routers.<router-name>.rule=Host(`example.com`)"
# TLS aktivieren mit Let's Encrypt
- "traefik.http.routers.<router-name>.tls.certresolver=default"
# Port des Container-Services (wenn != 80/443)
- "traefik.http.services.<service-name>.loadbalancer.server.port=8080"Wichtig:
<router-name>und<service-name>sollten eindeutig sein- Bei mehreren Services pro Container muss der Port explizit angegeben werden
- Hostname muss via DNS auf die öffentliche IP des Hosts zeigen
Deployment-Workflow
1. Vorbereitung
2. Applikation deployen
3. Verifizierung
Best Practices
Service-Konfiguration
- Restart Policy: Immer
restart: unless-stoppedverwenden - Pull Policy:
pull_policy: alwaysfür automatische Updates - Health Checks: Bei Datenbanken immer Health Checks definieren
- Depends On: Mit
condition: service_healthyauf DB-Verfügbarkeit warten
Netzwerk-Design
- Externe Services: Im
proxyNetwork - Datenbanken: Nur in internen Networks
- Port-Bindings: Für Entwicklung/Debugging, nicht für Produktion erforderlich
Secrets & Credentials Management
WICHTIG: Alle Secrets, Passwörter und Passwort-Hashes werden zentral in HashiCorp Vault verwaltet und automatisch via Puppet in die Docker Compose Files injiziert.
Vault + Puppet Workflow
Unser Secret-Management-Workflow basiert auf folgender Architektur:
┌──────────────┐
│ HashiCorp │
│ Vault │ ◄─── Zentrale Secret-Verwaltung
└──────┬───────┘
│
│ Vault Lookup
│
┌──────▼───────┐
│ Puppet │ ◄─── Konfigurationsmanagement
│ Server │ + Secret-Injection
└──────┬───────┘
│
│ Deploy Templates
│
┌──────▼────────────────────────┐
│ Kundenhost │
│ ┌─────────────────────────┐ │
│ │ docker-compose.yml │ │ ◄─── Secrets als Variablen injiziert
│ │ (aus Puppet Template) │ │
│ └─────────────────────────┘ │
└───────────────────────────────┘Puppet-Templates mit Vault-Integration
Puppet rendert die Docker Compose Files aus Templates (.erb) und injiziert dabei Secrets aus Vault als Variablen.
Beispiel aus dem Traefik-Compose-File:
labels:
# Variable wird von Puppet aus Vault injiziert
- 'traefik.http.middlewares.api-auth.basicauth.users=<%=@traefik_dashboard_auth%>'
# Hostname aus Puppet-Variable
- "traefik.http.routers.api.rule=Host(`<%=@traefik_url%>`)"Beispiel aus dem EDBS NG Compose-File:
labels:
# Hostnames werden von Puppet verwaltet
- "traefik.http.routers.edbs_ng_webapp.rule=Host(`<%=@hostname_webapp%>`)"
- "traefik.http.routers.edbs_ng_api.rule=Host(`<%=@hostname_api%>`)"
- "traefik.http.routers.edbs_ng_docs.rule=Host(`<%=@hostname_docs%>`)"Die <%= %> Syntax ist ERB (Embedded Ruby) und wird von Puppet beim Deployment durch die tatsächlichen Werte ersetzt.
Puppet Manifest Beispiel
Puppet-Klasse für Docker-Compose-Deployment:
class profiles::docker::edbs_ng (
String $hostname_webapp,
String $hostname_api,
String $hostname_docs,
String $db_password,
String $pgadmin_password,
) {
# Secrets aus Vault laden (via hiera-eyaml-vault)
$vault_db_password = lookup('profiles::docker::edbs_ng::db_password')
$vault_pgadmin_password = lookup('profiles::docker::edbs_ng::pgadmin_password')
# Docker Compose aus Template generieren
file { '/opt/applications/edbs-ng/docker-compose.yml':
ensure => file,
content => epp('profiles/docker/edbs-ng/docker-compose.yml.epp', {
'hostname_webapp' => $hostname_webapp,
'hostname_api' => $hostname_api,
'hostname_docs' => $hostname_docs,
'db_password' => $vault_db_password,
'pgadmin_password' => $vault_pgadmin_password,
}),
notify => Exec['docker-compose-up-edbs-ng'],
}
# Container starten/aktualisieren bei Änderungen
exec { 'docker-compose-up-edbs-ng':
command => 'docker compose up -d',
cwd => '/opt/applications/edbs-ng',
path => ['/usr/bin', '/usr/local/bin'],
refreshonly => true,
}
}Secret-Rotation via Puppet + Vault
Workflow für das Rotieren von Secrets:
# 1. Neues Passwort in Vault speichern
vault kv put secret/customers/acme/edbs-ng/db_password \
password="$(openssl rand -base64 32)"
# 2. Puppet-Run triggern (automatisch oder manuell)
puppet agent --test
# 3. Puppet führt aus:
# - Template mit neuem Secret rendern
# - Docker Compose File aktualisieren
# - Container neu starten (via notify)Passwort-Hashes für Basic Auth
Für Traefik Basic Authentication:
# 1. Hash generieren
htpasswd -nb admin 'SecurePassword123!'
# Output: admin:$apr1$xyz...
# 2. In Vault speichern ($ wird nicht escaped in Vault!)
vault kv put secret/customers/acme/traefik \
dashboard_auth='admin:$apr1$xyz...'
# 3. Puppet Template escaped automatisch für Docker Compose
# In .erb Template:
- 'traefik.http.middlewares.api-auth.basicauth.users=<%= @dashboard_auth %>'
# Puppet escaped $ zu $$ für Docker Compose automatischBest Practices
- Vault als Single Source of Truth: Alle Secrets nur in Vault pflegen
- Puppet für Deployment: Templates und Secret-Injection via Puppet
- Niemals Secrets in Git: Weder in Manifests noch in Templates
- Hiera für Konfiguration: Customer-spezifische Werte in Hiera
- Automatische Updates: Puppet-Agent läuft regelmäßig (Cron)
- Vault Policies: Least-Privilege-Access für Puppet-Agents
- Audit Logging: Vault Audit Logs für Compliance
Deployment-Workflow für neue Applikationen
- Secrets in Vault anlegen: https://vault.iteas.cloud
- App in Foreman (https://foreman.iteas.tools) dem Kunden App-Server zuweisen
Troubleshooting
Puppet rendert Template nicht:
# Puppet-Run im Debug-Modus
puppet agent --test --debug
# Template manuell testen
puppet apply --execute "notice(epp('profiles/docker/app/docker-compose.yml.epp', {...}))"Vault-Secret nicht verfügbar:
# Vault-Token prüfen
vault token lookup
# Secret manuell abrufen
vault kv get secret/customers/acme/app/secret_name
# Hiera-Lookup testen
puppet lookup profiles::docker::app::db_password --explainContainer startet nach Puppet-Run nicht:
# Docker Compose File prüfen
cat /opt/applications/app/docker-compose.yml
# Manuell starten für detaillierte Fehler
cd /opt/applications/app
docker compose up
# Puppet-Notification prüfen
journalctl -u puppet -fBackup-Strategie
Für Datenbank-Services immer automatische Backups einrichten:
pgbackups:
image: prodrigestivill/postgres-backup-local
environment:
- SCHEDULE=@daily
- BACKUP_KEEP_DAYS=7
- BACKUP_KEEP_WEEKS=4
- BACKUP_KEEP_MONTHS=6
volumes:
- db_backup:/backupsBackup-Verzeichnisse regelmäßig auf Host-Ebene sichern.
Monitoring
- Traefik Dashboard: Überblick über alle Services und Routen
- Container Logs:
docker compose logs -f <service> - Health Checks: Status mit
docker compose psprüfen
Troubleshooting
Service nicht erreichbar
# 1. Container läuft?
docker compose ps
# 2. Traefik erkennt Service?
# Dashboard prüfen: https://<traefik-url>:8080
# 3. Logs prüfen
docker compose logs traefik
docker compose logs <service>
# 4. DNS korrekt?
nslookup app.example.com
# 5. Labels korrekt?
docker inspect <container> | grep -A 20 LabelsLet's Encrypt Fehler
# ACME-Log prüfen
docker compose -f /opt/traefik/docker-compose.yml logs traefik | grep acme
# Häufige Ursachen:
# - Port 80 nicht erreichbar (HTTP Challenge)
# - DNS zeigt nicht auf Host
# - Rate Limits (5 Zertifikate pro Woche pro Domain)Datenbank-Probleme
# Health Check Status
docker compose ps db
# Datenbank-Logs
docker compose logs db
# Manuelle Verbindung testen
docker compose exec db psql -U <user> -d <database>Migration von Legacy-Systemen
Für die schrittweise Migration von Apache-basierten Legacy-Applikationen:
- Phase 1: Traefik parallel zu Apache deployen
- Phase 2: Neue Services über Traefik routen
- Phase 3: Legacy-Services via TLS SNI durch Traefik proxyen
- Phase 4: Schrittweise Migration zu Container-basiert
Dynamische Traefik-Konfiguration für Legacy-Support in /opt/traefik/dynamic/:
# traefik/dynamic/apache.yml
tcp:
routers:
apache-passthrough:
# This rule is key: It specifically matches the domain for Apache.
rule: "HostSNI(`apache.app.iteas.cloud`) || HostSNI(`apache2.app.iteas.cloud`)"
service: apache-ssl-service
entryPoints:
- https
tls:
passthrough: true # Enable passthrough mode
services:
apache-ssl-service:
loadBalancer:
servers:
# Forward to the host's HTTPS port
- address: "77.235.84.15:8443"