Symfony Local Server, Docker, and Varnish
A first approach of a complex setup locally with full performances.
Context
Researching resources before writing this post, I found this nice article from https://jolicode.com/blog/mon-serveur-local-avec-le-binaire-symfony which is a really good introduction. This is in French but you have also the original slides by @fabpot https://symfony.com/blog/local-web-server-reloaded-for-symfony-apps.
Being the author of eZ Launchpad for eZ Platform, I was eager to test that Symfony Local Web Server on a pure Symfony 4 project. And I wanted for this project to have awesome performances for my team on Mac OSX and on Linux. Getting the best performances on Mac OSX with Docker means using the local PHP and not having the PHP running inside a Docker container.
There is a counterpart of the approach, you need to install more stuff locally. More on that part in my feedback at the end.
Docker Stack
The services we usually have
- MariaDB
- Rabbit MQ
- Mailcatcher
- Redis
- Varnish
Here is our Dockerfile
version: '3'
services:
db:
image: 'mariadb:10.2'
environment:
- MYSQL_ROOT_PASSWORD=xxxx
ports:
- '${PROJECTPORTPREFIX}306:3306'
rabbitmq:
image: 'rabbitmq:3.7-management'
environment:
- RABBITMQ_DEFAULT_USER=xxxx
- RABBITMQ_DEFAULT_PASS=xxxx
ports:
- '${PROJECTPORTPREFIX}672:5672'
- '${PROJECTPORTPREFIX}673:15672'
mailcatcher:
image: schickling/mailcatcher
ports:
- '${PROJECTPORTPREFIX}180:1080'
redis:
image: 'redis:5'
ports:
- '${PROJECTPORTPREFIX}379:6379'
varnish:
image: plopix/docker-varnish6
network_mode: host
volumes:
- './varnish/backend.vcl:/etc/varnish/backend.vcl:ro'
- './varnish/varnish.vcl:/etc/varnish/default.vcl:ro'
ports:
- '${PROJECTPORTPREFIX}082:80'
As you can see, we don’t need Nginx or a container for PHP any more! Thank you Symfony Local Server!
The concept of PROJECTPORTPREFIX allows you to define a TCP Port per project so you don’t have to stop containers when working on multiple projects.
My Symfony env.local
looks like that:
DATABASE_URL=mysql://root:xxxxx@127.0.0.1:13306/xxxxx
REDIS_CACHE_URL=redis://127.0.0.1:13379
REDIS_SESSION_URL=redis://127.0.0.1:13379
MAILER_URL=smtp://127.0.0.1:13180
MESSENGER_TRANSPORT_DSN=amqp://xxxxx:xxxxx@127.0.0.1:13672/%2f/messages?queue[attributes][delivery_mode]=2
Boum! that’s it symfony:local:start --port 13080 -d
You will see that the Makefile automates everything
Still, we have 2 issues with Varnish here:
1/ It does not know how to talk to an HTTPS backend
2/ the backend itself will be different on Mac OSX and on Linux
1/ Varnish and Symfony “prod” mode
Indeed if you want to test your code with Varnish it makes sense to be in “prod” mode on your Symfony application. So you have to prefix the Symfony Local Server command with APP_ENV=prod
And regarding the HTTPS, you can tackle that issue with the option --no-tls
to start our Symfony Local Server in HTTP.
You will see in the Makefile below.
2/ OS-aware backend definition for Varnish
On Linux your container can reach the host using localhost
when you are in network mode host
and on Mac OSX it can be reached to host.docker.internal
You should then have 2 files:
The first one for Linux
backend symfony {
.host = "localhost";
.port = "13081";
}
And the one for Mac OS
backend symfony {
.host = "host.docker.internal";
.port = "13081";
}
And the global VCL:
vcl 4.0;
import std;
import xkey;
include "backend.vcl";... following by all your Varnish logic...
So we need to change that backend depending on the host OS. Thanks to Varnish include
function combined with an override on the mount points, you can do that.
version: '3'
services:
varnish:
volumes:
- './varnish/backend_osx.vcl:/etc/varnish/backend.vcl:ro'
This is the docker-composer-osx.yaml
file in which we are mounting a specific VCL file when we are on Mac OS X.
You will see that the Makefile will adapt the
docker-compose
command line depending on the kernel.
Makefile
When we are putting everything all together:
# Variables
PORT_PREFIX := 13
PHP := php
SYMFONY := symfony
DOCKER_COMPOSE := PROJECTPORTPREFIX=$(PORT_PREFIX) docker-compose -p myprojectname -f provisioning/dev/docker-compose.yaml
APP_DIR := $(shell pwd)/application
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin)
DOCKER_COMPOSE := $(DOCKER_COMPOSE) -f provisioning/dev/docker-compose-osx.yaml
endif
.PHONY: serve
serve: stop ## Start the web server in DEV mode, HTTPS, without Varnish
@$(DOCKER_COMPOSE) up -d
@cd $(APP_DIR) && $(PHP) bin/console cache:clear
@cd $(APP_DIR) && $(SYMFONY) local:server:start --port=$(PORT_PREFIX)080 -d
.PHONY: varnishserve
varnishserve: stop ## Start the web server in PROD mode, no HTTPS, through Varnish
@$(DOCKER_COMPOSE) up -d
@cd $(APP_DIR) && APP_ENV=prod $(PHP) bin/console cache:clear
@cd $(APP_DIR) && APP_ENV=prod $(SYMFONY) local:server:start --no-tls --port=$(PORT_PREFIX)081 -d
.PHONY: stop
stop: ## Stop the web server if it is running
@cd $(APP_DIR) && $(SYMFONY) local:server:stop
@$(DOCKER_COMPOSE) stop
Conclusion
Now you can work in ‘dev’ mode through HTTPs using make serve
. This is really helpful to work with HTTP/2 and HTTP/2 Server Push for instance (did not try yet the HTTP/2 Server Push).
And when you need Varnish to test in ‘prod’ mode with a real HTTP cache server (Varnish): use make varnishserve
.
To go Further
Actually, the best way would be to have HTTPS in front of Varnish. But as Symfony Local Server provides HTTPS for us that approach is the most pragmatic approach I have found.
You can achieve HTTPS in front of Varnish using haproxy for instance. But usually we have a PREPRODuction setup correctly to handle HTTPS and for local dev what described is enough! But don’t hesitate to comment with your solution!
My feedback
At Novactive we are working a lot on eZ Platform and therefore with eZ Launchpad which brings everything out of the box. I am usually not that comfortable to ask developers to install specific versions of PHP, yarn, node, convert, or anything locally on their computer. To me, that’s the power of Docker and that what’s should be used, that’s the perfect answer to those onboarding problems.
To illustrate this, the requirements for a fresh installation goes from Docker only to Docker, PHP, PHP-FPM, composer, yarn etc. Plus the PHP extensions, and for instance to install amq
extension (with pecl)
you need to install RabbitMQ client locally brew install rabbitmq-c
.
So this approach kind of break the golden rule, but as for any rule “some of them can be bent, others can be broken” told me a mentor in a famous movie.
Performances on day-to-day are an important part of the job, seconds matter. So to conclude I think we are going to give it a try for this consequent e-commerce project we are going to build in the next months. And we might go back to a container for PHP (and all the needed binaries) and one for Nginx (with HTTPS etc), that won’t be complex at all to switch back. I guess the decision will depend on how easy the onboarding will be using this approach.
It might be the right time for me to finish the full compliance of eZ Launchpad with pure Symfony project. Because when you have a container for PHP then you need to enter this container quite often (to run command) and this is where eZ Launchpad brings a lot of value. (EDIT: after re-reading this article I might start to do that ASAP! ;-))
Hope you liked it! Tell me what you think!