Forest header image

Symfony Finland
Random things on PHP, Symfony and web development

Consider Docker for your Symfony projects

During the first half of 2016 the web development community has talked about Docker quite a bit. The technology has been around since 2013, but during the last few months it has matured and started being adopted for mainstream web development. Docker is a great fit for Symfony projects too.

In the recent years many web development companies have invested time and effort into creating Virtual Machine based setups with Vagrant and accompanying tools like Ansible. While a well crafted setup works fine, many have been burned by slow disk access and other issues and have simply returned to a bare metal setup.

However as the complexity of the infrastructure used for your average web development project grows, using a vanilla local environment no longer cuts it. In addition to the standard LAMP components, you've often got a selection of delivery, caching and persistance application to support. Maintaining multiple instances of these supporting tools is impractical.

Docker takes an approach where it wraps a piece of software into something known as a container. These containers are shipped complete with the application code, a runtime, system tools and libraries. Containers are thus guaranteed to always run the same, but can share files and networking with the host and other containers.

Instead of packing up a number of services to a single container, you should treat them as individual components of your infrastructure. Whether if they're built in PHP, Scala, .NET or JavaScript running on Azure, AWS or your localhost is irrelevant. Developers can easily add new services and replicate complex environments reliably and without deep expertise on the individual components.

With Docker developers can setup development environments, but it's not limited to that as you can also deploy the exact same containers to production environments as well. This makes configuration management simple and coupled to the project code itself, instead of being completely separate.

Quite a few different kind of projects are adopting Docker as a installation method, ranging from eZ Platform to .NET Core to Neo4j. There are plenty of great resources available online about Docker:

An example Symfony Docker setup explained

This article does not contain a complete introduction to Docker as here is much more to learn and you're better off reading the official introduction to Docker if you're interested. For a quick practical look at just how one might start using on a Symfony Standard Edition project, assuming you already have Docker installed and have knowledge of server administration basics.

At the root of our project is the file docker-compose.yml, which describes the containers required to run an individual project. The file is used by a Docker Compose, which is a tool for defining and running applications with multiple containers. The file's structure is quite straightforward, but there are plenty of options which are described in the documentation.

We will be installing a rather standard PHP application with some extras like Redis and Varnish. Our compose file starts off with a version definition, that is only required because there are two completely separate versions of the docker-compose.yml specification itself:

version: '2'

This is followed by a full list of services (containers) with configuration options, beginning with the Nginx web server:

services:
  nginx:
    image: nginx
    ports:
      - 8086:80
    volumes:
      - ./:/var/www/symfony
      - ./docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf
    links:
      - fpm
      - varnish

In the nginx service definition we define the application image to be used. This refers to an account on Docker Hub, a central repository where application images are stored. In our case the nginx repository is the official one. Docker images can be created by anyone and depending on images that are not maintained is risky, this is why choosing an official image is often the most reliable choice.

In the ports section we define the internal port 80 of the Nginx container to be mapped to 8086 outside of the application. This means that you will be able to access the Nginx web server on your localhost port 8086. If you want to simulate load balancing using more web servers, you could simply add more nginx instances with different ports

The volumes section we define what files and directories are shared between the host machine and individual containers. In our example the root of our project is mapped to where you might normally expect the document root to be in Linux environments. In addition to the document root, the configuration file is mapped to be used as the configuration file. The file docker/nginx/nginx.conf itself is obviously a regular nginx configuration file containing the rewrites for Symfony (recommended Symfony Nginx configuration).

Next is up are links to other containers, which express dependencies between services, used for determining the order of individual container startup and so on. One of the linked containers is the PHP-FPM that we will used to execute our Symfony application with PHP 7.

For main data persistance we will use a MySQL database:

  mysql:
    image: mysql
    environment:
      MYSQL_DATABASE: devdb
      MYSQL_USER: devdb
      MYSQL_PASSWORD: devdb
      MYSQL_ROOT_PASSWORD: 
    volumes:
      - ./docker/mysql/data:/var/lib/mysql

In the the container definition image is set to be the official Docker image from MySQL, again a very safe option. In the environment section we pass the basic information required for running a MySQL database. These environment variables are used by the image itself. The usage of variables are defined by the image vendor, in this case Oracle.

For volumes we map the MySQL data directory to our local project directory, but you might want to place it elsewhere where it might not be deleted so easily. Our Docker image will obviously ship without any initial database content, so you'll need to have a data migration approach in place to synchronize database structure and content, etc.

For HTTP caching we will add Varnish, a reverse proxy defined as follows:

  varnish:
    image: newsdev/varnish:4.1.0
    environment:
        VARNISH_PORT: 8087
        VARNISH_MEMORY: 1G
    ports:
        - 8087:8087

All the configuration options for image, environment and ports. Are familiar to us by now, but what is different is the source and version of the image. For Varnish, there is currently no official Docker Hub account so the image we use comes from the New York Times development team.

In addition we also define the exact version number to be 4.1.0, where as in our previous definitions we've taken the default version from the repository. Next we setup Redis, a simple key-value store used for caching:

  redis:
    image: redis:latest

Without the need for data persistence a very simplistic configuration with the official image and no shared volumes. Configuration values will be default ones. Finally we will add our last piece, PHP to actually execute our Symfony application:

  fpm:
    build: docker/php-fpm/
    links:
      - redis
      - mysql
    volumes:
      - ./:/var/www/symfony
      - ./docker/php-fpm/php-ini-overrides.ini:/usr/local/etc/php/conf.d/99-overrides.ini

In the above configuration links and volumes we already familiar with, but build is something new (in the past we used image). Build allows us to take an existing image and make changes to it, our example Dockerfile for building our custom image is as follows:

FROM php:fpm
RUN docker-php-ext-enable opcache

In a docker file we choose the base image to be the official PHP docker image. We then just enable the opcache extension, but you can use the Dockerfile to script any commands to build an image. More complex Dockerfile creation is out of the scope of this article, but the official documentation has great resources and best practises for creating Dockerfiles.

To start the project you need to run docker-composer up:

Reggan:docker.sf janit$ docker-compose up
Pulling varnish (newsdev/varnish:4.1.0)...
4.1.0: Pulling from newsdev/varnish
d4bce7fd68df: Pulling fs layer
d4bce7fd68df: Downloading [==>                                                ] 2.096 MB/51.35 MB
a3ed95caeb02: Download complete
d444db50e532: Download complete
313b9a4ae98d: Waiting
9ee155cd21b7: Waiting

This will download all required images, run the build process and finally start your project with all the associated containers. To see which containers are running with the docker ps command:

-Reggan:~ janit$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS                           NAMES
9c090de4060d        nginx               "nginx -g 'daemon off"   About a minute ago   Up 4 seconds        443/tcp, 0.0.0.0:8086->80/tcp   dockersf_nginx_1
457c063786d0        dockersf_fpm        "php-fpm"                About a minute ago   Up 5 seconds        9000/tcp                        dockersf_fpm_1
b9d5913e2ab7        redis:latest        "docker-entrypoint.sh"   About a minute ago   Up 7 seconds        6379/tcp                        dockersf_redis_1
6f78908fe305        mysql   

If you want to try running the project for yourself, the example is available on Github: https://github.com/janit/docker.sf


Written by Jani Tarvainen on Friday August 12, 2016
Permalink - Tags: docker, symfony, php

« PHP/Symfony development with Windows Subsystem for Linux (WSL) - Serverless PHP with Docker(file) and Zeit▲now... Uhm, what? »