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.

Poradnik Symfony Docker 2026: środowisko deweloperskie i wdrożenie produkcyjne

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 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 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.

public/index.phpphp
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, 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.

Gotowy na rozmowy o Symfony?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

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 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 łą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.

src/Controller/HealthController.phpphp
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 oraz przewodnik po nowościach Symfony 8 i PHP 8.4, 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

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Tagi

#symfony
#docker
#frankenphp
#php
#deployment
#devops

Udostępnij

Powiązane artykuły