Пошаговая инструкция: генерируем проект 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. Если это мешает, варианты такие:
- Генерировать проект от своего пользователя:
bash
docker run --rm -it --user "$(id -u):$(id -g)" \
-e COMPOSER_HOME=/tmp/composer \
-v "$PWD":/app -w /app composer:2 require webapp
- Или выровнять владельца
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и не коммитьте их в репозиторий.