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.

AspektTryb worker FrankenPHPNginx + PHP-FPM
KonteneryJedenDwa (serwer WWW + PHP)
Uruchomienie jądraRaz na workerRaz na żądanie
HTTP/3WbudowaneWymaga dodatkowej konfiguracji
TLSAutomatyczny (Caddy)Ręczna konfiguracja certyfikatu
Obsługa stanuMusi resetować się między żądaniamiIzolowana dla każdego żądania
Koszt zimnego startuPłacony raz przy uruchomieniuPł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.

Wyzwanie dnia

Znajdziesz błąd w Symfony?

Prawdziwy fragment kodu, ukryty błąd, jedna próba dziennie. Bez konta, żeby spróbować.

Anthony Fillion-Maillet

Autor:

Anthony Fillion-Maillet

Założyciel SharpSkill

Programista fullstack od ponad 10 lat. Prowadzi SharpSkill i odpowiada za wszystko, co się tu ukazuje.

Zaktualizowano 6 lipca 2026

Tagi

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

Udostępnij

Powiązane artykuły