Kostenvergleich Apps Preise Blog Termin buchen

Symfony deployen ohne DevOps-Team: Docker, CI/CD und PHP-FPM-Tuning für Agenturen

Symfony deployen ohne DevOps-Team: Docker, CI/CD und PHP-FPM-Tuning für Agenturen

Ihre Agentur hat eine großartige Symfony-Anwendung gebaut. Die Features stehen, die Tests sind grün, der Kunde ist zufrieden. Und dann kommt die Frage: Wie kommt das Ding auf einen Server? Plötzlich geht es nicht mehr um Twig-Templates und Doctrine-Entities, sondern um Nginx-Konfigurationen, PHP-FPM-Pools und SSL-Zertifikate.

Für viele Agenturen ist genau dieser Übergang vom Entwicklungsprojekt zum produktiven Betrieb der schmerzhafte Bruch. Die Entwickler können hervorragend Code schreiben, aber Server-Administration ist nicht ihr Kerngeschäft. In diesem Artikel zeigen wir, wie Sie Symfony-Anwendungen mit Docker produktionstauglich deployen — und warum ein Managed Symfony Hosting für die meisten Agenturen der wirtschaftlichere Weg ist.

Der aktuelle Symfony-Stack: Was Sie 2025/2026 wissen müssen

Symfony 7.4 LTS ist die aktuelle Langzeit-Support-Version (Release November 2025). Sie erfordert mindestens PHP 8.2, empfohlen wird PHP 8.3 oder 8.4. Bugfixes gibt es bis November 2028, Sicherheits-Updates bis November 2029. Für neue Projekte ist Symfony 7.4 LTS die richtige Wahl — maximale Stabilität bei aktuellem Feature-Set.

Parallel dazu existiert Symfony 8.0 für Early Adopter, das PHP 8.4 voraussetzt. Für produktive Agentur-Projekte empfehlen wir, bei der LTS-Version zu bleiben.

Der Software-Stack im Überblick

  • PHP 8.3 oder 8.4 mit Extensions: pdo, pdo_mysql, intl, opcache, mbstring, gd, zip, exif, sodium
  • Composer 2 für Dependency-Management
  • Webserver: Nginx + PHP-FPM (klassisch) oder FrankenPHP (modern)
  • Datenbank: MySQL 8.0/8.4, MariaDB 11 oder PostgreSQL 16
  • Cache: Redis oder APCu für Session- und Application-Cache
  • Asset Mapper (kein Node.js mehr nötig) oder Webpack Encore für Legacy-Projekte

Docker-Setup: FrankenPHP vs. PHP-FPM + Nginx

Beim Docker-Setup für Symfony stehen zwei Architekturansätze zur Wahl. Beide haben ihre Berechtigung — die Wahl hängt von Ihrem Projekt und Ihrer Erfahrung ab.

FrankenPHP: Der neue Standard

Das offizielle Symfony-Docker-Projekt (dunglas/symfony-docker) setzt seit 2024 auf FrankenPHP — einen in Go geschriebenen PHP-Application-Server, der auf dem Caddy-Webserver basiert. FrankenPHP vereint Webserver und PHP-Runtime in einem einzigen Prozess:

  • Ein Container statt zwei: Kein separater Nginx-Container, kein FastCGI-Proxy. Weniger Komplexität, weniger Fehlerquellen.
  • Automatisches HTTPS: Let’s-Encrypt-Zertifikate werden ohne zusätzliche Konfiguration erstellt und erneuert.
  • HTTP/2 und HTTP/3: Nativ unterstützt, ohne Nginx-Konfiguration.
  • Worker Mode: Die Symfony-Anwendung bleibt zwischen Requests im Speicher. Kein erneutes Bootstrapping bei jedem Request — das bedeutet deutlich schnellere Antwortzeiten.

Ein neues Symfony-Projekt mit FrankenPHP starten Sie mit:

symfony new my-project --docker
# oder direkt:
composer create-project symfony/skeleton my-project
cd my-project && composer require webapp

PHP-FPM + Nginx: Der klassische Weg

