Настройка проекта на Symfony на работу с использованием поддоменов в Docker

Symfony на Docker

Бывают ситуации, когда проект на Symfony нуждается в роутинге, основанном на поддоменах. Например, когда в рамках одного проекта, одного репозитория присутствуют различные функциональные блоки. Скажем, когда помимо API для мобильных приложений, рядом еще есть админ часть, написанная на бандлах Symfony или есть какой-то функционал для доступа к публичным урлам со стороны пользователя.

Как вариант, это может выглядеть так:

  • api.project-name.work — API хост для клиентских приложений (веб, мобильные клиенты).
  • hub.project-name.work — хост, который используется для обратной связи с мобильным клиентом, когда функционал требует наличия URL, доступного из бразуера. Например, функционал сброса пароля, функционал подтверждения имейла, функционал, связанный с аннулированием имейл-подписки, обработка веб-хуков от платежных систем, обработка редиректов от платежных систем, странички типа user agreement, terms and conditions, которые должны открываться либо из браузера, либо из web view в мобильном приложении. Весь этот функционал можно разделить на разные поддомены, если нужно. Но хранить его на поддомене API нелогично, так как он не имеет отношения к клиентским запросам, и в большинстве случаев этот функционал имеет другой способ аутентификации, нежели API. Название поддомена hub очень популярно на разных сервисах, которые выносят на него аналогичный по типу функционал.
  • admin.project-name.work — хост, на котором хранится админ часть. Это удобно, когда админка выполнена на Symfony в рамках того же репозитория, что и код API. Используются общие сущности. И реализация может быть как на чистой Symfony, так и на одном из доступных админ-бандлов (SonataAdminBundle, EasyAdminBundle и т.д.). Удобно для небольших и средних стартапов с ограниченным бюджетом, в которых не планируется большая нагрузка на админ часть.
  • В зависимости от предметной области проекта, могут появиться и другие поддомены: my, stats, legacy и т.д.

Когда нужны отдельные поддомены:

Наличие отдельных поддоменов под разный функционал позволяет легче маршрутизировать и балансировать трафик, когда нужно повышать пропускную способность сервиса. Также дает возможность реализовать фронтенд на любой другой технологии, любом другом фреймворке и разместить на корневом домене project-name.work. Как пример, заказчик может захотеть на корневом домене сделать лендинг для своего мобильного приложения. Лендинг может делаться другой командой, независимо от вас. Таким образом, ваш проект на Symfony просто не будет обрабатывать роутинг корневого домена, а будет работать только с теми поддоменами, с которыми должен.

Как завести проект с поддоменами под Docker:

Чтобы завести проект на Symfony под Docker, необходима определенная дополнительная конфигурация. В Docker можно сделать маршрутизацию, основанную на различных портах, но нельзя из коробки сделать маршрутизацию по поддоменам, так как по умолчанию Docker работает в контексте одного хоста.

У Symfony есть другая проблема, Symfony не поддерживает роутинг по портам, но в то же время поддерживает роутинг по поддоменам. Т.е., настроить Symfony, чтобы, в зависимости от порта, она вызывала тот или иной контроллер, на данный момент не получится. И пока внедрения подобной поддержки нет в планах https://github.com/symfony/symfony/issues/7592#issuecomment-16278784 (можно подписаться на этот issue и следить за изменениями: возможно, когда-то к этой фиче вернутся).

Из двух вариантов: кастомизировать компонент Symfony Router или добавить поддержку поддоменов в Docker - второй вариант проще, и уже есть его готовая реализация.

Примечание: Это делается только в контексте локальной среды разработки в рамках Docker, это не является универсальным способом, который можно использовать и в production режиме.

Чтобы добавить поддержку поддоменов в Docker, необходимо в свой docker-compose.yml добавить еще один контейнер, основанный на имейдже jwilder/nginx-proxy, а также конфигурацию для него. Приведем пример части конфигурации docker-compose.yml для двух контейнеров: nginx и nginx-proxy. В вашем проекте кроме этих двух контейнеров, скорее всего, будут и другие. В данном примере jekakm/nginx-core:201802261 — это наш студийный имейдж, который мы используем для разработки.

docker-compose.yml

version: '2'
services:
    nginx_proxy:
        image: jwilder/nginx-proxy
        ports:
            - "80:80"
        volumes:
            - /var/run/docker.sock:/tmp/docker.sock:ro
 
    nginx:
        image: jekakm/nginx-core:201802261
         environment:
            - "VIRTUAL_HOST=hub.project-name.work,admin.project-name.work,api.project-name.work"
        volumes:
            - "./docker-configs/nginx.conf:/etc/nginx/sites-enabled/default"
            - ".:/app:cached"
        expose:
            - 80
        depends_on:
            - "php"

Контейнер nginx_proxy будет слушать локальный порт 80 и пробрасывать его в Docker. Также в контейнер nginx добавляется переменная окружения VIRTUAL_HOST. В ней через запятую нужно указать все хосты (с поддоменами или без), которые должны проксироваться в контейнер nginx. Благодаря этому прокси, к Symfony будут приходить реквесты от указанных хостов без модификации, и Symfony сможет по правилам роутинга на основе поддоменов определить, какой контроллер должен обработать запрос.

