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 :)
Comments