Für bestehende Projekte und Teams mit Nginx-Erfahrung bleibt der klassische Stack eine solide Wahl. PHP-FPM lauscht auf Port 9000, Nginx leitet Requests per FastCGI weiter:

# docker-compose.yml
services:
  php:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - ./:/app
    environment:
      APP_ENV: prod

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./:/app:ro
      - ./docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - php

  database:
    image: mariadb:11
    environment:
      MARIADB_ROOT_PASSWORD: secret
      MARIADB_DATABASE: app
    volumes:
      - db-data:/var/lib/mysql

  redis:
    image: redis:7-alpine

volumes:
  db-data:

Das zugehörige Dockerfile für den PHP-Container:

FROM php:8.3-fpm-alpine

RUN apk add --no-cache icu-dev libzip-dev 
    && docker-php-ext-install pdo_mysql intl opcache zip

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

WORKDIR /app
COPY . .

RUN APP_ENV=prod composer install --no-dev --no-scripts --prefer-dist --no-interaction 
    && composer dump-autoload --optimize --classmap-authoritative --no-dev 
    && php bin/console cache:warmup --env=prod 
    && chown -R www-data:www-data var/cache var/log

USER www-data

Unsere Empfehlung: FrankenPHP für neue Projekte, PHP-FPM + Nginx für bestehende Setups und Teams die den klassischen Stack beherrschen.

PHP-FPM-Tuning: Die Parameter, die wirklich zählen

Ein falsch konfigurierter PHP-FPM-Pool ist der häufigste Grund für langsame Symfony-Anwendungen in Produktion. Die drei Process-Manager-Modi haben unterschiedliche Einsatzgebiete:

  • static: Feste Anzahl Worker-Prozesse. Kein Fork-Overhead, sofort bereit. Ideal wenn der Server ausschließlich für die Symfony-App läuft.
  • dynamic: Skaliert zwischen Minimum und Maximum. Guter Kompromiss für die meisten Produktions-Setups.
  • ondemand: Forkt erst bei eingehendem Request. Minimaler Idle-Verbrauch, aber langsamer erster Request.

Empfohlene Produktionskonfiguration

# php-fpm.d/www.conf
pm = dynamic
pm.max_children = 60
pm.start_servers = 8
pm.min_spare_servers = 8
pm.max_spare_servers = 20
pm.max_requests = 500

Die entscheidende Berechnung: pm.max_children = verfügbarer RAM / RAM pro PHP-Prozess. Ein Symfony-Prozess verbraucht typischerweise 80 bis 120 MB. Bei einem Server mit 8 GB RAM, wovon 2 GB für Betriebssystem, Datenbank und Redis reserviert sind, bleiben 6 GB für PHP — also etwa 60 Worker.

OPcache: Der größte Performance-Hebel

OPcache speichert kompilierten PHP-Bytecode im Shared Memory. In Produktion ist eine korrekte OPcache-Konfiguration der wichtigste Performance-Faktor:

# php.ini
opcache.enable = 1
opcache.memory_consumption = 256
opcache.max_accelerated_files = 32531
opcache.validate_timestamps = 0
opcache.preload = /app/config/preload.php
opcache.preload_user = www-data

Kritisch: validate_timestamps = 0 in Produktion. Damit prüft PHP nicht bei jedem Request, ob sich Dateien geändert haben. Das bringt messbare Performance-Gewinne — aber nach einem Deployment muss der PHP-FPM-Pool neu gestartet werden, damit neuer Code geladen wird.

Die Preloading-Funktion geht noch weiter: Symfony kann häufig verwendete Klassen beim FPM-Start in den Shared Memory laden. Das eliminiert File-I/O für den Autoloader bei jedem Request.

CI/CD-Pipeline: Vom Git Push zum Deployment

Eine automatisierte Pipeline nimmt Ihrem Team den manuellen Deployment-Prozess ab. Das typische Muster mit GitHub Actions:

# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          extensions: intl, pdo_mysql, opcache
      - run: composer install --no-dev --prefer-dist
      - run: php bin/phpstan analyse
      - run: php bin/phpunit
      - run: symfony security:check

  build-and-deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build Docker Image
        run: docker build -t myapp:${{ github.sha }} .
      - name: Push to Registry
        run: |
          echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
          docker tag myapp:${{ github.sha }} ghcr.io/myorg/myapp:latest
          docker push ghcr.io/myorg/myapp:latest
      - name: Deploy
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: deploy
          key: ${{ secrets.SSH_KEY }}
          script: |
            cd /opt/myapp
            docker compose pull
            docker compose up -d
            docker compose exec -T php php bin/console doctrine:migrations:migrate --no-interaction

Die Pipeline durchläuft vier Schritte: Code prüfen (PHPStan + PHPUnit), Docker-Image bauen, in eine Registry pushen, auf dem Server deployen. Der gesamte Prozess dauert typischerweise 3 bis 5 Minuten.

Doctrine-Migrations im Deployment

Datenbankmigrationen sind der heikelste Teil des Deployments. Wichtige Regeln:

  • Migrationen VOR dem Code-Switch ausführen: Das Datenbankschema muss bereit sein, bevor die neue Version Traffic bekommt.
  • Rückwärtskompatibilität: Jede Migration muss mit der alten UND neuen Codeversion funktionieren. Das bedeutet: Spalten erst im nächsten Release löschen, nicht im selben.
  • --no-interaction ist Pflicht: Ohne dieses Flag wartet Doctrine auf manuelle Bestätigung — in einer Pipeline ein Deadlock.
  • Migrationen nur in Produktion ausführen, nie erstellen: doctrine:migrations:diff gehört in die Entwicklung, doctrine:migrations:migrate in die Pipeline.

Asset Mapper: Kein Node.js mehr nötig

Eine der erfreulichsten Neuerungen seit Symfony 6.3: Der Asset Mapper ersetzt Webpack Encore für die meisten Projekte. Statt eines Node.js-Build-Schritts mit npm, Webpack und Babel nutzt Asset Mapper native Browser-Features (ES Modules, Import Maps).

Was das für Ihr Deployment bedeutet:

  • Kein Node.js auf dem Server: Eine Abhängigkeit weniger. Kein npm install, kein yarn build, kein Webpack.
  • Schnelleres Deployment: Der Asset-Build entfällt komplett. Symfony verwaltet JavaScript-Imports über Import Maps.
  • Weniger Docker-Image-Größe: Kein Node.js-Layer im Image spart 200 bis 400 MB.

Webpack Encore bleibt weiterhin verfügbar für Projekte, die React, Vue oder andere Frameworks mit Build-Step benötigen. Aber für klassische Symfony-Anwendungen mit Twig-Templates ist Asset Mapper die zeitgemäße Wahl.

Secrets und Umgebungsvariablen richtig handhaben

Symfony bietet mit dem Secrets Vault eine integrierte Lösung für sensitive Konfiguration. Pro Environment existiert ein eigener Vault mit verschlüsselten Key-Value-Paaren:

# Secret erstellen
php bin/console secrets:set DATABASE_URL --env=prod

# Secrets für Deployment in lokale Datei entschlüsseln
php bin/console secrets:decrypt-to-local --env=prod

Die Best Practices für Umgebungsvariablen:

  • .env wird committet — enthält nur Default- und Entwicklungswerte.
  • .env.local für lokale Overrides — wird nicht committet.
  • .env.prod für Production-Defaults — wird committet.
  • Sensitive Werte kommen über CI/CD-Secrets oder Docker Secrets auf den Server. Nie ins Repository, nie ins Docker-Image.

Ein häufiger Fehler: DATABASE_URL im Docker-Image hartcodieren. Was im Entwicklungs-Container praktisch erscheint, ist in Produktion ein Sicherheitsrisiko — jeder mit Zugriff auf das Image sieht die Zugangsdaten.

Die 10 häufigsten Deployment-Fehler

1. APP_ENV=dev in Produktion

Der Debug-Modus zeigt detaillierte Fehlermeldungen mit Stack Traces, Datenbankabfragen und Konfigurationswerten. Ein Sicherheits-Albtraum. Prüfen Sie nach jedem Deployment, dass APP_ENV=prod und APP_DEBUG=0 gesetzt sind.

