# Symfony i Docker w 2026 roku: środowisko deweloperskie i wdrożenie produkcyjne > Kompletny przepływ pracy Symfony z Dockerem na 2026 rok: środowisko deweloperskie FrankenPHP, wieloetapowy obraz produkcyjny, tryb worker, szyfrowane sekrety, optymalizacja obrazu i wdrożenia bez przestojów wraz z migracjami. - Published: 2026-06-25 - Updated: 2026-07-06 - Author: SharpSkill - Tags: symfony, docker, frankenphp, php, deployment, devops - Reading time: 10 min --- Ten poradnik dotyczący Symfony i Dockera buduje kompletny przepływ pracy w kontenerach: powtarzalne lokalne środowisko programistyczne oraz utwardzony obraz produkcyjny, oba oparte na FrankenPHP i Symfony 7.4 LTS. Uruchamianie Symfony w kontenerach eliminuje klasyczny rozjazd konfiguracji między laptopami a serwerami i zamienia każde wdrożenie w dostarczenie pojedynczego, niezmiennego artefaktu zamiast wykonywania kruchej sekwencji ręcznych poleceń na serwerze. > **Stack Symfony Docker w 2026 roku** > > Oficjalny szablon Symfony Docker dostarcza teraz [FrankenPHP](https://frankenphp.dev/) jako środowisko uruchomieniowe, zastępując tradycyjną parę Nginx plus PHP-FPM. Jeden plik binarny obsługuje HTTP/2 i HTTP/3 oraz uruchamia Symfony w trwałym trybie worker, utrzymując jądro aplikacji w stanie uruchomionym między żądaniami zamiast odtwarzać je przy każdym wywołaniu. ## Środowisko programistyczne Symfony w Docker Compose Dobra konfiguracja deweloperska daje każdemu współpracownikowi tę samą wersję PHP, te same rozszerzenia i tę samą bazę danych bez ingerencji w maszynę hosta. Docker Compose opisuje ten stack w sposób deklaratywny. Poniższy przykład łączy kontener FrankenPHP zbudowany z lokalnego pliku `Dockerfile` z usługą PostgreSQL 17, połączone ze sobą w domyślnej sieci Compose. ```yaml # compose.yaml services: php: build: context: . target: frankenphp_dev ports: - "80:80" - "443:443" volumes: - ./:/app # bind mount: edits on the host appear instantly - caddy_data:/data # persist TLS certificates across restarts environment: DATABASE_URL: "postgresql://app:app@database:5432/app?serverVersion=17" depends_on: - database database: image: postgres:17-alpine environment: POSTGRES_DB: app POSTGRES_USER: app POSTGRES_PASSWORD: app volumes: - db_data:/var/lib/postgresql/data volumes: caddy_data: db_data: ``` Bind mount w usłudze `php` mapuje katalog główny projektu do wnętrza kontenera, dzięki czemu zmiany w plikach zaczynają obowiązywać bez przebudowywania obrazu. FrankenPHP generuje lokalny certyfikat TLS przy pierwszym uruchomieniu, dlatego port 443 jest wystawiony na zewnątrz, a `caddy_data` to nazwany wolumen: certyfikat przetrwa `docker compose down`. Wskazówka `serverVersion=17` w DSN pozwala Doctrine pominąć zapytanie wykrywające wersję przy każdym połączeniu. Gdy stack już działa, każde polecenie Symfony wykonuje się wewnątrz kontenera `php`, a nie na hoście, co gwarantuje właściwą wersję PHP oraz rozszerzenia. Utworzenie bazy danych, uruchomienie migracji czy otwarcie powłoki przebiega według tego samego wzorca przez `docker compose exec`. > **Uruchamianie poleceń konsoli w kontenerze** > > Poprzedzaj wywołania Symfony i Composera przedrostkiem `docker compose exec php`, aby uruchamiać je względem środowiska PHP w kontenerze, na przykład `docker compose exec php bin/console make:entity` lub `docker compose exec php composer require symfony/uid`. Host nigdy nie potrzebuje zainstalowanego PHP. ## Wieloetapowy Dockerfile dla produkcyjnego Symfony [Wieloetapowy Dockerfile](https://docs.docker.com/build/building/multi-stage/) współdzieli wspólną bazę, a następnie rozgałęzia się na cel deweloperski i cel produkcyjny. Etap produkcyjny instaluje wyłącznie zależności potrzebne w czasie działania, usuwa deweloperskie pakiety Composera i rozgrzewa cache w trakcie budowania, dzięki czemu działający kontener startuje natychmiast. ```dockerfile # Dockerfile FROM dunglas/frankenphp:1-php8.4 AS base WORKDIR /app # Install the PHP extensions Symfony relies on RUN install-php-extensions \ intl \ opcache \ pdo_pgsql \ zip COPY --from=composer/composer:2-bin /composer /usr/bin/composer # --- Development target --- FROM base AS frankenphp_dev ENV APP_ENV=dev RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini" # --- Production target --- FROM base AS frankenphp_prod ENV APP_ENV=prod ENV FRANKENPHP_CONFIG="worker ./public/index.php" ENV APP_RUNTIME="Runtime\FrankenPhpSymfony\Runtime" RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" # Layer caching: copy manifests first, install, then copy the source COPY composer.json composer.lock symfony.lock ./ RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist COPY . . RUN composer dump-autoload --no-dev --classmap-authoritative \ && composer dump-env prod \ && bin/console cache:warmup ``` Skopiowanie `composer.json` oraz plików lock przed resztą kodu źródłowego to kluczowa optymalizacja: Docker buforuje warstwę instalacji zależności i uruchamia ją ponownie tylko wtedy, gdy zmieniają się manifesty, a nie przy każdej edycji kodu. Flaga `--classmap-authoritative` mówi Composerowi, że mapa klas jest kompletna, więc autoloader nigdy nie sięga w czasie działania po odczyty z systemu plików. Rozgrzanie cache podczas budowania sprawia, że pierwsze żądanie produkcyjne trafia na w pełni skompilowany kontener. ## Tryb worker FrankenPHP i komponent Runtime w Symfony Klasyczny PHP-FPM uruchamia jądro Symfony, obsługuje jedno żądanie i wyrzuca wszystko po jego zakończeniu. Tryb worker utrzymuje jądro oraz kontener wstrzykiwania zależności przy życiu między żądaniami, i to właśnie stąd bierze się większość oszczędności na opóźnieniach. Pakiet `runtime/frankenphp-symfony` umożliwia to dzięki komponentowi Symfony Runtime oraz standardowemu bootstrapowi `public/index.php`. ```php // public/index.php use App\Kernel; require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; return function (array $context) { return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); }; ``` Linia `FRANKENPHP_CONFIG="worker ./public/index.php"` ustawiona w pliku Dockerfile aktywuje tę pętlę. Ponieważ kontener jest wykorzystywany ponownie, każda usługa przechowująca stan związany z konkretnym żądaniem musi resetować się między żądaniami. Symfony obsługuje usługi frameworka automatycznie, ale własne usługi ze stanem powinny implementować `Symfony\Contracts\Service\ResetInterface` i być oznaczone tagiem `kernel.reset`, aby nagromadzony stan nie przeciekał do kolejnego żądania. To ta sama dyscyplina, jakiej wymagają długo działające [workery Messengera](/blog/symfony/symfony-messenger-queues-workers-async-architecture), a nagrodą jest przepustowość żądań kilkukrotnie wyższa niż w PHP-FPM na tym samym sprzęcie. W lokalnym środowisku deweloperskim uruchomienie FrankenPHP z flagą `--watch` automatycznie restartuje worker przy zmianie plików, więc tryb worker nie przeszkadza w pętli edycja-odświeżenie. ## FrankenPHP kontra PHP-FPM w Symfony Wybór między FrankenPHP a klasycznym stackiem Nginx plus PHP-FPM kształtuje zarówno Dockerfile, jak i zachowanie w czasie działania. Poniższa tabela podsumowuje praktyczne różnice dla produkcyjnej konfiguracji Symfony. | Aspekt | Tryb worker FrankenPHP | Nginx + PHP-FPM | |--------|------------------------|-----------------| | Kontenery | Jeden | Dwa (serwer WWW + PHP) | | Uruchomienie jądra | Raz na worker | Raz na żądanie | | HTTP/3 | Wbudowane | Wymaga dodatkowej konfiguracji | | TLS | Automatyczny (Caddy) | Ręczna konfiguracja certyfikatu | | Obsługa stanu | Musi resetować się między żądaniami | Izolowana dla każdego żądania | | Koszt zimnego startu | Płacony raz przy uruchomieniu | Płacony przy każdym żądaniu | Tryb worker wygrywa pod względem przepustowości, ponieważ rozkłada kosztowny etap uruchomienia jądra i kompilacji kontenera na tysiące żądań. PHP-FPM pozostaje istotny wtedy, gdy aplikacja opiera się na zmiennych globalnych lub bibliotekach zewnętrznych zakładających świeży proces dla każdego żądania, ponieważ takie rozwiązania psują się w ponownie wykorzystywanym workerze. > **Przecieki stanu w trybie worker** > > Usługa buforująca dane żądania w prywatnym polu poda dane jednego żądania kolejnemu, o ile się nie zresetuje. Przejrzyj singletony, subskrybentów zdarzeń oraz wszystko, co przechowuje token bezpieczeństwa lub bieżące żądanie, zanim przełączysz się na tryb worker w produkcji. ## Zarządzanie zmiennymi środowiskowymi i sekretami w produkcji Symfony odczytuje konfigurację ze zmiennych środowiskowych, a istnieją dwa solidne sposoby dostarczania ich w produkcji. Pierwszy to wstrzykiwanie zwykłych zmiennych przez orkiestrator lub plik Compose. Drugi to zaszyfrowany sejf sekretów w Symfony, który przechowuje wrażliwe wartości w repozytorium w postaci szyfrogramu i odszyfrowuje je w czasie działania pojedynczym kluczem prywatnym. ```bash # Generate the keypair; commit the public key, keep the private key out of the image php bin/console secrets:generate-keys # Encrypt a value into config/secrets/prod/ php bin/console secrets:set DATABASE_URL # In production, expose only the decryption key to the container export SYMFONY_DECRYPTION_SECRET="$(cat config/secrets/prod/prod.decrypt.private.php)" ``` Krok `composer dump-env prod` w pliku Dockerfile kompiluje pliki `.env` do jednego zoptymalizowanego `.env.local.php`, co usuwa koszt parsowania plików dotenv przy uruchomieniu. Każda zmienna ustawiona w rzeczywistym środowisku kontenera nadal nadpisuje skompilowane wartości domyślne, więc sekrety dostarczane przez orkiestrator zawsze mają pierwszeństwo. [Przewodnik wdrożeniowy Symfony](https://symfony.com/doc/current/deployment.html) opisuje pełną kolejność pierwszeństwa, ale praktyczna zasada jest prosta: nigdy nie wypalaj `APP_SECRET` ani danych dostępowych do bazy w obrazie i wstrzykuj je przy starcie kontenera. ## Optymalizacja produkcyjnego obrazu Symfony Za większość różnicy w wydajności produkcyjnej odpowiadają dwa ustawienia OPcache: wyłączenie walidacji znaczników czasu oraz włączenie preloadingu. Ponieważ obraz kontenera jest niezmienny, kod źródłowy nigdy nie zmienia się w czasie działania, więc OPcache nigdy nie powinien sprawdzać plików w poszukiwaniu edycji. Preloading idzie o krok dalej, ładując klasy Symfony i aplikacji do pamięci współdzielonej raz, przy starcie serwera. ```ini ; docker/php/prod.ini opcache.enable=1 opcache.preload=/app/var/cache/prod/App_KernelProdContainer.preload.php opcache.preload_user=root opcache.memory_consumption=256 opcache.max_accelerated_files=20000 opcache.validate_timestamps=0 ; immutable image: never re-check the filesystem realpath_cache_size=4096K realpath_cache_ttl=600 ``` Symfony generuje wymieniony powyżej plik `preload.php` podczas `cache:warmup`, więc plik istnieje już w obrazie po zakończeniu budowania. [Preloading OPcache](https://www.php.net/manual/en/opcache.preloading.php) łączy te klasy przy starcie i pomija etap kompilacji przy pierwszym żądaniu, które ich potrzebuje. Ustawienie `validate_timestamps=0` jest bezpieczne wyłącznie przy niezmiennych wdrożeniach, w których nowe wydanie oznacza nowy obraz; na zmiennym hoście serwowałoby przestarzały kod. Ustawienie `max_accelerated_files` powyżej rzeczywistej liczby plików utrzymuje cały framework w cache bez usuwania wpisów. ## Zmniejszanie rozmiaru obrazu Symfony za pomocą .dockerignore Brak pliku `.dockerignore` po cichu rozdmuchuje budowanie. Bez niego krok `COPY . .` przenosi do obrazu i kontekstu budowania lokalny katalog `vendor`, deweloperski `var/cache`, wynik builda node oraz historię `.git`. Wykluczenie ich zmniejsza obraz, przyspiesza przesyłanie kontekstu budowania do demona i zapobiega nadpisywaniu świeżo zainstalowanych zależności produkcyjnych przez artefakty deweloperskie. ```gitignore # .dockerignore /.git/ /vendor/ /node_modules/ /var/ /.env.local /.env.*.local /tests/ /docker/ compose*.yaml Dockerfile ``` Ignorowanie `/vendor/` ma największe znaczenie: etap produkcyjny wykonuje `composer install --no-dev`, więc przeniesienie deweloperskiego katalogu vendor z hosta całkowicie zniweczyłoby ten krok. Wykluczenie `/var/` trzyma deweloperski cache i logi poza obrazem, pozwalając wykonanemu przy budowaniu `cache:warmup` wyprodukować czysty cache produkcyjny. W połączeniu z budowaniem wieloetapowym i rozszerzeniami opartymi na Alpine odchudzony obraz Symfony zwykle mieści się znacznie poniżej 150 MB. ## Wdrażanie kontenerów Symfony i uruchamianie migracji Produkcyjny plik Compose odwołuje się do wcześniej zbudowanego obrazu z rejestru zamiast budować go lokalnie i definiuje health check, dzięki czemu orkiestrator kieruje ruch wyłącznie do kontenera, który odpowiada. Sekrety trafiają jako zmienne środowiskowe, nigdy jako warstwy obrazu. ```yaml # compose.prod.yaml services: php: image: registry.example.com/app:${TAG} environment: APP_ENV: prod APP_SECRET: ${APP_SECRET} DATABASE_URL: ${DATABASE_URL} healthcheck: test: ["CMD", "curl", "-fsS", "http://localhost/health"] interval: 10s timeout: 3s retries: 3 restart: unless-stopped ``` Health check odpytuje przez curl trasę `/health`, więc aplikacja musi ją wystawić. Minimalny kontroler zwracający kod 200 bez odwoływania się do bazy danych utrzymuje sprawdzenie szybkim i pozwala uniknąć oznaczenia kontenera jako niesprawnego podczas przejściowego problemu z bazą. Gdy gotowość bazy danych ma znaczenie, drugi, głębszy test może uruchomić lekkie zapytanie, ale punkt liveness pozostaje trywialny. ```php // src/Controller/HealthController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\Routing\Attribute\Route; final class HealthController extends AbstractController { #[Route('/health', name: 'health', methods: ['GET'])] public function __invoke(): JsonResponse { return new JsonResponse(['status' => 'ok']); } } ``` Migracje bazy danych powinny działać jako odrębny krok przed przejęciem ruchu przez nowe kontenery, a nie wewnątrz startu aplikacji. Uruchomienie ich w jednorazowym kontenerze gwarantuje, że schemat zostanie zaktualizowany dokładnie raz na wydanie, nawet gdy kilka replik aplikacji startuje równolegle. ```bash # deploy.sh set -euo pipefail docker compose -f compose.prod.yaml pull # Apply migrations once, in an isolated container docker compose -f compose.prod.yaml run --rm php \ bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration # Start replicas and wait for the health check to pass docker compose -f compose.prod.yaml up -d --wait ``` Flaga `--wait` blokuje działanie do momentu, aż każda usługa zgłosi stan zdrowy, dając skryptowi wdrożeniowemu realny sygnał sukcesu zamiast uruchomienia w trybie „odpal i zapomnij”. Połączenie tego z aktualizacją kroczącą w orkiestratorze takim jak Kubernetes czy Docker Swarm daje wydania bez przestojów: stare kontenery obsługują ruch, dopóki nowe nie przejdą swojego health check. Szerszy obraz wersji frameworka, do których celuje ten przepływ pracy, przedstawiają [przegląd platformy Symfony](/technologies/symfony) oraz [przewodnik po nowościach Symfony 8 i PHP 8.4](/blog/symfony/symfony-8-new-features-php-84-lazy-objects), które omawiają zmiany w bieżącej linii wydawniczej. ## Podsumowanie - Używaj wieloetapowego pliku Dockerfile, aby obrazy deweloperski i produkcyjny współdzieliły jedną bazę, a cel produkcyjny był dostarczany bez deweloperskich zależności Composera - Kopiuj `composer.json` i pliki lock przed kodem źródłowym, aby utrzymać warstwę instalacji zależności w cache mimo zmian w kodzie - Uruchamiaj FrankenPHP w trybie worker, aby ponownie wykorzystywać uruchomione jądro między żądaniami, i resetuj usługi ze stanem za pomocą `ResetInterface`, aby zapobiec przeciekom stanu - Rozgrzewaj cache Symfony i kompiluj `.env` przez `dump-env prod` w czasie budowania, aby działający kontener startował w pełni zainicjalizowany - Włącz preloading OPcache i ustaw `validate_timestamps=0` w produkcji, co jest bezpieczne właśnie dlatego, że obraz jest niezmienny - Trzymaj `APP_SECRET`, dane dostępowe do bazy oraz klucz odszyfrowujący sejf poza warstwami obrazu; wstrzykuj je jako zmienne środowiskowe przy starcie kontenera - Stosuj migracje Doctrine w jednorazowym kontenerze przed przejęciem ruchu przez nowe repliki i uzależniaj wdrożenie od health check za pomocą `up -d --wait` --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pl/blog/symfony/symfony-docker-development-production