Skip to content

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

  1. Kundenhost: Dedizierter Host pro Kunde

    • Gehostet bei Styrion oder auf Kunden-Proxmox Cluster
    • Läuft auf Proxmox-Virtualisierung
  2. Öffentliche IP: Eine öffentliche IP-Adresse pro Kunde

    • Alle Services laufen über diese IP
    • DNS-Routing über verschiedene Hostnames
  3. 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:

yaml
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 mit traefik.enable=true Label 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.json Volume
  • Certificate Resolver Name: default

Dashboard Security

Basic Authentication für das Traefik Dashboard:

bash
# Passwort-Hash generieren ($ als $$ escapen für docker-compose.yml)
htpasswd -nb admin password
# Output: admin:$apr1$xyz...
# In Vault eintragen

Applikations-Deployment

Network-Architektur

Jede Applikation verwendet zwei Netzwerk-Typen:

  1. proxy Network (external):

    • Verbindet Services mit Traefik
    • Shared zwischen allen Applikationen
    • Muss vor dem ersten App-Deployment existieren
  2. Interne Networks:

    • App-spezifisch (z.B. edbsdb für Datenbank)
    • Isolieren Backend-Services
    • Nicht von außen erreichbar

Beispiel-Applikation: EDBS NG

Eine Multi-Service-Applikation mit Web-Frontend, API, Dokumentation, Datenbank und Administration:

yaml
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:

yaml
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

  1. Restart Policy: Immer restart: unless-stopped verwenden
  2. Pull Policy: pull_policy: always für automatische Updates
  3. Health Checks: Bei Datenbanken immer Health Checks definieren
  4. Depends On: Mit condition: service_healthy auf DB-Verfügbarkeit warten

Netzwerk-Design

  1. Externe Services: Im proxy Network
  2. Datenbanken: Nur in internen Networks
  3. 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:

yaml
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:

yaml
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:

puppet
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:

bash
# 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:

bash
# 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 automatisch

Best Practices

  1. Vault als Single Source of Truth: Alle Secrets nur in Vault pflegen
  2. Puppet für Deployment: Templates und Secret-Injection via Puppet
  3. Niemals Secrets in Git: Weder in Manifests noch in Templates
  4. Hiera für Konfiguration: Customer-spezifische Werte in Hiera
  5. Automatische Updates: Puppet-Agent läuft regelmäßig (Cron)
  6. Vault Policies: Least-Privilege-Access für Puppet-Agents
  7. Audit Logging: Vault Audit Logs für Compliance

Deployment-Workflow für neue Applikationen

  1. Secrets in Vault anlegen: https://vault.iteas.cloud
  2. App in Foreman (https://foreman.iteas.tools) dem Kunden App-Server zuweisen

Troubleshooting

Puppet rendert Template nicht:

bash
# 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:

bash
# 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 --explain

Container startet nach Puppet-Run nicht:

bash
# 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 -f

Backup-Strategie

Für Datenbank-Services immer automatische Backups einrichten:

yaml
pgbackups:
  image: prodrigestivill/postgres-backup-local
  environment:
    - SCHEDULE=@daily
    - BACKUP_KEEP_DAYS=7
    - BACKUP_KEEP_WEEKS=4
    - BACKUP_KEEP_MONTHS=6
  volumes:
    - db_backup:/backups

Backup-Verzeichnisse regelmäßig auf Host-Ebene sichern.

Monitoring

  1. Traefik Dashboard: Überblick über alle Services und Routen
  2. Container Logs: docker compose logs -f <service>
  3. Health Checks: Status mit docker compose ps prüfen

Troubleshooting

Service nicht erreichbar

bash
# 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 Labels

Let's Encrypt Fehler

bash
# 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

bash
# 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:

  1. Phase 1: Traefik parallel zu Apache deployen
  2. Phase 2: Neue Services über Traefik routen
  3. Phase 3: Legacy-Services via TLS SNI durch Traefik proxyen
  4. Phase 4: Schrittweise Migration zu Container-basiert

Dynamische Traefik-Konfiguration für Legacy-Support in /opt/traefik/dynamic/:

yaml
# 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"

Weitere Ressourcen

Iteas Tools Integration Platform Version v1.0.21

Version: v1.0.21 Version: v1.0.21
Commit: 7a0e1c11
Deployed at: 2026-09-24T13:56:52Z