How to dockerise a Symfony 4 project ?

Publié le

25/09/2018

Lénaïc and Antoine would like to  share with you their experience with Docker and Symfony 4.

Lénaic has recently joined KNP as a back-end developer, which does not stop him from playing with React on an internal project. You can follow him on github and twitter.

Antoine came to us 3 years ago as a Symfony Lover and developed a preference for front-end projects with React. You can follow him on github and twitter .

The aim of this article is to present a simple and complete step by step tutorial to bootstrap a Symfony 4 project running into Docker containers. Here is the list of what we need to run our application :

  • An HTTP server that serves our pages (i.e. Apache or Nginx)
  • A SQL database to store our application’s data (i.e. MySQL, MariaDB, PostgreSQL etc.)
  • A running instance of PHP fpm that handles PHP scripts and communicates with the HTTP server

These requirements are probably the most common ones if you want to develop a Symfony 4 application.

Why do we recommend Docker in your Symfony application ?

  • Widely reduces the cost of switching between different projects
    • You don’t have to handle several library versions on your machine. (For instance : you work on two projects that respectively use php 5.6 and php 7.2)
  • Widely reduces the time needed to install a project as a new developer
  • Allows to reproduce the production infrastructure (Same libraries, same versions)
  • Allows you to run your tests on Docker directly on the CI platform you use
    • The time when your tests were valid on your local machine but failed on the CI for some reasons is over.

First step : Install Docker

The first thing we need is to install Docker on your computer.

We need :

  • Docker
  • Docker-compose

Docker brings a simplified way to wrap our softwares into containers that includes everything it needs to run: code, runtime, system tools and libraries.

Docker-compose is a tool for defining and running multi-containers based applications. In particular, it brings everything we need to allow our containers to communicate to each other through a private network and shares filesystem area through volumes.

Install

If you are using a Ubuntu system, you will just have to do :

apt-get install docker docker-compose 

See https://docs.docker.com/install/ & https://docs.docker.com/compose/install/ for full documentation.

And it is … done. You have everything that you need on your computer.

Second step: organize the project directory

    - apps/
       - my-symfony-app/ [← the actual root dir of your symfony app]
     - bin/
     - docker/
     - .env
     - docker-compose.yml

Important note: This directory structure is just what we do at KNP Labs most of the time. It is not based on any convention and it is up to you to follow it or not. Just note that the following steps will be based on it and that you might have to adapt the code according to your own directory structure.

Third step : The docker-compose file

You need a file where you will list all the images required in your application. In the root of your project, create a file “docker-compose.yml”

Now we will enumerate what we need in our Symfony application.

Setting up the database

First of all, we need a database engine. Let’s say MySql. You can of course change to Postgres or MariaDB.

Let’s try to create our file.

You can paste the following code :

     version:  '3.8'
     services:
       mysql:
         image: mysql:8.0
         restart: on-failure
         environment:
           MYSQL_ROOT_PASSWORD: root
           MYSQL_DATABASE: example

What have we done ?

The first line specifies the docker-compose syntax version. Then we add our list of services for our applications. It is a list of Docker images. You can see all images on Docker Hub : https://hub.docker.com/ We just added the latest version of mysql. We set up environment variable such as root password and database name. Full option is available here : https://hub.docker.com/r/library/mysql/

For those who like having a web interface, you can get adminer easily. Just add the following code :

    version: '3.8'
    services:
      mysql:
         …
    
      adminer:
        image: adminer
        restart: on-failure
        ports:
          - '8080:8080'

The ports part is used for communicating with your containers. We link the port 8080 of your container to your computer. So you will be able to access adminer while going to http://localhost:8080/

Setting up the Http Server

Now that we have a database engine, we will need a http server such as Apache or Nginx. In this example, we will use Nginx.

In your docker-compose file you can add :

    version: '3.8'
    
    services:
      mysql:
         …
    
      nginx:
        image: nginx:1.19.0-alpine
        restart: on-failure
        volumes:
          - './apps/my-symfony-app/public/:/usr/src/app'
        ports:
          - '80:80'

As you can see, there are more instructions than the mysql configuration.

You may notice that we provide a specific version of Nginx, “alpine”. You can chose the one you prefer on Docker Hub, but we recommend Alpine. Alpine is one of the lightest weight version of Linux, so it will cost less resources for your computer.

We will also link the port 80 to our computer (the default http port). The application will be able to run as http://localhost