2. Container als Root laufen lassen

Ohne explizite USER-Anweisung im Dockerfile laufen PHP-Prozesse als Root. Wenn ein Angreifer eine Schwachstelle ausnutzt, hat er Root-Zugriff im Container — und möglicherweise darüber hinaus.

3. OPcache validate_timestamps=1

Bei jedem Request prüft PHP, ob sich Dateien auf der Festplatte geändert haben. In Produktion ändert sich Code nur beim Deployment — die Prüfung ist reiner Overhead. Auf 0 setzen und nach dem Deployment PHP-FPM neustarten.

4. Keine Healthchecks

Ein Container kann “Running” sein, obwohl PHP-FPM intern tot ist. Docker Healthchecks erkennen das und starten den Container automatisch neu:

healthcheck:
  test: ["CMD-SHELL", "curl -f http://localhost/healthz || exit 1"]
  interval: 30s
  timeout: 5s
  retries: 3

5. TRUSTED_PROXIES nicht gesetzt

Hinter einem Reverse Proxy (Traefik, Nginx, Cloudflare) bekommt Symfony die IP des Proxys statt des Clients. TRUSTED_PROXIES=REMOTE_ADDR in der .env löst das Problem.

6. Migrations nach dem Code-Switch

Die neue Codeversion erwartet Datenbank-Spalten, die noch nicht existieren. Ergebnis: 500er-Fehler für alle Benutzer. Migrationen immer vor dem Container-Switch ausführen.

7. Dev-Dependencies in Produktion

composer install ohne --no-dev installiert PHPUnit, PHPStan und andere Entwicklungstools. Das vergrößert das Image, verlangsamt den Autoloader und kann Sicherheitsrisiken einführen.

8. Kein –no-scripts bei Composer

Composer-Scripts können bei install automatisch ausgeführt werden. In einer CI/CD-Pipeline können diese Scripts fehlschlagen, weil die Datenbank noch nicht verfügbar ist. Besser: Scripts explizit im Dockerfile aufrufen.

9. Fehlende Security-Header

Content-Security-Policy, Strict-Transport-Security, X-Frame-Options — diese Header fehlen in der Nginx-Konfiguration, wenn man sie nicht explizit hinzufügt. Symfony’s Security-Bundle hilft bei einigen, aber die Webserver-Konfiguration muss stimmen.

10. SSL-Zertifikate manuell verwalten

Let’s Encrypt mit Certbot manuell einrichten und Renewal-Cronjobs pflegen ist fehleranfällig. Traefik oder Caddy (mit FrankenPHP) übernehmen das vollautomatisch — eine Sorge weniger.

Reverse Proxy mit Traefik

Für Produktions-Deployments mit PHP-FPM + Nginx empfehlen wir Traefik als Reverse Proxy. Labels in der docker-compose.yml reichen für die Konfiguration:

nginx:
  labels:
    - "traefik.enable=true"
    - "traefik.http.routers.myapp.rule=Host(`app.example.com`)"
    - "traefik.http.routers.myapp.entrypoints=websecure"
    - "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
    - "traefik.http.services.myapp.loadbalancer.server.port=80"
  networks:
    - traefik-proxy
    - default

Traefik erkennt neue Container automatisch, fordert SSL-Zertifikate an und leitet Traffic an den richtigen Service weiter. Kein manuelles Nginx-Conf-Editieren, kein Certbot-Cronjob.

Messenger-Worker: Hintergrundaufgaben richtig deployen

Viele Symfony-Anwendungen nutzen den Symfony Messenger für asynchrone Aufgaben: E-Mails versenden, Bilder verarbeiten, Reports generieren, API-Synchronisationen. Im Entwicklungsmodus werden diese Jobs oft synchron abgearbeitet — in Produktion müssen dedizierte Worker-Prozesse laufen.

# docker-compose.yml — dedizierter Worker-Container
worker:
  build:
    context: .
    dockerfile: Dockerfile
  command: php bin/console messenger:consume async --time-limit=3600 --memory-limit=256M -vv
  restart: unless-stopped
  depends_on:
    - database
    - redis
  environment:
    APP_ENV: prod

