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.

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.
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.
# 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.
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
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 /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:warmupSkopiowanie 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.
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.
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.
# 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.
; 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=600Symfony 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.
# .dockerignore
/.git/
/vendor/
/node_modules/
/var/
/.env.local
/.env.*.local
/tests/
/docker/
compose*.yaml
DockerfileIgnorowanie /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.
# 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-stoppedHealth 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.
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.
# 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 --waitFlaga --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.jsoni 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
.envprzezdump-env prodw czasie budowania, aby działający kontener startował w pełni zainicjalizowany - Włącz preloading OPcache i ustaw
validate_timestamps=0w 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
Udostępnij
Powiązane artykuły

Symfony 8 w 2026 roku: nowe funkcje, PHP 8.4 Lazy Objects i pytania rekrutacyjne
Symfony 8 wprowadza natywne lazy objects PHP 8.4, formularze wielokrokowe, komendy invokable i nowe komponenty. Poznaj kluczowe zmiany i pytania rekrutacyjne.

Doctrine ORM: Opanowanie relacji w Symfony
Kompletny przewodnik po relacjach Doctrine ORM w Symfony. OneToMany, ManyToMany, strategie ładowania i optymalizacja wydajności z praktycznymi przykładami.

Pytania na rozmowę Symfony: Top 25 w 2026
25 najczęściej zadawanych pytań na rozmowach kwalifikacyjnych dotyczących Symfony. Architektura, Doctrine ORM, serwisy, bezpieczeństwo, formularze i testy ze szczegółowymi odpowiedziami i przykładami kodu.