IT-журнал

Как создать Symfony 8 в Docker-контейнере

Пошаговая инструкция по Symfony 8: генерация проекта и запуск php-fpm, nginx и PostgreSQL в Docker Compose с миграциями, первым контроллером и Makefile.

Обновлено 19.09.2026

⏱ 3 мин чтения · 👁 прочитали 3

Пошаговая инструкция: генерируем проект Symfony 8, собираем стек
php-fpm + nginx + PostgreSQL в Docker Compose и получаем рабочий каркас
для разработки.

Что понадобится

  • Docker Engine 24+ и Docker Compose v2 (docker compose, не docker-compose);
  • установленный Symfony 8 требует PHP 8.4 или новее;
  • свободные порты 8080 (веб) и 5432 (база), если выводите её наружу.

Версию PHP внутри контейнера мы не наследуем от хоста — образ php:8.4-fpm
несёт собственный интерпретатор. Локально достаточно, чтобы был установлен
Docker.

Итоговая структура проекта

symfony8-docker/
├── docker/
│   ├── php/
│   │   ├── Dockerfile
│   │   └── php.ini
│   └── nginx/
│       └── default.conf
├── compose.yaml
├── bin/
├── config/
├── public/
├── src/
└── composer.json

Шаг 1. Генерация каркаса Symfony 8

composer create-project требует пустой каталог, поэтому сначала генерируем
скелет одноразовым контейнером composer:2 — сам PHP и Composer берутся из
образа, на хосте ничего ставить не нужно.

mkdir symfony8-docker && cd symfony8-docker

docker run --rm -it \
  -v "$PWD":/app \
  -w /app \
  composer:2 \
  create-project symfony/skeleton:"8.0.*" .

Альтернатива — официальный образ Symfony CLI:

docker run --rm -it \
  -v "$PWD":/app \
  -w /app \
  symfonycorp/cli \
  new . --version=8.0

Устанавливаем полноценный веб-набор и Doctrine:

docker run --rm -it -v "$PWD":/app -w /app composer:2 require webapp
docker run --rm -it -v "$PWD":/app -w /app composer:2 require symfony/orm-pack
docker run --rm -it -v "$PWD":/app -w /app composer:2 require --dev symfony/maker-bundle

webapp подтягивает Twig, форму, валидатор, Security, Mailer, Stimulus,
Web Profiler и PHPUnit Bridge. ORM и Maker выносим явными командами, чтобы
состав зависимостей был очевиден.

Шаг 2. Dockerfile для PHP-FPM

Создаём docker/php/Dockerfile. Образ на Alpine, доустанавливаем расширения,
которые обычно нужны Symfony: intl, pdo_pgsql, zip, opcache.

FROM php:8.4-fpm-alpine

RUN apk add --no-cache \
        git \
        unzip \
        icu-dev \
        libzip-dev \
        postgresql-dev \
    && docker-php-ext-install -j"$(nproc)" \
        intl \
        pdo_pgsql \
        zip \
        opcache

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

# Настройки PHP для контейнера разработки
COPY php.ini /usr/local/etc/php/conf.d/app.ini

WORKDIR /var/www/html

И docker/php/php.ini:

memory_limit = 512M
upload_max_filesize = 32M
post_max_size = 32M

opcache.enable = 1
opcache.validate_timestamps = 1
opcache.revalidate_freq = 0

opcache.validate_timestamps=1 и нулевой revalidate_freq нужны, чтобы в
dev-режиме правки PHP-файлов подхватывались без перезапуска контейнера.

Шаг 3. Конфигурация nginx

Создаём docker/nginx/default.conf. Корень — public/, весь остальной код
закрыт от веба. FastCGI проксируем на сервис php по имени в compose.

server {
    listen 80;
    server_name localhost;
    root /var/www/html/public;

    location / {
        try_files $uri /index.php$is_args$args;
    }

    location ~ ^/index\.php(/|$) {
        fastcgi_pass php:9000;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
        fastcgi_param HTTPS off;
        internal;
    }

    # Любые другие .php закрываем: Symfony использует единый front controller.
    location ~ \.php$ {
        return 404;
    }

    access_log /var/log/nginx/project_access.log;
    error_log /var/log/nginx/project_error.log;
}

Шаг 4. Docker Compose

compose.yaml:

