Symfony і Docker у 2026 році: середовище розробки та production-деплой
Повний робочий процес Symfony Docker для 2026 року: середовище розробки на FrankenPHP, багатоетапний production-образ, режим воркера, зашифровані секрети, оптимізація образу та деплой без простою з міграціями.

Цей посібник із Symfony та Docker описує повний контейнерний робочий процес: відтворюване локальне середовище розробки та захищений production-образ, обидва побудовані навколо FrankenPHP і Symfony 7.4 LTS. Запуск Symfony у контейнерах прибирає класичний розрив між середовищами ноутбука та сервера, а кожен деплой перетворює на доставку єдиного незмінного артефакта замість крихкої послідовності ручних серверних команд.
Офіційний шаблон Symfony Docker тепер постачається з FrankenPHP як середовищем виконання, замінюючи традиційну пару Nginx плюс PHP-FPM. Один бінарний файл обслуговує HTTP/2 та HTTP/3 і запускає Symfony у постійному режимі воркера, тримаючи ядро завантаженим між запитами замість того, щоб перебудовувати його на кожному виклику.
Середовище розробки Symfony на Docker Compose
Гарне середовище розробки дає кожному учаснику однакову версію PHP, однакові розширення й однакову базу даних, не торкаючись хост-машини. Docker Compose описує цей стек декларативно. Приклад нижче поєднує контейнер FrankenPHP, зібраний із локального Dockerfile, із сервісом PostgreSQL 17, з'єднаними у стандартній мережі 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 на сервісі 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 має спільну базу, яка далі розгалужується на ціль розробки й ціль production. Етап production встановлює лише залежності часу виконання, відкидає dev-пакети Composer і прогріває кеш під час збірки, тож запущений контейнер стартує миттєво.
# 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:warmupКопіювання composer.json та lock-файлів перед рештою вихідного коду — ключова оптимізація: Docker кешує шар встановлення залежностей і повторно виконує його лише тоді, коли змінюються маніфести, а не при кожній правці коду. Прапорець --classmap-authoritative повідомляє Composer, що classmap повний, тож автозавантажувач ніколи не звертається до пошуку у файловій системі під час виконання. Прогрів кешу під час збірки означає, що перший production-запит потрапляє у повністю скомпільований контейнер.
Режим воркера FrankenPHP та Symfony Runtime
Традиційний PHP-FPM завантажує ядро Symfony, обробляє один запит і викидає все геть. Режим воркера тримає ядро та контейнер впровадження залежностей живими між запитами, і саме звідси надходить більшість виграшу в затримці. Пакет runtime/frankenphp-symfony реалізує це через компонент Symfony Runtime та стандартний завантажувач 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, а винагородою є пропускна здатність запитів у кілька разів вища, ніж у 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.
Готовий до співбесід з Symfony?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Керування змінними середовища та секретами в production
Symfony читає конфігурацію зі змінних середовища, і є два надійні способи постачати їх у production. Перший — впровадження звичайних змінних через оркестратор або файл Compose. Другий — зашифроване сховище секретів Symfony, яке зберігає чутливі значення в репозиторії як шифротекст і розшифровує їх під час виконання одним приватним ключем.
# 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 документує повний порядок пріоритетів, але практичне правило просте: ніколи не запікайте APP_SECRET чи облікові дані бази даних в образ, а впроваджуйте їх під час старту контейнера.
Оптимізація production-образу Symfony
Два налаштування OPcache відповідають за більшу частину розриву в продуктивності production: вимкнення перевірки часових міток та ввімкнення попереднього завантаження. Оскільки образ контейнера незмінний, вихідний код ніколи не змінюється під час виконання, тож OPcache не повинен звертатися до файлів, щоб перевіряти правки. Попереднє завантаження йде далі, завантажуючи класи Symfony та застосунку у спільну пам'ять один раз під час старту сервера.
; 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 генерує вказаний вище файл preload.php під час cache:warmup, тож файл уже присутній в образі після збірки. Попереднє завантаження OPcache зв'язує ці класи під час старту й пропускає крок компіляції на першому запиті, якому вони потрібні. Встановлення validate_timestamps=0 безпечне лише для незмінних розгортань, де новий реліз означає новий образ; на змінному хості це віддавало б застарілий код. Задавання max_accelerated_files вище за реальну кількість файлів тримає весь фреймворк у кеші без витіснення.
Зменшення розміру образу Symfony за допомогою .dockerignore
Відсутність файлу .dockerignore непомітно роздуває збірку. Без нього крок COPY . . відправляє в образ і контекст збірки локальний каталог vendor, var/cache з розробки, вихідні файли збірки node та історію .git. Виключення їх зменшує образ, прискорює завантаження контексту збірки до демона й запобігає перезапису розробницькими артефактами свіжовстановлених production-залежностей.
# .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 посилається на попередньо зібраний образ із реєстру, а не збирає його локально, і визначає перевірку працездатності, тож оркестратор спрямовує трафік лише до контейнера, який відповідає. Секрети надходять як змінні середовища, ніколи як шари образу.
# 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 без звернення до бази даних, тримає перевірку швидкою й уникає позначення контейнера як несправного під час тимчасового збою бази даних. Коли важлива готовність бази даних, окрема глибша проба може виконати легкий запит, але точка перевірки живучості залишається тривіальною.
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']);
}
}Міграції бази даних мають виконуватися окремим кроком перед тим, як нові контейнери візьмуть трафік, а не всередині завантаження застосунку. Запуск їх в одноразовому контейнері гарантує, що схема оновлюється рівно один раз на реліз, навіть коли кілька реплік застосунку стартують паралельно.
# 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 та посібник із можливостей Symfony 8 і PHP 8.4 висвітлюють, що змінилося у поточній лінійці релізів.
Висновок
- Використовуйте багатоетапний 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
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Теги
Поділитися
Пов'язані статті

Symfony 8 у 2026 році: нові можливості, PHP 8.4 Lazy Objects та питання для співбесід
Symfony 8 new features: нативні lazy objects PHP 8.4, багатокрокові форми, invokable-команди, JSON Streamer та питання для технічних співбесід 2026.

Doctrine ORM: Опанування зв'язків у Symfony
Повний посібник зі зв'язків Doctrine ORM у Symfony. OneToMany, ManyToMany, стратегії завантаження та оптимізація продуктивності з практичними прикладами.

Питання для співбесіди Symfony: Топ 25 у 2026
25 найчастіших питань зі співбесід Symfony. Архітектура, Doctrine ORM, сервіси, безпека, форми й тести з детальними відповідями та прикладами коду.