Небольшой пример роутинга Symfony на основе поддоменов:

easy_admin_bundle:
    resource: "@EasyAdminBundle/Controller/AdminController.php"
    type: annotation
    host: "%admin_host%" # <-- хост админки
 
api_fos_oauth_server_token:
    resource: "@FOSOAuthServerBundle/Resources/config/routing/token.xml"
    host: "%api_host%" <-- хост API
    methods: POST #
 
api:
    resource: ../src/Controller/API
    type: annotation
    host: "%api_host%" <-- хост API
    prefix: /v1.0
 
hub:
    resource: ../src/Controller/Hub
    type: annotation
    host: "%hub_host%" <-- хост для всех "фронтенд фич"
 
admin:
    resource: ../src/Controller/Admin
    type: annotation
    host: "%admin_host%" <-- хост для админки, обработка кастомных экшенов, которые пришлось имплементировать для админки
 
admin_logout:
    path: /logout
    host: "%admin_host%"

В конфигурации для контейнера nginx есть конфигурация хоста ./docker-configs/nginx.conf, которая подставляется в контейнер. В этой конфигурации параметр SERVER_NAME имеет значение project-name-docker; это важно, так как потом это значение будет использоваться для настройки XDebug в PhpStorm (об этом чуть позже). Также привожу пример полной конфигурации для хоста в nginx, чтобы можно было сверить отличия, если что.

Внимание! Приведенный ниже конфиг рассчитан на версию Symfony 4 и выше, так как в нем используется путь public/index.php к фронтенд контроллеру:

server {
        gzip            	on;
        gzip_types      	text/plain text/css application/x-javascript text/xml application/xml application/rss+xml text/javascript image/x-icon application/json;
        gzip_min_length     1000;
        gzip_comp_level     6;
        gzip_http_version   1.0;
        gzip_vary       	on;
        gzip_proxied    	expired no-cache no-store private auth;
        gzip_disable    	msie6;
 
        listen 80;
 
        client_max_body_size 50M;
 
        root /app/public;
 
        rewrite ^/index\.php/?(.*)$ /$1 permanent;
 
        location / {
                index index.php;
                try_files $uri @rewriteapp;
        }
 
        location @rewriteapp {
                rewrite ^(.*)$ /index.php/$1 last;
        }
 
        location ~ ^/(index|config)\.php(/|$) {
                fastcgi_pass   php:9001;
                fastcgi_split_path_info ^(.+\.php)(/.*)$;
                include fastcgi_params;
                fastcgi_param  SERVER_NAME    	project-name-docker;
                fastcgi_param  SCRIPT_FILENAME	$document_root$fastcgi_script_name;
                fastcgi_param  HTTPS          	off;
        }
 
        location ~* ^.+\.(jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|pdf|ppt|txt|tar|wav|bmp|rtf|htc)$ {
                expires 	31d;
                add_header  Cache-Control private;
 
                error_page 404 = @rewriteapp;
        }
 
        location ~* \.(css|js)$ {
                expires 	7d;
                add_header  Cache-Control private;
        }
}

После добавления контейнера nginx-proxy в файл docker-compose.yml не забудьте сбилдить новые контейнеры и перезапустить текущие контейнеры, если они запущены.

$ docker-compose build
$ docker-compose down && docker-compose up -d

Добавление правил редиректа:

Последнее, что нужно сделать, - это добавить правила редиректа для ваших локальных хостов, чтобы используемые вами хосты (или хосты с поддоменами) редиректили на localhost. Это можно сделать путем редактирования файла /etc/hosts и добавления в него следующих строчек:

127.0.0.1   api.project-name.work
127.0.0.1   hub.project-name.work
127.0.0.1   admin.project-name.work

Либо можно использовать утилиту dnsmasq и настроить в ней глобальное правило. Например, если для локальных хостов использовать домен .work, то это конфигурируется следующим правилом в dnsmasq:

address=/.work/127.0.0.1

Потребность добавлять редиректы для локальных виртуальных хостов выходит за рамки того, что принято считать best practice для работы Docker (а именно, разворачивать рабочее окружение без каких либо дополнительных команд на локальной машине). К сожалению, решить проблему с поддоменами для Symfony в Docker другим способом не удалось. И это компромиссное решение. Желательно все-таки пользоваться утилитой dnsmasq, чтобы задать общее правило для локальной машины.

Для того, чтобы PhpStorm подхватывал XDebug подключение для этого случая, нужно добавить локальный сервер в конфигурацию PHP -> Servers:

Symfony project on Docker handling

Название хоста должно быть таким же, как значение опции SERVER_NAME из настроек nginx. В нашем случае это project-name-docker.

Итак:

Мы рассмотрели, как настроить проект на Symfony на работу с использованием поддоменов в Docker. Надеемся, статья будет вам полезна. Рады обсуждению и сотрудничеству!