How to dockerise a Symfony project ?

Published on

Sep 25, 2018

The aim of this article is to present a simple and complete step by step tutorial to bootstrap a Symfony project running in 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 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.

Note: Here is the install for docker compose v1. To use docker compose v2, please refer to: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-22-04

Second step: organize the project directory

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

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.yaml”

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_RANDOM_ROOT_PASSWORD: "yes"
            MYSQL_DATABASE: example
            MYSQL_PASSWORD: password
            MYSQL_USER: user

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 user password and database name. Full option is available here: https://hub.docker.com/_/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.23.3-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 your 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 6 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 domain.tld www.domain.tld;
    root /var/www/project/public;

    location / {
        # try to serve file directly, fallback to index.php
        try_files $uri /index.php$is_args$args;
    }

    location ~ ^/index\.php(/|$) {
        fastcgi_pass unix:/var/run/php/php-fpm.sock;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
    }

    location ~ \.php$ {
        return 404;
    }

    error_log /var/log/nginx/project_error.log;
    access_log /var/log/nginx/project_access.log;
}

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.yaml 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.23.3-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 file 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.23.3-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 point: 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 composer:2.4.2 as composer

##################################

FROM php:8.1-fpm-alpine3.16
    
RUN apk add --no-cache \
    bash=~5.1 \
    git=~2.36 \
    icu-dev=~71.1

RUN mkdir -p /usr/src/app \
    && apk add --no-cache --virtual=.build-deps \
        autoconf=~2.71 \
        g++=~11.2 \
    && docker-php-ext-configure intl \
    && docker-php-ext-install -j"$(nproc)" intl pdo_mysql \
    && pecl install apcu \
    && docker-php-ext-enable apcu intl \
    && apk del .build-deps

WORKDIR /usr/src/app

COPY apps/my-symfony-app/composer.json /usr/src/app/composer.json
COPY apps/my-symfony-app/composer.lock /usr/src/app/composer.lock
    
RUN PATH=$PATH:/usr/src/app/vendor/bin:bin

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

RUN composer install --no-scripts

COPY apps/my-symfony-app /usr/src/app

RUN chown -R 1000:1000 /usr/src/app
USER 1000:1000

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

We will also need to install any PHP libraries needed to build the Symfony application.

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.yaml.

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 bin/console doctrine:migrations:migrate
docker compose exec php bin/console doctrine:fixtures:load

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.yaml file. We will export these parameters inside a .env file which allows to override them with env vars.

Which variables will we export?

  • Mysql user
  • 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_USER=user
MYSQL_PASSWORD=password
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_RANDOM_ROOT_PASSWORD: "yes"
            MYSQL_USER: ${MYSQL_USER}
            MYSQL_PASSWORD: ${MYSQL_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.23.3-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.yaml 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.yaml to the rescue !

The docker-compose.override.yaml 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.yaml
version: '3.8'
services:
    mysql:
        image: mysql:8.0
        restart: on-failure
        environment:
            MYSQL_RANDOM_ROOT_PASSWORD: "yes"
            MYSQL_USER: ${MYSQL_USER}
            MYSQL_PASSWORD: ${MYSQL_PASSWORD}
            MYSQL_DATABASE: ${MYSQL_DATABASE}
    
    php:
        build:
            context: .
            dockerfile: docker/php/Dockerfile
        restart: on-failure
        user: ${LOCAL_USER}
    
    nginx:
        image: nginx:1.23.3-alpine
        restart: on-failure
        depends_on:
            - php
    
    adminer:
        image: adminer
        restart: on-failure
# docker-compose.override.yaml
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 :)

Written by

Antoine Lelaisant
Antoine Lelaisant

Caen

Front and backend developer with a pref for mobile apps. Loves to share his XP with clients & KNPeers during our trainings.

Lénaïc Couëllan
Lénaïc Couëllan

LĂ©naic loves doing front and back end development and guess what? He loves even more working hand in hand with our clients, communicating and helping define their needs.

Comments