Die Parameter --time-limit und --memory-limit sind in Produktion unverzichtbar. Sie bewirken, dass der Worker sich nach einer Stunde oder bei 256 MB Speicherverbrauch selbst beendet und durch Docker (restart: unless-stopped) neu gestartet wird. Das verhindert Memory-Leaks und stellt sicher, dass nach einem Deployment neuer Code geladen wird.

Für Anwendungen mit hohem Durchsatz können Sie mehrere Worker-Instanzen parallel laufen lassen:

worker:
  deploy:
    replicas: 3

Oder in Docker Compose ohne Swarm-Mode über ein Scaling-Kommando:

docker compose up -d --scale worker=3

Ein oft übersehener Punkt: Worker müssen im Deployment-Prozess neu gestartet werden. Wenn Sie neuen Code deployen, läuft der alte Worker weiter mit dem alten Code im Speicher. Die Pipeline muss den Worker-Container explizit neu starten oder das Time-Limit so wählen, dass Worker regelmäßig recyclen.

Logging und Debugging in Produktion

Symfony’s Monolog-Bundle schreibt Logs standardmäßig nach var/log/. In einer Docker-Umgebung ist das suboptimal — Container sind ephemeral, Logs gehen beim Neustart verloren. Die bessere Strategie: Logs nach STDOUT/STDERR schreiben und Docker die Log-Aggregation überlassen.

# config/packages/monolog.yaml (prod)
monolog:
  handlers:
    main:
      type: stream
      path: "php://stderr"
      level: error
    console:
      type: console
      process_psr_3_messages: false

Damit landen Fehler in docker compose logs php und können von Log-Aggregation-Tools (Loki, ELK, Papertrail) abgeholt werden. Für eine detailliertere Übersicht empfehlen wir unseren Artikel über Monitoring für Self-Hosted-Stacks.

Debugging ohne var_dump

In Produktion fehlen Ihnen die Symfony-Toolbar und der Profiler. Einige Alternativen:

  • Structured Logging: Kontext-Informationen in Log-Nachrichten mitgeben ($logger->error('Payment failed', ['order_id' => $id, 'amount' => $amount])).
  • Sentry oder Bugsnag: Exception-Tracking-Services, die Stack Traces, Request-Daten und Breadcrumbs automatisch erfassen. Sentry bietet ein selbst hostbares Open-Source-Paket.
  • Health-Endpoint: Ein einfacher Controller, der Datenbankverbindung, Redis-Verbindung und Dateisystem-Zugriff prüft und als JSON zurückgibt. Nützlich für Monitoring und schnelle Diagnose.

Security-Hardening für Symfony in Docker

Eine produktive Symfony-Anwendung muss gehärtet sein. Die wichtigsten Maßnahmen:

Container-Sicherheit

  • Non-Root-User: Die USER www-data-Anweisung im Dockerfile ist Pflicht. Kein Prozess im Container sollte als Root laufen.
  • Read-Only Filesystem: Volumes für var/cache und var/log mounten, den Rest des Dateisystems als read-only markieren.
  • Keine Shell im Production-Image: Alpine-basierte Images ohne installierte Shell reduzieren die Angriffsfläche.
  • Image-Scanning: Tools wie Trivy oder Snyk Container scannen Docker-Images auf bekannte Schwachstellen in Basis-Images und OS-Packages.

Symfony-spezifische Sicherheit

  • Security-Checker: symfony security:check prüft Ihre Composer-Dependencies auf bekannte Schwachstellen. In die CI/CD-Pipeline einbauen.
  • CSRF-Protection: Symfony’s Form-Component schützt automatisch gegen Cross-Site-Request-Forgery — aber nur wenn Sie es nutzen. API-Endpoints brauchen alternative Schutzmechanismen (API-Tokens, OAuth).
  • Rate-Limiting: Symfony’s RateLimiter-Component schützt Login-Endpoints und APIs vor Brute-Force-Angriffen. Konfigurieren Sie sinnvolle Limits pro IP und Benutzer.
  • Content-Security-Policy: Ein gut konfigurierter CSP-Header verhindert XSS-Angriffe. Symfony’s NelmioSecurityBundle vereinfacht die Konfiguration.