services:
  php:
    build:
      context: ./docker/php
    volumes:
      - .:/var/www/html
    working_dir: /var/www/html
    environment:
      DATABASE_URL: "postgresql://app:secret@database:5432/app?serverVersion=17&charset=utf8"
    depends_on:
      database:
        condition: service_healthy

  nginx:
    image: nginx:1.27-alpine
    ports:
      - "8080:80"
    volumes:
      - .:/var/www/html:ro
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - php

  database:
    image: postgres:17-alpine
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 5s
      timeout: 5s
      retries: 10

volumes:
  db-data:

Ключевые моменты:

  • DATABASE_URL из compose перекрывает значение из .env, поэтому строка
    подключения всегда указывает на хост database, а не на localhost.
  • serverVersion задаём явно — Doctrine перестаёт опрашивать сервер при
    старте и не падает на несовпадении версии клиента и сервера.
  • Порт базы наружу не публикуем: он доступен только контейнерам compose.
    Нужен доступ с хоста — добавьте ports: ["5432:5432"] сервису database.

Шаг 5. Запуск и первый запуск

Собираем и поднимаем стек:

docker compose up -d --build

Проверяем, что контейнеры живы:

docker compose ps
curl -i http://localhost:8080

Symfony 8 отдаст стартовую страницу с информацией о среде.

Шаг 6. Настройка приложения и базы

Сгенерируйте каркас ORM-конфига и создайте миграцию. Команды выполняем
внутри контейнера:

docker compose exec php php bin/console doctrine:database:create --if-not-exists
docker compose exec php php bin/console make:entity
docker compose exec php php bin/console make:migration
docker compose exec php php bin/console doctrine:migrations:migrate --no-interaction

Для разработки включите .env.local (в git не попадает) — но помните, что
переменные из compose.yaml имеют приоритет над .env-файлами.

Шаг 7. Первый контроллер

docker compose exec php php bin/console make:controller HealthController

Отредактируйте src/Controller/HealthController.php:

<?php

declare(strict_types=1);

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: 'app_health', methods: ['GET'])]
    public function index(): JsonResponse
    {
        return $this->json(['status' => 'ok']);
    }
}

Symfony 8 работает только с PHP-атрибутами — аннотации в докблоках
окончательно удалены, маршруты описываются через #[Route].

docker compose exec php php bin/console debug:router
curl http://localhost:8080/health

Частые команды

Добавьте Makefile, чтобы не печатать длинные строки:

up:
    docker compose up -d --build

down:
    docker compose down

sh:
    docker compose exec php sh

console:
    docker compose exec php php bin/console $(ARGS)

composer:
    docker compose exec php composer $(ARGS)

logs:
    docker compose logs -f

Использование:

make up
make console ARGS="cache:clear"
make composer ARGS="require symfony/mailer"

Права доступа и права на файлы

Файлы, созданные контейнером composer:2 от root, на хосте окажутся
принадлежащими root. Если это мешает, варианты такие:

  1. Генерировать проект от своего пользователя:

bash docker run --rm -it --user "$(id -u):$(id -g)" \ -e COMPOSER_HOME=/tmp/composer \ -v "$PWD":/app -w /app composer:2 require webapp

  1. Или выровнять владельца var/ и vendor/ один раз:

bash docker compose exec -u root php chown -R www-data:www-data var public

Не делайте chmod -R 777 — это лечит симптом, повышая риски безопасности.

Типичные ошибки

Симптом Причина и решение
composer create-project пишет «directory is not empty» Запускайте генерацию в пустом каталоге или используйте временную папку.
nginx отдаёт 502 Bad Gateway fastcgi_pass php:9000 не совпадает с именем сервиса или PHP-FPM не запущен: docker compose logs php.
502 и «File not found» В SCRIPT_FILENAME попал неверный root: проверьте root /var/www/html/public;.
Doctrine не подключается к БД В DATABASE_URL указан localhost вместо database; контейнеры общаются по именам сервисов.
Изменения в PHP не применяются Включите opcache.validate_timestamps=1 и revalidate_freq=0, затем перезапустите php.
Symfony пишет в var/ ошибку доступа Права на var/: chown -R www-data:www-data var от root внутри контейнера.
Class "..." not found после установки пакета Не выполнен composer install внутри контейнера и не сгенерирован автолоадер: docker compose exec php composer dump-autoload.

Что дальше

  • добавьте Mailpit (axllent/mailpit) для перехвата писем в dev;
  • настройте Xdebug в php.ini и пробросьте порт 9003 для отладки;
  • для продакшена соберите многоэтапный образ: стадия сборки с composer install --no-dev --optimize-autoloader, затем php:8.4-fpm-alpine без
    git/unzip и с предварительно прогретым cache:warmup;
  • вынесите секреты из compose.yaml в .env и не коммитьте их в репозиторий.