Docker PHP Symfony Tutorial


We've now got pretty much everything in place to make a Symfony Docker image. At this point we could use this setup to make pretty much any PHP-based application, whether that be Symfony, Laravel, WordPress, or any other project you may have.

Also, what's super cool about this approach is that right now we are at PHP 7.1. In about 8 weeks time, at the time of recording, we are expecting PHP 7.2 to land.

To go from PHP 7.1 to PHP 7.2 is extremely easy using this setup.

We only need to update the PHP Docker image from 7.1 to 7.2, and rebuild our Symfony / Laravel / WordPress / whatever image, and we are done.

This makes this kind of setup perfect for a serious Continuous Integration pipeline, particularly on open source projects where you may need to support PHP 5.6, and PHP 7.x. Building multiple image variants becomes much, much easier.

Now, the specifics of what needs to happen in this image vary depending on your project. We're going to cover a Symfony setup in this video. In a few videos time, we will run through a WordPress setup to see how this approach can differ to meet your needs.

Docker and Symfony

What follows is one opinionated approach. This is my approach. The approach I use right now, in production.

Your needs may differ, and I would encourage you to alter this image to meet your needs, rather than adapt your project to use my code.

Before we go further, be aware that our directory structure at this point looks as follows:

tree -L 1
.
├── Dockerfile
├── README.md
├── app
├── bin
├── composer.json
├── composer.lock
├── phpunit.xml.dist
├── src
├── tests
├── var
├── vendor
├── volumes
└── web

Yes, it's literally the outcome of a symfony new ... command, and a touch Dockerfile :)

The key thing to note is that the Dockerfile is in the project root (i.e. at the same level as app, var, src, etc).

Open up the Dockerfile and add the following:

FROM docker.io/codereviewvideos/php-7:latest

ARG WORK_DIR
WORKDIR $WORK_DIR

COPY composer.lock $WORK_DIR
COPY composer.json $WORK_DIR

COPY app $WORK_DIR/app
COPY bin $WORK_DIR/bin
COPY src $WORK_DIR/src
COPY web $WORK_DIR/web

COPY ./app/config/parameters.yml.dist $WORK_DIR/app/config/parameters.yml

ENV COMPOSER_ALLOW_SUPERUSER 1

RUN composer install --no-dev --prefer-dist --optimize-autoloader --no-scripts

RUN chown -R www-data:www-data $WORK_DIR

USER www-data

Ok, let's break this down.

FROM docker.io/codereviewvideos/php-7:latest

This one should be fairly self explanatory at this point. We have created a separate Docker image to contain all the 'core' parts of our PHP7 setup. We are using this as our starting point.

ARG WORK_DIR

The WORK_DIR here refers to the working directory.

Our working directory will be the root of our Symfony project.

In the Docker nginx PHP Tutorial we added a line to our symfony.conf file to say that this will be /var/www/dev:

server {
    listen 80 default;
    server_name dev;
    root /var/www/dev/web;

    # ...

We are going to use the WORK_DIR argument (ARG) instruction to allow us to change the working directory path at build time.

In other words, this allows us to copy / paste this same Dockerfile between any / all of our Symfony projects, and pass in an argument / variable only at the time we run docker build ... to tell Docker about our current, specific project.

Of course you can hardcode this, and that may be preferable if you only have one or very few Symfony projects.

By way of a quick reference, in setting an ARG instruction we must then pass in a --build-arg flag when running docker build, e.g.:

docker build --build-arg WORK_DIR=/var/www/dev .

This becomes tedious, but not to worry as we will move all this long-winded stuff inside a Makefile anyway.

Anyway, think of an ARG instruction as a variable that we can pass into our Dockerfile at build-time. This is handy because:

WORKDIR $WORK_DIR

We now want to set our WORKDIR based on this passed in value.

WORKDIR is a Docker instruction.

$WORK_DIR is the syntax we use to reference a passed in ARG. In other words, whatever name we gave our ARG, we can reference the value of that ARG by pre-pending the ARG name with a dollar sign. I think they stole this idea from PHP :D

Setting a WORKDIR instruction is how we can make sure a Docker container will cd (change directory) into that specific directory when the container starts running.

What we are saying here, in effect, is hey Docker, please cd /var/www/dev when you start up, ok? Thanks!

We are going use a volume to set this local directory as the /var/www/dev directory inside the running container. This way we will be able to work locally, but have the changes immediately updated and available inside our running Docker container. Neato.

COPY composer.lock $WORK_DIR
COPY composer.json $WORK_DIR

We want to use Docker's COPY instruction to copy the composer.json and composer.lock files from our current directory into the resulting Docker image, in the directory we have set as our WORK_DIR.

We've already covered that $WORK_DIR is whatever path we want as our project's root. So these two commands copy the composer.json and composer.lock files from our current directory into, in our case, /var/www/dev/composer.json and /var/www/dev/composer.lock inside the resulting Docker image.

COPY app $WORK_DIR/app
COPY bin $WORK_DIR/bin
COPY src $WORK_DIR/src
COPY web $WORK_DIR/web

Very similar to the above.

Docker can be a little greedy in what it copies from your build to your image. This can lead to Docker images that are larger than necessary.

Also, it can lead to files getting copied into your Docker images that you may not want to be in there, for whatever reason. This is particularly important if using a public Docker registry, such as Docker Hub.

As such, here we copy the specific directories important to Symfony from our local directory to the resulting Docker image.

Because we already changed directory to the $WORK_DIR in an earlier instruction, we don't technically need the full path here. But I like the explicitness. You may not, and that's cool too.

COPY ./app/config/parameters.yml.dist $WORK_DIR/app/config/parameters.yml

Now this line is very important.

In any new Symfony project we start with a parameters.yml.dist file, and as the file extension implies, this file is suitable for distribution to other people who use your project.

What parameters.yml.dist allows us to do is to specify some sane defaults, which when a composer install is run after cloning the project on another machine, will be used as the starting point for the creation of the parameters.yml file.

As you likely know already, the parameters.yml file contains all the important information needed to actually make our project work. Things like the database username / password / host name, etc.

It seems somewhat unusual, therefore, that we want to use the parameters.yml.dist as our parameters.yml file, no?

Yes, it is. Unless we make the following change:

# app/config/parameters.yml.dist

parameters:

    database_host:      '%env(DB_HOST)%'
    database_port:      '%env(DB_PORT)%'
    database_name:      '%env(DB_DATABASE)%'
    database_user:      '%env(DB_USER)%'
    database_password:  '%env(DB_PASSWORD)%'

    mailer_transport:   "smtp"
    mailer_host:       '%env(MAILER_HOST)%'
    mailer_user:       '%env(MAILER_USER)%'
    mailer_password:   '%env(MAILER_PASSWORD)%'
    mailer_port:       '%env(MAILER_PORT)%'
    mailer_encryption: 'ssl'
    mailer_auth_mode:  'login'

    # A secret key that's used to generate certain security-related tokens
    secret:            '%env(SECRET_KEY)%'

Ok, so you may need to change some of these values, or you may severely dislike this approach.

Why might you dislike it?

Well, using environment variables in this way is a potential security risk.

If there are any errors, all your environment variables will be dumped to your error log. This can mean your user / pass combos end up in plain text in a log file. Bad times.

In practice, I hardcode this file when going into production.

You may wish to use some form of secrets (e.g. Hashicorp's Vault), but this in itself is a larger undertaking than can be covered here. As ever, make the decisions based on your own needs.

What this does allow us to do is to use an environment variables file (.env, typically), or similar, to alter the credentials used on a per project basis. This has proven very useful to me in development and CI environments.

This technique will only work, to the best of my knowledge, from Symfony 3.2 onwards.

ENV COMPOSER_ALLOW_SUPERUSER 1

Here we set an ENVironment variable inside our Docker image to stop Composer complaining that we are running as root. I don't know of an easy way around this, so either accept the warning (no bad thing), or use this ENV variable to suppress the warning.

RUN composer install --no-dev --prefer-dist --optimize-autoloader --no-scripts

This one should be fairly recognisable.

If we want to build a Docker image for production then we will need to run composer install to get all our third party dependencies.

As this will be for production we follow the guidance laid out by the official Symfony docs.

You could add in further flags here to suppress even more of the build output - --no-progress for example, or --no-suggest.

See the official Composer docs for a full breakdown of available commands.

Important to note here is that we won't actually use the output of this command during development. This is a step for production, which is why we run with --no-dev. This will be addressed in more detail shortly.

RUN chown -R www-data:www-data $WORK_DIR

Ahh, Docker and permissions. Likely the single biggest headache you will face when working with Docker. At least, that's been my experience.

Here we recursively (-R) set the owning user and group (www-data:www-data) of all the files now copied in to the working directory.

Remember from our nginx video that nginx will need access to these files, and will also be running as www-data by default. Harmony!

Finally:

USER www-data

The USER instruction tells Docker to become the given user when the container is running.

That's mighty helpful, as we just gave www-data complete ownership of all our data :)

Using The Symfony Docker Image

Right, that's us good to go.

Well, almost.

We do need to run a build, and ideally copy the resulting image up to Docker Hub, or our own private registry.

Let's sort this out real quick.

I do this by using a Makefile:

touch Makefile

Cool, now let's add the following contents:

docker_build:
    @docker build \
        --build-arg WORK_DIR=/var/www/dev/ \
        -t docker.io/codereviewvideos/symfony.dev .

docker_push:
    @docker push docker.io/codereviewvideos/symfony.dev

bp: docker_build docker_push

This gives us three Make commands.

We can run make docker_build, which runs the docker build ... command, passing in the required WORK_DIR which at this point we hardcode to the current project.

This docker build command also tags the image as latest under our Docker Hub repository.

This command, however, doesn't actually send the resulting Docker image up to the Docker Hub repository.

For that we need to docker push up to the repository.

This is taken care of by the docker_push command, which you can invoke with make docker_push.

However, being the lazy sort, 99% of the time I want to run these two commands together.

make bp will run both commands, and is my short hand for docker build and docker push plz thx.

Composing with Docker

Trying to bring together our database, nginx, and this Symfony build by using a bunch of Docker commands is tedious and painful.

We aren't going to inflict such things on ourselves.

Instead, we are going to use Docker Compose to bring everything together.

We've done all the hard work at this point. Now we get to spin up our Dockerised Symfony environment and start having some fun.

Code For This Course

Get the code for this course.

Episodes