Netzwerk-Sicherheit

In einer Docker-Umgebung sollten nur der Webserver und der Reverse Proxy aus dem Internet erreichbar sein. Datenbank, Redis und Worker kommunizieren ausschließlich über das interne Docker-Netzwerk. Eine Host-Firewall (UFW) als zusätzliche Schutzschicht ist empfehlenswert — beachten Sie aber, dass Docker standardmäßig eigene iptables-Regeln setzt und UFW umgehen kann. Die Lösung: Docker so konfigurieren, dass es iptables nicht manipuliert, oder spezifische Firewall-Regeln für Docker-Netzwerke setzen.

Was kostet Symfony-Deployment wirklich?

Die reine Server-Infrastruktur ist günstig: Ein VPS mit 4 Kernen, 8 GB RAM und SSD-Speicher kostet bei europäischen Anbietern 20 bis 40 Euro pro Monat. Aber die wahren Kosten liegen woanders:

  • Initiale Einrichtung: Docker-Setup, CI/CD-Pipeline, SSL, Monitoring — rechnen Sie mit 2 bis 5 Arbeitstagen eines erfahrenen Entwicklers.
  • Laufende Wartung: PHP-Updates, Security-Patches, Docker-Image-Updates, Zertifikatsmanagement. 4 bis 8 Stunden pro Monat.
  • Firefighting: Der Server ist nachts nicht erreichbar, die Datenbank ist voll, PHP-FPM reagiert nicht. Diese ungeplanten Einsätze kommen in den ungünstigsten Momenten.

Bei einem internen Stundensatz von 90 Euro (Agentur-typisch) summiert sich das auf 400 bis 800 Euro monatlich — für eine einzelne Anwendung. Bei mehreren Kundenprojekten multipliziert sich der Aufwand.

Managed Symfony Hosting: Die Agentur-Lösung

Für Agenturen, die Symfony-Projekte abliefern aber keinen Ops-Aufwand betreiben wollen, ist Managed Hosting die wirtschaftlich sinnvollere Lösung:

  • Docker-basierte Infrastruktur: PHP-FPM, Nginx, Redis und Datenbank als orchestrierter Stack — nicht als fragile Einzelinstallation.
  • Automatische Deployments: Git Push, Pipeline läuft, Anwendung ist live. Ohne SSH-Zugang und manuelle Container-Restarts.
  • PHP-FPM-Tuning inklusive: OPcache, Preloading, Worker-Konfiguration — optimiert für Symfony, nicht für WordPress.
  • Monitoring und Backups: Tägliche Backups, Health-Checks, Alerting bei Problemen. Sie werden informiert, nicht geweckt.
  • DSGVO-konform: Europäische Server, AVV, dokumentierte Datenverarbeitung.

Bei netzspitze.tech betreiben wir Symfony-Anwendungen als vollständig gemanagten Docker-Stack. Ihr Team konzentriert sich auf Features und Kundenprojekte — wir kümmern uns um PHP-FPM-Pools, OPcache-Invalidierung und nächtliche Datenbank-Backups.

Fazit: Deployment ist kein Agentur-Kerngeschäft

Symfony produktiv zu deployen ist kein Hexenwerk — aber es ist auch nicht trivial. Docker, CI/CD-Pipelines, PHP-FPM-Tuning und Security-Hardening erfordern Wissen, das in vielen Agenturen nicht zum Tagesgeschäft gehört. Und jede Stunde, die ein Entwickler mit Server-Konfiguration verbringt, fehlt bei der Feature-Entwicklung.

Wenn Sie gerade vor der Entscheidung stehen, wie Sie Ihr nächstes Symfony-Projekt deployen: In einem kostenlosen 15-Minuten-Call klären wir, ob Managed Hosting für Ihre Agentur passt — und wie der Umzug funktioniert.

Bereit für eigene Infrastruktur?

15 Minuten Call – wir klären ob Self-Hosting für euch passt.

Termin buchen