The ultimate guide for Symfony 4 + Docker Compose + Traefik 2

Caendra Tech
Caendra Tech Blog
Published in
9 min readJan 8, 2020
Photo by chuttersnap on Unsplash

by Eugenio Carocci, Web Developer at Caendra Inc.

In our latest article, we came out with a solution to quickly have a working environment for a Symfony 4 application embracing the power of Docker Compose.

But … we are all about the motto:

Do the best you can until you know better. Then, do better.

For this reason, we were aware that we made something really good. For us, though, good is only a starting point, and this article will move everything to a whole new level.

And… you’re going to love it.

Photo by Alexander Sinn on Unsplash

In this article, we will cover:

  • The Symfony 4 application.
  • The Docker Compose environment to easily manage all the defined services (or the containers for sake of simplicity).
  • Configurable and extensible Docker images for PHP-FPM and Apache.
  • Traefik, to expose services to the host OS.

3, 2, 1 … Go!

Since the goal of this article is to provide you with a great set of tools, these are the steps that you have to follow on your local machine to enjoy this solution:

  • Clone the Git repo from here.
  • Follow the very brief README.md file in order to get the development environment up & running.
  • Open a browser and visit http://www.symfony-4-docker-dev.com and get the following:
Don’t worry about the 404, is totally normal :)

Architecture overview

The provided solution comes with the following services defined in the Docker Compose cluster:

  • php-fpm
  • www
  • mysql
  • phpmyadmin
  • traefik

The architecture is described in docker-compose.yml, which you can check out below:

version: "3.7"
# --------- #
# Services #
# --------- #
services:
########
# BASE #
########
base:
build:
context: "./docker/base"
image: caendra/base
###########
# PHP-FPM #
###########
php-fpm:
build:
context: "./docker/php-fpm/"
depends_on:
- mysql
networks:
internal:
aliases:
- php-fpm.internal
volumes:
- ${PROJECT_ROOT}/:/var/www/html/
- ${PHPFPM_PATH_CONF_FOLDER}/:/docker-entrypoint-init.d/
environment:
COMPOSER_MEMORY_LIMIT: ${PHPFPM_COMPOSER_MEMORY_LIMIT}
HOST_USER_ID: ${HOST_USER}
HOST_GROUP_ID: ${HOST_GROUP}
##########
# Apache #
##########
www:
build:
context: "./docker/apache/"
depends_on:
- php-fpm
networks:
- internal
volumes:
- ${PROJECT_ROOT}/:/var/www/html/
- ${WWW_PATH_CONF_FOLDER}/:/docker-entrypoint-init.d/
environment:
HOST_USER_ID: ${HOST_USER}
HOST_GROUP_ID: ${HOST_GROUP}
labels:
- "traefik.http.routers.www.rule=Host(`www.symfony-4-docker-dev.com`)"
- "traefik.http.services.www.loadbalancer.server.port=80"
- "traefik.enable=true"
#########
# MySQL #
#########
mysql:
image: mysql:5.7
networks:
internal:
aliases:
- mysql.internal
volumes:
- data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}"
MYSQL_DATABASE: "${MYSQL_DATABASE}"
##############
# phpMyAdmin #
##############
phpmyadmin:
image: phpmyadmin/phpmyadmin
depends_on:
- mysql
networks:
- internal
links:
- mysql:db
volumes:
- /sessions
labels:
- "traefik.http.routers.phpmyadmin.rule=Host(`phpmyadmin.symfony-4-docker-dev.com`)"
- "traefik.enable=true"
###########
# Traefik #
###########
traefik:
image: traefik:v2.1
command:
- --api.insecure=true
- --providers.docker
- --providers.docker.exposedByDefault=false
- --providers.docker.network=internal
ports:
- 80:80
- ${TRAEFIK_PORT_DASHBOARD}:8080
networks:
- internal
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# --------- #
# Networks #
# --------- #
networks:
internal:
# ------- #
# Volumes #
# ------- #
volumes:
data:

As you can see we have adopted a quite standard, yet effective, LAMP architecture.

But, how does it work?

This is a fair question at this point.