We also have volumes. Volumes are used to share a file system area between your computer to your container. This is particularly useful in dev environment when you are constantly making changes in your source code. As the container and you local machine share the code through the volume, the container doesn’t have to be rebuilt each time a change occurs.

In this example, we use a Symfony 4 application, so we need to share the ‘public’ directory, if you are using a Symfony 3 application, you will have to share the ‘web’ directory. By convention, we store the source code into the ‘/usr/src/app’ directory of containers. That’s the reason why we have - './public/:/usr/src/app'

We now have to tell nginx how to serve our project files. The following configuration is based on the one described in the official Symfony documentation

https://symfony.com/doc/current/setup/web_server_configuration.html .

On the root of your application, you can create a directory “docker”, in the directory, you can add a subdirectory “nginx”. Now you can add the configuration file given below named “default.conf”.

    # ./docker/nginx/default.conf
    server {
     server_name ~.*;
    
     location / {
         root /usr/src/app;
    
         try_files $uri /index.php$is_args$args;
     }
    
     location ~ ^/index\.php(/|$) {
         client_max_body_size 50m;
    
         fastcgi_pass php:9000;
         fastcgi_buffers 16 16k;
         fastcgi_buffer_size 32k;
         include fastcgi_params;
         fastcgi_param SCRIPT_FILENAME /usr/src/app/public/index.php;
     }
    
     error_log /dev/stderr debug;
     access_log /dev/stdout;
    }

For the Symfony3 version, you can change “public” by “web” and “index.php” by “app.php”.

We still need to give this configuration file to our nginx container. This can be done by sharing this file through a volume.

You can add this line to your docker-compose.yml file:

`- './docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro'`

Your nginx service definition should now look like this:

    nginx:
      image: nginx:1.19.0-alpine
      restart: on-failure
      volumes:
        - './apps/my-symfony-app/public:/usr/src/app'
        - './docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro'
      ports:
        - '80:80'

Nginx is now configured.

Setting up the PHP Server

Our docker-compose is almost finished.

You can add the following lines :

    php:
        build:
          context: .
          dockerfile: docker/php/Dockerfile
        volumes:
          - './apps/my-symfony-app/:/usr/src/app'
        restart: on-failure
        env_file:
          - .env
        user: 1000:1000

In this container, we will share our application in the /usr/src/app. The nginx configuration file is already set up using this path so you have nothing else to do. I would still like to highlight one point:

In our default.conf file we have this line:

fastcgi_pass php: 9000 ;

This line means that we’ll transfer any incoming request to php-fpm on port 9000. Indeed, containers communicate to each other through a virtual network on your computer and on this network the “php” container can be reached through the name *drum roll*... “php” !  

One last update to add to our nginx service definition :

    version: '3.8'
    services:
     mysql:
        …
    
     nginx:
       image: nginx:1.19.0-alpine
       restart: on-failure
       volumes:
         - './apps/my-symfony-app/public/:/usr/src/app'
         - './docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro'
       ports:
         - '80:80'
       depends_on:
         - php

Which means that the nginx container must be built and run after the php one.

Final poin t: why does the statement user: 1000:1000 stand for ?

This statement is used to tell the service that it will have to use the user with uid 1000 and gid 1000 internally. In most cases, the user we use on our local machine is the user with uid 1000 and gid 1000.

If you want to check your uid, just run the command :

$ id

By adding this statement, both container and local system will interact with files (through the volume) with the exact same user (uid) and that will prevent you to have permission related issues.

Now there is another thing to do. As you can see, there is no image. We provide a Dockerfile. We will see now how to write it.

4th step : The DockerFile

The Dockerfile is used to give several instructions when we get our images. You can, for instance, install php extensions or execute unix commands.

In our example, you can create a “php” subdirectory in your “docker” directory. In this directory, you can create a file “Dockerfile”.

    # ./docker/php/Dockerfile

   FROM php:7.4-fpm
    
   RUN docker-php-ext-install pdo_mysql
    
   RUN pecl install apcu
    
   RUN apt-get update && \
   apt-get install -y \
   libzip-dev
    
   RUN docker-php-ext-install zip
   RUN docker-php-ext-enable apcu
    
   WORKDIR /usr/src/app
    
   COPY --chown=1000:1000 apps/my-symfony-app /usr/src/app
    
   RUN PATH=$PATH:/usr/src/app/vendor/bin:bin

   COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

   RUN composer install --no-scripts --prefer-dist --no-interaction

