# Symfony і Docker у 2026 році: середовище розробки та production-деплой > Повний робочий процес Symfony Docker для 2026 року: середовище розробки на FrankenPHP, багатоетапний production-образ, режим воркера, зашифровані секрети, оптимізація образу та деплой без простою з міграціями. - Published: 2026-06-25 - Updated: 2026-07-06 - Author: SharpSkill - Tags: symfony, docker, frankenphp, php, deployment, devops - Reading time: 10 min --- Цей посібник із Symfony та Docker описує повний контейнерний робочий процес: відтворюване локальне середовище розробки та захищений production-образ, обидва побудовані навколо FrankenPHP і Symfony 7.4 LTS. Запуск Symfony у контейнерах прибирає класичний розрив між середовищами ноутбука та сервера, а кожен деплой перетворює на доставку єдиного незмінного артефакта замість крихкої послідовності ручних серверних команд. > **Стек Symfony Docker у 2026 році** > > Офіційний шаблон Symfony Docker тепер постачається з [FrankenPHP](https://frankenphp.dev/) як середовищем виконання, замінюючи традиційну пару Nginx плюс PHP-FPM. Один бінарний файл обслуговує HTTP/2 та HTTP/3 і запускає Symfony у постійному режимі воркера, тримаючи ядро завантаженим між запитами замість того, щоб перебудовувати його на кожному виклику. ## Середовище розробки Symfony на Docker Compose Гарне середовище розробки дає кожному учаснику однакову версію PHP, однакові розширення й однакову базу даних, не торкаючись хост-машини. Docker Compose описує цей стек декларативно. Приклад нижче поєднує контейнер FrankenPHP, зібраний із локального `Dockerfile`, із сервісом PostgreSQL 17, з'єднаними у стандартній мережі 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 на сервісі `php` монтує корінь проєкту в контейнер, тож зміни файлів набувають чинності без перебудови образу. FrankenPHP генерує локальний TLS-сертифікат під час першого запуску — саме тому порт 443 відкрито, а `caddy_data` є іменованим томом: сертифікат переживає `docker compose down`. Підказка `serverVersion=17` у DSN дозволяє Doctrine пропускати запит на визначення версії при кожному з'єднанні. Щойно стек запущено, кожна команда Symfony виконується всередині контейнера `php`, а не на хості, що гарантує коректну версію PHP та розширення. Створення бази даних, запуск міграцій або відкриття оболонки — усе відбувається за однаковим шаблоном через `docker compose exec`. > **Запуск консольних команд у контейнері** > > Додавайте префікс `docker compose exec php` до викликів Symfony та Composer, щоб запускати їх у середовищі PHP контейнера, наприклад `docker compose exec php bin/console make:entity` або `docker compose exec php composer require symfony/uid`. На хості PHP встановлювати не потрібно. ## Багатоетапний Dockerfile для production Symfony [Багатоетапний Dockerfile](https://docs.docker.com/build/building/multi-stage/) має спільну базу, яка далі розгалужується на ціль розробки й ціль production. Етап production встановлює лише залежності часу виконання, відкидає dev-пакети Composer і прогріває кеш під час збірки, тож запущений контейнер стартує миттєво. ```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 ``` Копіювання `composer.json` та lock-файлів перед рештою вихідного коду — ключова оптимізація: Docker кешує шар встановлення залежностей і повторно виконує його лише тоді, коли змінюються маніфести, а не при кожній правці коду. Прапорець `--classmap-authoritative` повідомляє Composer, що classmap повний, тож автозавантажувач ніколи не звертається до пошуку у файловій системі під час виконання. Прогрів кешу під час збірки означає, що перший production-запит потрапляє у повністю скомпільований контейнер. ## Режим воркера FrankenPHP та Symfony Runtime Традиційний PHP-FPM завантажує ядро Symfony, обробляє один запит і викидає все геть. Режим воркера тримає ядро та контейнер впровадження залежностей живими між запитами, і саме звідси надходить більшість виграшу в затримці. Пакет `runtime/frankenphp-symfony` реалізує це через компонент Symfony Runtime та стандартний завантажувач `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']); }; ``` Рядок `FRANKENPHP_CONFIG="worker ./public/index.php"`, заданий у Dockerfile, активує цей цикл. Оскільки контейнер повторно використовується, будь-який сервіс, що зберігає стан, специфічний для запиту, має скидати себе між запитами. Symfony обробляє фреймворкові сервіси автоматично, але власні сервіси зі станом мають реалізувати `Symfony\Contracts\Service\ResetInterface` і бути позначеними тегом `kernel.reset`, щоб накопичений стан не просочувався в наступний запит. Це та сама дисципліна, якої вимагають довготривалі [воркери Messenger](/blog/symfony/symfony-messenger-queues-workers-async-architecture), а винагородою є пропускна здатність запитів у кілька разів вища, ніж у PHP-FPM на тому самому обладнанні. Для локальної розробки запуск FrankenPHP із прапорцем `--watch` автоматично перезапускає воркер при зміні файлів, тож режим воркера не заважає циклу «правка — оновлення». ## FrankenPHP проти PHP-FPM у Symfony Вибір між FrankenPHP та класичним стеком Nginx плюс PHP-FPM формує і Dockerfile, і поведінку під час виконання. Таблиця нижче узагальнює практичні відмінності для production-налаштування Symfony. | Аспект | Режим воркера FrankenPHP | Nginx + PHP-FPM | |--------|------------------------|-----------------| | Контейнери | Один | Два (вебсервер + PHP) | | Завантаження ядра | Один раз на воркер | Один раз на запит | | HTTP/3 | Вбудовано | Потребує додаткового налаштування | | TLS | Автоматично (Caddy) | Ручне налаштування сертифіката | | Обробка стану | Треба скидати між запитами | Ізольовано на кожен запит | | Вартість холодного старту | Сплачується раз при завантаженні | Сплачується на кожному запиті | Режим воркера виграє за пропускною здатністю, бо розподіляє дорогий крок завантаження ядра й компіляції контейнера на тисячі запитів. PHP-FPM залишається доречним, коли застосунок покладається на глобальні змінні чи сторонні бібліотеки, що припускають свіжий процес на кожен запит, оскільки вони ламаються у повторно використовуваному воркері. > **Витоки стану в режимі воркера** > > Сервіс, що кешує дані запиту у приватній властивості, віддасть дані одного запиту наступному, якщо не скинеться. Перевірте синглтони, підписники подій і все, що тримає токен безпеки чи поточний запит, перш ніж переходити на режим воркера в production. ## Керування змінними середовища та секретами в production Symfony читає конфігурацію зі змінних середовища, і є два надійні способи постачати їх у production. Перший — впровадження звичайних змінних через оркестратор або файл Compose. Другий — зашифроване сховище секретів Symfony, яке зберігає чутливі значення в репозиторії як шифротекст і розшифровує їх під час виконання одним приватним ключем. ```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)" ``` Крок `composer dump-env prod` у Dockerfile компілює файли `.env` в один оптимізований `.env.local.php`, що усуває витрати часу виконання на розбір dotenv-файлів під час завантаження. Будь-яка змінна, задана в реальному середовищі контейнера, все одно перекриває скомпільовані значення за замовчуванням, тож секрети, надані оркестратором, завжди мають пріоритет. [Посібник із розгортання Symfony](https://symfony.com/doc/current/deployment.html) документує повний порядок пріоритетів, але практичне правило просте: ніколи не запікайте `APP_SECRET` чи облікові дані бази даних в образ, а впроваджуйте їх під час старту контейнера. ## Оптимізація production-образу Symfony Два налаштування OPcache відповідають за більшу частину розриву в продуктивності production: вимкнення перевірки часових міток та ввімкнення попереднього завантаження. Оскільки образ контейнера незмінний, вихідний код ніколи не змінюється під час виконання, тож OPcache не повинен звертатися до файлів, щоб перевіряти правки. Попереднє завантаження йде далі, завантажуючи класи Symfony та застосунку у спільну пам'ять один раз під час старту сервера. ```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 генерує вказаний вище файл `preload.php` під час `cache:warmup`, тож файл уже присутній в образі після збірки. [Попереднє завантаження OPcache](https://www.php.net/manual/en/opcache.preloading.php) зв'язує ці класи під час старту й пропускає крок компіляції на першому запиті, якому вони потрібні. Встановлення `validate_timestamps=0` безпечне лише для незмінних розгортань, де новий реліз означає новий образ; на змінному хості це віддавало б застарілий код. Задавання `max_accelerated_files` вище за реальну кількість файлів тримає весь фреймворк у кеші без витіснення. ## Зменшення розміру образу Symfony за допомогою .dockerignore Відсутність файлу `.dockerignore` непомітно роздуває збірку. Без нього крок `COPY . .` відправляє в образ і контекст збірки локальний каталог `vendor`, `var/cache` з розробки, вихідні файли збірки node та історію `.git`. Виключення їх зменшує образ, прискорює завантаження контексту збірки до демона й запобігає перезапису розробницькими артефактами свіжовстановлених production-залежностей. ```gitignore # .dockerignore /.git/ /vendor/ /node_modules/ /var/ /.env.local /.env.*.local /tests/ /docker/ compose*.yaml Dockerfile ``` Ігнорування `/vendor/` має найбільше значення: етап production запускає `composer install --no-dev`, тож відправлення хостового каталогу vendor у dev-варіанті повністю знецінило б цей крок. Виключення `/var/` тримає кеш розробки та логи поза образом, дозволяючи `cache:warmup` на етапі збірки створити чистий production-кеш. У поєднанні з багатоетапною збіркою та розширеннями на основі Alpine компактний образ Symfony зазвичай виходить значно меншим за 150 МБ. ## Розгортання контейнерів Symfony та запуск міграцій Production-файл Compose посилається на попередньо зібраний образ із реєстру, а не збирає його локально, і визначає перевірку працездатності, тож оркестратор спрямовує трафік лише до контейнера, який відповідає. Секрети надходять як змінні середовища, ніколи як шари образу. ```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 ``` Перевірка працездатності робить curl до маршруту `/health`, тож застосунок має його надавати. Мінімальний контролер, що повертає 200 без звернення до бази даних, тримає перевірку швидкою й уникає позначення контейнера як несправного під час тимчасового збою бази даних. Коли важлива готовність бази даних, окрема глибша проба може виконати легкий запит, але точка перевірки живучості залишається тривіальною. ```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']); } } ``` Міграції бази даних мають виконуватися окремим кроком перед тим, як нові контейнери візьмуть трафік, а не всередині завантаження застосунку. Запуск їх в одноразовому контейнері гарантує, що схема оновлюється рівно один раз на реліз, навіть коли кілька реплік застосунку стартують паралельно. ```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 ``` Прапорець `--wait` блокує виконання, доки кожен сервіс не повідомить про справність, даючи скрипту деплою справжній сигнал успіху замість запуску за принципом «запустив і забув». Поєднання цього з поетапним оновленням в оркестраторі на кшталт Kubernetes чи Docker Swarm дає релізи без простою: старі контейнери продовжують обслуговувати, доки нові не пройдуть перевірку працездатності. Для ширшого погляду на версії фреймворку, на які орієнтований цей процес, [огляд платформи Symfony](/technologies/symfony) та [посібник із можливостей Symfony 8 і PHP 8.4](/blog/symfony/symfony-8-new-features-php-84-lazy-objects) висвітлюють, що змінилося у поточній лінійці релізів. ## Висновок - Використовуйте багатоетапний Dockerfile, щоб образи розробки та production мали одну базу, тоді як ціль production постачається без dev-залежностей Composer - Копіюйте `composer.json` та lock-файли перед вихідним кодом, щоб тримати шар встановлення залежностей у кеші попри зміни коду - Запускайте FrankenPHP у режимі воркера, щоб повторно використовувати завантажене ядро між запитами, і скидайте сервіси зі станом через `ResetInterface`, щоб запобігти витокам стану - Прогрівайте кеш Symfony та компілюйте `.env` за допомогою `dump-env prod` під час збірки, щоб запущений контейнер стартував повністю ініціалізованим - Вмикайте попереднє завантаження OPcache і задавайте `validate_timestamps=0` у production — це безпечно саме тому, що образ незмінний - Тримайте `APP_SECRET`, облікові дані бази даних і ключ розшифрування сховища поза шарами образу; впроваджуйте їх як змінні середовища під час старту контейнера - Застосовуйте міграції Doctrine в одноразовому контейнері перед тим, як нові репліки візьмуть трафік, і залежте розгортання від перевірки працездатності через `up -d --wait` --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/uk/blog/symfony/symfony-docker-development-production