Each element of the architecture has its own purpose, and you will find more details as follows:

  • www runs an Apache server that analyzes all input requests and, if valid, forwards them to the PHP-FPM daemon running in the php-fpm service. Inside this container, the application folder is mounted at the /var/www/html folder.
  • php-fpm runs a PHP-FPM daemon that executes PHP scripts in the application folder. Moreover, this is the main container where all the primary tasks are executed, like dependencies management either through Composer or Yarn. Inside this container, the application folder is mounted at the /var/www/html folder.
  • mysql runs a MySQL database storing application data.
  • phpmyadmin runs a phpMyAdmin application connected to the database provided by the mysql service. This is not strictly necessary, but it can be helpful for developers working with the database without additional tools.
  • traefik is a reverse proxy that allows the user on the host machine to access the services within the Docker world. This element uses the Docker socket to analyze what’s going on, reacting to the various events.

The beauty of configurable and extensible Docker images

The architecture we are proposing is partially composed of three Docker images that are not out-of-the-box: base, php-fpm and www.

The first one is just a wrapper of CentOS distribution with some additional tools while the other two are extensions of the former. Here are the details of each image:

  • php-fpm provides PHP-FPM, Composer and Yarn.
  • www provides Apache server.

The base image

The base image is a CentOS that is used as the starting point for the other two. It provides a customized version of CentOS with all the tools needed for both the php-fpm and www images.

Below is the base image Dockerfile:

FROM centos:7.6.1810LABEL maintainer="eugenio.c@elearnsecurity.com"RUN yum -y update && \
yum clean all && \
yum install policycoreutils-python -y && \
yum install epel-release -y && \
yum-config-manager --enable "Extra Packages for Enterprise Linux 7 - x86_64" && \
yum install wget -y# Refer to: https://ius.io/GettingStarted/
WORKDIR /tmpRUN wget https://centos7.iuscommunity.org/ius-release.rpm && \
rpm -Uvh ius-release.rpm && \
rm -rf ius-release.rpm && \
yum -y update# Install utilities
RUN yum install dos2unix \
git \
hg \
htop \
less \
nano \
ntp \
openssl \
psmisc \
unzip \
vim -y

The php-fpm image

The php-fpm image is the most feature-rich among our custom images.

Inside there are several tools installed that are needed for a variety of tasks, like managing both back-end and front-end dependencies respectively with Composer and Yarn.

The entry point of the image is a bash script, which allows the user to customize the container when it starts. Some of the features of the entry point are:

  • Configuration ofboth PHP and PHP-FPM.
  • Installation of additional PHP extensions.
  • Ability to run bash scripts.

Below is the php-fpm image Dockerfile:

FROM caendra/base

LABEL maintainer="eugenio.c@elearnsecurity.com"

# Install Composer
COPY --from=composer /usr/bin/composer /usr/bin/composer

# Install Node.js and Yarn
WORKDIR /tmp
RUN curl -sL https://rpm.nodesource.com/setup_10.x | bash - && \
yum install nodejs -y && \
curl -sL https://dl.yarnpkg.com/rpm/yarn.repo | tee /etc/yum.repos.d/yarn.repo && \
rpm --import https://dl.yarnpkg.com/rpm/pubkey.gpg && \
yum install yarn -y

# Install base PHP Dependencies
RUN yum update -y && \
yum install \
php71u-cli \
php71u-fpm \
php71u-intl \
php71u-json \
php71u-mbstring \
php71u-mysqlnd \
php71u-posix \
php71u-pdo \
php71u-xml \
-y

COPY docker-entrypoint.sh /docker-entrypoint.sh

RUN chmod u+x /docker-entrypoint.sh && \
mkdir /docker-entrypoint-init.d

WORKDIR /var/www/html

CMD ["/docker-entrypoint.sh"]

The www image

The www image is the one providing the Apache web server.

As for the php-fpm image, the bash script at the entry point allows the user to customize the container when it is starting for the first time. Some of the features of the entry point are:

  • Configuration of Apache.
  • Ability to run bash scripts.

Below is the www image Dockerfile:

FROM caendra/baseLABEL maintainer="eugenio.c@elearnsecurity.com"# Install Apache2
RUN yum update -y && \
yum install httpd -y
RUN rm -rf /etc/httpd/conf.d/autoindex.conf && \
rm -rf /etc/httpd/conf.d/userdir.conf && \
rm -rf /etc/httpd/conf.d/welcome.conf && \
rm -rf /etc/httpd/conf.d/000-default.conf && \
rm -rf /etc/httpd/conf.modules.d/00-dav.conf && \
rm -rf /etc/httpd/conf.modules.d/00-lua.conf && \
rm -rf /etc/httpd/conf.modules.d/01-cgi.conf && \
rm -rf /var/www/html && \
rm -rf /var/www/cgi-bin
COPY docker-entrypoint.sh /docker-entrypoint.shRUN chmod u+x /docker-entrypoint.sh && \
mkdir /docker-entrypoint-init.d
WORKDIR /var/www/htmlCMD ["/docker-entrypoint.sh"]