The code here, will use a Docker Image “php:7.4-fpm”.

After getting this image, the “pdo_mysql” library will be installed.

For more convenience, you tell Docker that the default directory will be “/usr/src/app”.

Then we copy our current application in our computer, to “/usr/src/app”.

We have now everything for running our app.

Last step : Run the app

Important note : You must be sure to free the ports of your local machine. For instance if you already have a running instance of nginx listening on port 80, be sure to switch this service off to allow your brand new container to use it.

service  stop  nginx

We are almost done ! It is easy now. You just have to open a terminal at the root of your application.

Then run :

docker-compose build

This command will build everything we need from the “docker-compose.yml”.

Once your app is built. You can run :

docker-compose up -d

To stop your containers, use :

docker-compose stop

Now you can run the Symfony commands for building your application on your Docker container such as :

docker-compose exec php composer install
docker-compose exec php php bin/console doctrine:schema:create
docker-compose exec php php bin/console doctrine:fixtures:load
docker-compose exec php php bin/console assets:install –symlink public/

And voilà! You can now see your application running on Docker and available on : http://localhost/

To go further

Even if we have a running project on docker, there is still some improvements we can do to clean things.

First we hard coded some parameters inside our `docker-compose.yml` file. We will export these parameters inside a .env file which allows to override them with env vars.

Which variables will we export ?

  • Mysql password
  • Mysql database
  • All ports binding
  • The user the container must use

Let’s create a `.env` file in the project root directory :

    MYSQL_ROOT_PASSWORD=root
    MYSQL_DATABASE=example
    
    NGINX_PORT=80
    
    ADMINER_PORT=8080
    
    LOCAL_USER=1000:1000

Note: We highly recommend you to not version this file but create a duplicated `.env.dist` file that will be versioned so that each developer of your team can configure their own `.env` file.

Now let’s replace our hard coded parameters by our brand new env vars :

    version: '3.8'
    services:
    mysql:
      image: mysql:8.0
      restart: on-failure
      environment:
        MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
        MYSQL_DATABASE: ${MYSQL_DATABASE}
    
    php:
       build:
         context: .
         dockerfile: docker/php/Dockerfile
       restart: on-failure
       volumes:
         - './apps/my-symfony-app/:/usr/src/app'
       user: ${LOCAL_USER}
    
    nginx:
       image: nginx:1.19.0-alpine
       restart: on-failure
       volumes:
         - './apps/my-symfony-app/public/:/usr/src/app'
         - './docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro'
       ports:
         - '${NGINX_PORT}:80′
       depends_on:
         - php
    adminer:
       image: adminer
       restart: on-failure
       ports:
         - '${ADMINER_PORT}:8080'

Now our docker-compose.yml can have env vars based configuration which allows each developer of the team to configure the project as they intend to. But we can go even further !

Let’s say we want to deploy these containers on production. Do we really need to share volumes and rebind ports between my containers and the host machine ? No. We need a way to configure such things in a separated place that just belongs to the host machine. docker-compose.override.yml to the rescue !

The docker-compose.override.yml is natively supported by docker-compose and allows you to override host specific configuration. This is what we will have at the end :

    # docker-compose.yml
    version: '3.8'
    services:
     mysql:
      image: mysql:8.0
      restart: on-failure
      environment:
        MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
        MYSQL_DATABASE: ${MYSQL_DATABASE}
    
     php:
       build:
         context: .
         dockerfile: docker/php/Dockerfile
       restart: on-failure
       user: ${LOCAL_USER}
    
     nginx:
       image: nginx:1.19.0-alpine
       restart: on-failure
       depends_on:
         - php
    
     adminer:
       image: adminer
       restart: on-failure
    

    # docker-compose.override.yml
    version: '3.8'
    services:
     php:
       volumes:
         - './apps/my-symfony-app/:/usr/src/app'
    
     nginx:
       volumes:
         - './apps/my-symfony-app/public/:/usr/src/app'
         - './docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro'
       ports:
         - '${NGINX_PORT}:80'
    
     adminer:
       ports:
         - '${ADMINER_PORT}:8080'

Now you have a really clean Docker configuration that is usable by anyone on your team and easily deployable on a real server !

If you have any question, feel free to ping us on twitter @KNPLabs :)

Publié par

Antoine
Antoine Lelaisant


Commentaires