Это сложно, необходимо установить много утилит, команд для запуска и обслуживания такого проекта, и вообще, я не думаю, что держать всё в одном проекте — жизнеспособная вещь.
Именно так я думал, когда начинал разбираться в этой теме. Но разобравшись, я с уверенностью могу сказать, что это реально, такая идея имеет место быть, и обслуживать такую архитектуру легко.
В данной статье будет описан процесс реализации архитектуры веб‑приложения с удобной системой докер-контейнеров. Здесь не будет обсуждаться CI/CD, исключительно development‑side процесса разработки.

Prerequirements
Сперва хочется сказать, что я буду использовать в этом проекте. Я не хочу засорять свой образ Windows 11 какими‑либо лишними программами и папками. Поэтому для разработки я всегда использую WSL (не говорите мне ничего плохого о нем, я всё знаю). Это необязательно, можно использовать любой дистрибутив линукса или яблочной ОС. Да хоть native винду) Каждый это делает как хочет. Но лично мне нравится работа в WSL.
Проекты хранятся внутри WSL, не снаружи в Windows хосте, а именно внутри WSL. Это делается для скорости работы. Если проект будет лежать на хосте, то при работе команд доступ к файлам будет осуществляться по особенному и гипер НЕ-быстрому протоколу. Не рекомендую. Но если файлы лежат внутри WSL, то доступ к файлам будет мгновенным.
В WSL у меня стоит самая свежая Ubuntu 24.04.
Естественно, ещё нужно установить Docker.
Я использую самый свежий на момент написания PHPStorm 2026.1.4 от Jetbrains, но абсолютно не важно, какой IDE вы пользуетесь. В теории можно и в блокноте писать хороший код.
Вот именно, что незачем, тем более, когда есть множество классных IDE, которые предлагают очень хороший автокомплит кода, сэмплы кода, работу с Git и так далее.
Также для удобства надо поставить команду make, которая умеет работать с файлами Makefile, так как все команды я обычно оборачиваю в свои собственные, это очень удобно.
Project skeleton
Хватит воды, приступим сразу к делу.
Создаём проект, я буду хранить его в домашней папке юзера: cd ~ && mkdir angular‑symfony‑template && cd angular‑symfony‑template.
Все следующие команды я буду писать из корня проекта.
Я обычно создаю отдельную папку docker в корне проекта, где хранятся конфиги докера. `mkdir docker`.
Создаем главный файл конфига докера: touch./docker/docker‑compose.yml.
services: server: image: nginx:stable-alpine # Самый стабильный минимальный образ nginx container_name: angular_symfony_template_server restart: always ports: - ${SERVER_PORT:-8080}:80 # Проброс порта из файла ./docker/.env volumes: - ../.:/var/www/project - ./nginx/default.conf:/etc/nginx/conf.d/default.conf # Проброс кастомного конфига nginx depends_on: # Не запускать этот контейнер пока не будут запущены следующие контейнеры - php networks: # Общая сетка - angular_symfony_template node: build: context: .. dockerfile: ./docker/node/Dockerfile container_name: angular_symfony_template_node restart: always # Монтируем весь проект, но Dockerfile сам зайдет в подпапку frontend volumes: - ../:/var/www/project ports: - ${NODE_PORT:-4200}:4200 networks: - angular_symfony_template php: container_name: angular_symfony_template_php restart: always build: context: . dockerfile: ./php/Dockerfile # Будем юзать кастомную сборку PHP args: USER_ID: "${USER_ID}" GROUP_ID: "${GROUP_ID}" USER_NAME: "${USER_NAME}" ports: - ${PHP_PORT:-9000}:9000 # Проброс порта из файла ./docker/.env volumes: - ../.:/var/www/project networks: - angular_symfony_templatenetworks: angular_symfony_template:
Делаем ./docker/.env файл и заполняем его:
USER_ID=1000 # ID вашего юзера в контейнереUSER_NAME=!ChangeMe! # имя вашего юзера в контейнереGROUP_ID=1000 # группа вашего юзера в конейнереSERVER_PORT=8088 # порт nginxPHP_PORT=9099 # порт PHPNODE_PORT=4200 # порт Node
Делаем кастомный конфига nginx — ./docker/nginx/default.conf:
server { listen 80; server_name localhost; # Выносим root на самый верх, чтобы он работал везде одинаково root /var/www/project/public; error_log /var/log/nginx/project_error.log; access_log /var/log/nginx/project_access.log; # 1. Запросы к API (обрабатывает Symfony) location /api { # Если пришли на /api или /api/, перенаправляем на index.php rewrite ^/api/?(.*)$ /index.php/$1 last; } # 2. Обработка фронт-контроллера Symfony location ~ ^/index\.php(/|$) { fastcgi_pass php:9000; fastcgi_split_path_info ^(.+\.php)(/.*)$; include fastcgi_params; fastcgi_buffer_size 128k; fastcgi_buffers 4 256k; fastcgi_busy_buffers_size 256k; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $document_root; internal; } # 3. Все остальные запросы отдаем на Angular Dev Server location / { proxy_pass http://node:4200; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } # Запрещаем прямой вызов других PHP файлов location ~ \.php$ { return 404; }}
Делаем Dockerfile для PHP — `./docker/php/Dockerfile`:
FROM php:8.4-fpmARG USER_IDARG GROUP_IDARG USER_NAME# Создаем пользователя для работы в DockerRUN groupadd -g ${GROUP_ID} ${USER_NAME} || true && \ useradd -l -u ${USER_ID} -g ${GROUP_ID} -m -s /bin/bash ${USER_NAME} || true# Настройка прав для папки проектаRUN mkdir -p /var/www/project && \ chown -R ${USER_ID}:${GROUP_ID} /var/www# Устанавливаем только необходимый минимум для расширенийRUN apt-get update && apt-get install -y --no-install-recommends \ git \ unzip \ libicu-dev \ libzip-dev \ && docker-php-ext-install intl opcache pdo pdo_mysql zip \ && rm -rf /var/lib/apt/lists/*# Установка ComposerRUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer# Установка Symfony CLIRUN curl -sS https://get.symfony.com/cli/installer | bash && \ mv /root/.symfony5/bin/symfony /usr/local/bin/symfonyWORKDIR /var/www/project# Переключаемся на созданного пользователяUSER ${USER_NAME}
Не забываем о кастомной сборке ноды, где у нас уже будет установлен Angular CLI для комфортной работы с Angular:
FROM node:26-alpine# Устанавливаем Angular CLI глобально в системуRUN npm install -g @angular/cli# Выставляем рабочую директорию сразу в папку с фронтендомWORKDIR /var/www/project/frontend# Открываем порт для Angular Dev ServerEXPOSE 4200CMD ["npm", "run", "start"]
И сделаем ./Makefile, в котором мы будем описывать основные команды проекта:
# VARIABLESDOCKER_COMPOSE := docker compose -f ./docker/docker-compose.ymlINTERACTIVE := $(shell [ -t 0 ] && echo "-it" || echo "-T")COMPOSE_EXEC := $(DOCKER_COMPOSE) exec $(INTERACTIVE)## --- DOCKER ---.PHONY: up down start stop rebuild rebuild-php rebuild-nodeup:@$(DOCKER_COMPOSE) up -ddown:@$(DOCKER_COMPOSE) down --remove-orphansstart:@$(DOCKER_COMPOSE) startstop:@$(DOCKER_COMPOSE) stoprebuild:@$(DOCKER_COMPOSE) up -d --buildrebuild-php:@$(DOCKER_COMPOSE) up -d --build phprebuild-node:@$(DOCKER_COMPOSE) up -d --build node## --- APPLICATION INIT ---.PHONY: app-initapp-init: up composer-install front-init@echo "Application initialization completed!"## --- PHP / SYMFONY ---.PHONY: shell composer-installshell:@$(DOCKER_COMPOSE) exec php bashcomposer-install:@echo "Installing composer dependencies..."@$(COMPOSE_EXEC) php composer install --no-interaction --optimize-autoloader## --- FRONTEND (Angular) ---.PHONY: front-shell front-init front-clear front-reinitfront-shell:@$(DOCKER_COMPOSE) exec node shfront-init:@echo "Installing npm dependencies..."@$(COMPOSE_EXEC) node npm installfront-clear:@echo "Cleaning frontend artifacts..."@$(COMPOSE_EXEC) node rm -rf ./node_modules ./.angular ./distfront-reinit: front-clear front-init## --- LOGS ---.PHONY: logs logs-nginx logs-php logs-nodelogs:@$(DOCKER_COMPOSE) logs -f --tail=100logs-nginx:@$(DOCKER_COMPOSE) logs -f --tail=100 serverlogs-php:@$(DOCKER_COMPOSE) logs -f --tail=100 phplogs-node:@$(DOCKER_COMPOSE) logs -f --tail=100 node
Все готово. Строим контейнеры! `make up`
Убедимся, что все запущено и работает командой docker ps. Если там есть наши контейнеры, то все отлично!
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES7a5b07e1818b nginx:stable-alpine "/docker-entrypoint.…" 33 seconds ago Up 32 seconds 0.0.0.0:8088->80/tcp, [::]:8088->80/tcp angular_symfony_template_server4dc9069c4fa1 docker-php "docker-php-entrypoi…" 33 seconds ago Up 32 seconds 0.0.0.0:9099->9000/tcp, [::]:9099->9000/tcp angular_symfony_template_phpbc8ae9b37155 node:26-alpine "docker-entrypoint.s…" 33 seconds ago Up 32 seconds 0.0.0.0:4200->4200/tcp, [::]:4200->4200/tcp angular_symfony_template_node
Скелет нашего проекта готов. С этого момента можно накатывать angular и symfony.
Symfony
Я приверженец всего нового, я не вижу смысла новые проекты заводить на старых инструментах, поэтому будем использовать Symfony 8 — самую свежую версию фреймворка на момент написания статьи.
Проваливаемся в контейнер php: make shell. Эта команда закинет нас внутрь работающего PHP‑контейнера, где уже есть всё необходимое: PHP, Composer и Symfony CLI.
Поскольку композер не даст нам развернуть фреймворк симфони внутри папки, где уже есть какие‑либо файлы, то развернем его во временной папке: composer create‑project symfony/skeleton tmp_symfony . После того, как все пакеты загрузились нам надо закинуть все файлы в корень нашего проекта: cp ‑rP tmp_symfony/... Не забываем удалить старую временнуую папку — она нам больше не нужна: rm ‑rf tmp_symfony .
По сути, после этого Symfony готова. В данной статье я не буду рассматривать внутреннюю кухню как работает фреймворк, что за файлы он создал. Возможно, когда‑то в будущем.
Кстати, не забываем выходить из контейнера PHP: exit.
Angular
Проваливаемся в контейнер node: make front‑shell. У нас уже все готово в этом контейнере для наката скелета Angular. Сделаем это командой: ng new frontend‑app ‑directory=. ‑routing ‑style=scss ‑skip‑git.
На вопрос делиться ли псевдоанонимной информацией с гуглом отвечайте сами. Я ничего такого плохого в этом не вижу, поэтому отвечаю Y (yes).
Дальше ангуляр спросит будем ли мы юзать SSR. У нас Single Page Application(SPA), который общается с бэкендом на Symfony через API, это только усложнит архитектуру. SSR (Server‑Side Rendering) заставил бы Node.js постоянно рендерить страницы на сервере, что нагрузит контейнер, а в связке с Symfony. Поэтому твердо и чётко — No.
Следующий вопрос от установщика — какие ИИ тулы вы ходите сконфигурировать в проекте. Я не юзаю такое. Выбираю none.
После этого начинают накатываться файлы фреймворка и подтягиваться зависимости. Все файлы будут положены в папке frontend (так мы настроили в конфиге докера).
Выходим из контейнера node: exit.
Мы настроили проксирование запросов для nginx. Осталось только допилить конфиг ангуляра под это.
В файле frontend/package.json нам надо поправить строку start, чтобы Nginx из своего контейнера мог достучаться до dev‑сервера Angular, Angular должен слушать внешние порты Docker: «start»: «ng serve ‑host 0.0.0.0».
Всё. Теперь нам просто последний раз надо перестроить проект: make rebuild.
После этого всего мы можем проверить наш ангуляр по ссылке: localhost:8088. Порт только свой укажите.
Работает hot reload — значит, если изменить файлы фронта и сохранить их, браузер перезагрузит элемент, который меняли. Очень удобная штука.
Также если сейчас зайти на localhost:8088/api, то бэк вернет стандартный ответ о том, что сейчас рут не найден. Тут не стоит пугаться, мы и вправду не сделали еще рута. Предлагаю быстренько его сделать. Для этого заходим в контейнер php: make shell. Так как симфони предлагает очень удобный бандл для создания сущностей, то я не смею не исспользовать его. Но для начала накатим его, так как симфони у нас сейчас голый: composer require symfony/maker‑bundle ‑dev. И после этого уже создаем рут api: php bin/console make:controller ApiHomeController.
Создался файл src/Controller/ApiHomeController, заполним его базовой заглушкой:
<?phpnamespace App\Controller;use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;use Symfony\Component\HttpFoundation\Response;use Symfony\Component\Routing\Attribute\Route;final class ApiHomeController extends AbstractController{ #[Route('/api', name: 'app_api_home', methods: ['GET'])] public function index(): Response { return new Response('// TODO: implement me'); }}
После этого, если перейти по ссылке localhost:8088/api, то нас будет приветствовать как раз наше сообщение, а не ошибка, как было раньше.
Итоги
Мы проделали отличную работу и развернули современный, отказоустойчивый стек для full‑stack разработки. Локальное окружение теперь настроено по всем правилам хорошего тона.
Nginx, склеил независимые контейнеры бэкенда и фронтенда на одном порту.
Фронтенд (http://localhost:8088/): Отдаёт чистый, работающий Angular 22 в режиме Watch‑mode с хот‑релоадом (изменения в коде сразу отображаются в браузере).
Бэкенд (http://localhost:8088/api): Успешно проксирует запросы на Symfony 8. Мы создали первый боевой контроллер ApiHomeController, который теперь вместо системной заглушки фреймворка отдаёт текстовый ответ: // TODO: implement me
Каркас приложения полностью готов к наполнению бизнес‑логикой. Всё работает как единый слаженный механизм.
Для удобства разработчиков я завернул все в гит репозиторий. Можете скачать его и юзать как основу. Распространяется на бесплатных условиях. Упоминать меня не обязательно:) Звездочку на гитхабе поставьте только, если не сложно.
Всем добра! Предлагайте свои доработки к данному скелету или подскажите где я ошибаюсь. Будет интересно почитать и пообщаться.
ссылка на оригинал статьи https://habr.com/ru/articles/1059392/