How to Set up a Symfony Project for Work with Docker Subdomains

Symfony with Docker

There are situations when Symfony needs routing based on subdomains. For example, when we deal with various functional clusters within one project or one repository. Let’s say, when, apart from API, there is an administrative part written in Symfony bundles or some functionality for public access urls from the client-side.

Here are some options:

  • api.project-name.work — an API host for client applications (web and mobile clients).
  • hub.project-name.work — a host used for the reverse connection with a mobile client when some feature needs URl available from the browser. For example, in the case of the password reset feature, email confirmation feature, email unsubscription feature, processing web-hooks from payment systems, handling payment systems redirects, and pages like user agreement or terms and conditions which should open either from the browser or from the web view in the mobile app. You can divide all this functionality into subdomains if you need. But it is unreasonable to store all this stuff on the API subdomain since it doesn’t relate to the client queries and in most cases has an authentication method different from API. Subdomains with the ‘hub’ name are very popular on various services for features of the same type.
  • admin.project-name.work — a host where the administrative part is stored. It is convenient when the administrator panel is implemented in written in Symfony within the same repository as the API code. Common entities are used, and both pure Symfony or some available admin bundle (like SonataAdminBundle, EasyAdminBundle etc.) implementations are possible. It is handy when you deal with small or middle startups with a limited budget and a light planned loading of the administrative part.
  • Depending on the subject area of the project, there may be used other subdomains, like ‘my’, ‘stats’, ‘legacy’ etc.

When You Need Separate Subdomains:

Having separate subdomains for different functionality, it is easier to route and balance the traffic when you need to increase the bandwidth of the service. It also allows you to implement the front end with the help of any other technology or framework and to place it on the root domain project-name.work. For example, when the client wants to place the landing for the mobile app on the root domain. The landing can be developed by another team without your participation. And then you Symfony project just will be separated from the root domain routing and will deal only with relevant subdomains.

How to Customize the Project with Subdomains for Docker:

To customize the project in Symfony for Docker, you need some additional configuration. In Docker, you can create routing based on different ports, but you can’t apply routing from the package to the subdomains because, by default, Docker works within the context of a single host.

With Symfony, there is another problem: it does not support routing by ports, but it supports subdomain routing. That is, so far, you can’t set up Symfony so that it invokes this or that controller depending on the port. And this support option implementation is not expected from https://github.com/symfony/symfony/issues/7592#issuecomment-16278784 in the nearest future (you may subscribe on this issue to trace all the amendments: maybe, they will turn back to this feature at some point).

From the two options: either customize the Symfony Router component or add support for the subdomains in Docker, the latter is easier, and it already has a ready-made implementation.

Note: It can be applied only in the local development environment for Docker and it is in no way a universal solution which can be used in production.

To add the subdomain support in Docker, you need to add another container based on jwilder/nginx-proxy image and configuration for it into your docker-compose.yml. Let’s consider the example of the part of docker-compose.yml configuration for two containers: nginx and nginx-proxy. Apparently, there will be other containers apart from these in your project. In this example jekakm/nginx-core:201802261 is our studio image which we use for development.

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 will listen to the local port 80 and traverse it into Docker. Also, VIRTUAL_HOST environment variable is added into the nginx container. In it, comma-separated, you need to mention all the hosts (with subdomains and without) which should be proxied to the nginx container. Via this proxy, Symfony will receive requests from the mentioned hosts without modification, and it will be able, according to routing rules, to define which controller should process the request based on subdomains.

The Quick Example of Subdomain-Based Symfony Routing:

easy_admin_bundle:
    resource: "@EasyAdminBundle/Controller/AdminController.php"
    type: annotation
    host: "%admin_host%" # <-- admin panel host
 
api_fos_oauth_server_token:
    resource: "@FOSOAuthServerBundle/Resources/config/routing/token.xml"
    host: "%api_host%" <--  API host
    methods: POST #
 
api:
    resource: ../src/Controller/API
    type: annotation
    host: "%api_host%" <-- API host
    prefix: /v1.0
 
hub:
    resource: ../src/Controller/Hub
    type: annotation
    host: "%hub_host%" <-- host for all "frontend features"
 
admin:
    resource: ../src/Controller/Admin
    type: annotation
    host: "%admin_host%" <-- host for admin panel, the processing of all custom actions which were implemented from the admin panel  
admin_logout:
    path: /logout
    host: "%admin_host%"
 

In the configuration for the nginx container, there is the host configuration ./docker-configs/nginx.conf which is substituted into the container. In this configuration, SERVER_NAME parameter has the project-name-docker value. It is important since this value later will be used for setting up XDebug in PhpStorm (we’ll cover this issue a bit later on). I also give the example of a full configuration for the nginx host to make it possible to check the differences if necessary.

Attention! The presented config is intended for Symfony 4 and later versions since it uses public/index.php path to the frontend controller:

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;
        }
}

After you have added a nginx-proxy container to the docker-compose.yml file, don’t forget to build up new containers and restart the running containers.

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

Redirect Rules Adding:

The last thing you need to do is adding redirect rules for your local hosts so that the hosts you use (or the hosts with subdomains) redirect to the localhost. You can do it with editing /etc/hosts file and adding the following lines to it:

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

Or you can use the dnsmasq utility and configure a global rule in it. For example, if you use the .work domain for the local hosts, it is configured with the following rule:

address=/.work/127.0.0.1

Adding redirects for the local virtual hosts is far from what is considered to be Docker best practice (namely, when you deploy work environment without any additional commands to the local machine). But unfortunately, there wasn’t another solution to this problem for Symfony subdomains in Docker. And this is a compromise solution. It is still better to use dnsmasq utility to set up a general rule for the local machine.

For PhpStorm to establish XDebug connection, you need to add the local server to the PHP -> Servers configuration:

Symfony project on Docker handling

The name of the host should be the same as the value of the SERVER_NAME option from nginx configurations. In our case, it is project-name-docker.

So,

We have examined how to set up the project in Symfony for work with Docker subdomains. Hope the article will be useful to you. Welcome to discussion and cooperation!