Expose the application using Traefik

A common problem when dealing with a Docker-like type of development environment is exposing the virtualized elements to the host OS.

There are several approaches to solve this common problem.

The least flexible, yet the easiest one, is to hard code the IP address assigned to each service. Another approach is using the docker-hostmanager image, as explained in our previous article.

BUT…

Wouldn’t it be great to have an element to take care of all the internal Docker dynamics while offering a stable interface to the host?

Yes! That would be awesome :)

By saying stable interface, we mean that it is always possible to interact with the application web server using a standard name like www.symfony-4-docker-dev.com, even if its IP dynamically changes from one execution to another.

The good news is that there is a whole category of elements that do this work for us, and the best one, from our perspective, is Traefik.

A brief overview of Traefik

Traefik architecture

Traefik is an open-source Edge Router that makes publishing your services a fun and easy experience. It receives requests on behalf of your system and finds out which components are responsible for handling them.

What sets Traefik apart, besides its many features, is that it automatically discovers the right configuration for your services. The magic happens when Traefik inspects your infrastructure, where it finds relevant information and discovers which service serves which request.

How we used Traefik

Traefik offers the user the ability to work with a variety of providers, and in our use case, Docker has been configured as its unique provider.

To integrate Traefik in our cluster, we have added it inside the docker-compose.yml as shown below:

###########
# Traefik #
###########
traefik:
image: traefik:v2.1
command:
- --api.insecure=true
- --providers.docker
- --providers.docker.exposedByDefault=false
- --providers.docker.network=internal
ports:
- "80:80"
- ${TRAEFIK_PORT_DASHBOARD}:8080
networks:
- internal
volumes:
- /var/run/docker.sock:/var/run/docker.sock

Below are the meanings of the options provided in the command property:

  • api.insecure=true provides a nice, shiny dashboard where it is possible to retrieve a lot of information about all the exposed containers.
  • providers.docker uses Docker as its provider.
  • providers.docker.exposedByDefault=false explicitly shows exposed containers only.
  • providers.docker.network=internal uses the internal network.

Note that api.insecure=true is a way to quickly enable the shiny dashboard where it is possible to retrieve a lot of information about all the exposed containers. Since the dashboard provides sensitive information it should be protected along acme and basicauth.

Here is the dashboard offered by Traefik:

Traefik dashboard — Homepage
Traefik dashboard — HTTP Routers

We are no wizards, and this magic has been achieved by simply adding some labels to the services in the docker-compose.yml.

##########
# Apache #
##########
www:
...
networks:
- internal
...
labels:
- "traefik.http.routers.www.rule=Host(`www.symfony-4-docker-dev.com`)"
- "traefik.http.services.www.loadbalancer.server.port=80"
- "traefik.enable=true"

These labels inform Traefik that this service is enabled and should be exposed using the specified router pointing to the specified port.

The configuration for the phpmyadmin service is almost identical:

##############
# phpMyAdmin #
##############
phpmyadmin:
...
networks:
- internal
...
labels:
- "traefik.http.routers.phpmyadmin.rule=Host(`phpmyadmin.symfony-4-docker-dev.com`)"
- "traefik.enable=true"

The last piece to the puzzle is telling the host OS that it is possible to reach the application mapping the hostnames to 127.0.0.1, where Traefik is listening.

Luckily, there are at least two possible approaches to handle this problem.

The first is configuring the DNS to behave as intended, and the latter is simply configuring the hosts file adding two lines, as shown below:

127.0.0.1 www.symfony-4-docker-dev.com
127.0.0.1 phpmyadmin.symfony-4-docker-dev.com

Wrap up

So, here we are to the end of this article but don’t worry, as it won’t be our last one on this topic since:

The battle of getting better is never ending.

Thank you for your time, and we hope that this article helps some of you in the creation of great applications with all the presented tools.

Let us know what you think by leaving a comment!

Resources

--

--

Caendra Tech
Caendra Tech Blog

We are the Caendra Tech team, the engineers behind Caendra, eLearnSecurity, Hack.me and the Ethical Hacker Network platforms.