Это сложно, необходимо установить много утилит, команд для запуска и обслуживания такого проекта, и вообще, я не думаю, что держать всё в одном проекте — жизнеспособная вещь.
Именно так я думал, когда начинал разбираться в этой теме. Но разобравшись, я с уверенностью могу сказать, что это реально, такая идея имеет место быть, и обслуживать такую архитектуру легко.
В данной статье будет описан процесс реализации архитектуры веб‑приложения с удобной системой докер-контейнеров. Здесь не будет обсуждаться 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_template
networks:
angular_symfony_template:
Делаем ./docker/.env файл и заполняем его:
USER_ID=1000 # ID вашего юзера в контейнере
USER_NAME=!ChangeMe! # имя вашего юзера в контейнере
GROUP_ID=1000 # группа вашего юзера в конейнере
SERVER_PORT=8088 # порт nginx
PHP_PORT=9099 # порт PHP
NODE_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-fpm
ARG USER_ID
ARG GROUP_ID
ARG USER_NAME
# Создаем пользователя для работы в Docker
RUN 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/*
# Установка Composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
# Установка Symfony CLI
RUN curl -sS https://get.symfony.com/cli/installer | bash &&
mv /root/.symfony5/bin/symfony /usr/local/bin/symfony
WORKDIR /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 Server
EXPOSE 4200
CMD ["npm", "run", "start"]
И сделаем ./Makefile, в котором мы будем описывать основные команды проекта:
# VARIABLES
DOCKER_COMPOSE := docker compose -f ./docker/docker-compose.yml
INTERACTIVE := $(shell [ -t 0 ] && echo "-it" || echo "-T")
COMPOSE_EXEC := $(DOCKER_COMPOSE) exec $(INTERACTIVE)
## --- DOCKER ---
.PHONY: up down start stop rebuild rebuild-php rebuild-node
up:
@$(DOCKER_COMPOSE) up -d
down:
@$(DOCKER_COMPOSE) down --remove-orphans
start:
@$(DOCKER_COMPOSE) start
stop:
@$(DOCKER_COMPOSE) stop
rebuild:
@$(DOCKER_COMPOSE) up -d --build
rebuild-php:
@$(DOCKER_COMPOSE) up -d --build php
rebuild-node:
@$(DOCKER_COMPOSE) up -d --build node
## --- APPLICATION INIT ---
.PHONY: app-init
app-init: up composer-install front-init
@echo "Application initialization completed!"
## --- PHP / SYMFONY ---
.PHONY: shell composer-install
shell:
@$(DOCKER_COMPOSE) exec php bash
composer-install:
@echo "Installing composer dependencies..."
@$(COMPOSE_EXEC) php composer install --no-interaction --optimize-autoloader
## --- FRONTEND (Angular) ---
.PHONY: front-shell front-init front-clear front-reinit
front-shell:
@$(DOCKER_COMPOSE) exec node sh
front-init:
@echo "Installing npm dependencies..."
@$(COMPOSE_EXEC) node npm install
front-clear:
@echo "Cleaning frontend artifacts..."
@$(COMPOSE_EXEC) node rm -rf ./node_modules ./.angular ./dist
front-reinit: front-clear front-init
## --- LOGS ---
.PHONY: logs logs-nginx logs-php logs-node
logs:
@$(DOCKER_COMPOSE) logs -f --tail=100
logs-nginx:
@$(DOCKER_COMPOSE) logs -f --tail=100 server
logs-php:
@$(DOCKER_COMPOSE) logs -f --tail=100 php
logs-node:
@$(DOCKER_COMPOSE) logs -f --tail=100 node
Все готово. Строим контейнеры! `make up`
Убедимся, что все запущено и работает командой docker ps. Если там есть наши контейнеры, то все отлично!
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7a5b07e1818b nginx:stable-alpine "/docker-entrypoint.…" 33 seconds ago Up 32 seconds 0.0.0.0:8088->80/tcp, [::]:8088->80/tcp angular_symfony_template_server
4dc9069c4fa1 docker-php "docker-php-entrypoi…" 33 seconds ago Up 32 seconds 0.0.0.0:9099->9000/tcp, [::]:9099->9000/tcp angular_symfony_template_php
bc8ae9b37155 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, заполним его базовой заглушкой:
<?php
namespace AppController;
use SymfonyBundleFrameworkBundleControllerAbstractController;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentRoutingAttributeRoute;
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
Каркас приложения полностью готов к наполнению бизнес‑логикой. Всё работает как единый слаженный механизм.
Для удобства разработчиков я завернул все в гит репозиторий. Можете скачать его и юзать как основу. Распространяется на бесплатных условиях. Упоминать меня не обязательно:) Звездочку на гитхабе поставьте только, если не сложно.
Всем добра! Предлагайте свои доработки к данному скелету или подскажите где я ошибаюсь. Будет интересно почитать и пообщаться.
Автор: antoxabobylev
