Starting to use Docker is not an easy task. Starting to use Symfony on Docker is far more complex.
There are too many things to understand before being able to dockerize a Symfony application.
I studied Docker for more than one month before being able to dockerize our Symfony applications.
How did I reach the goal? By trial and error. A lot of trial and error. And a lot of reading. Really really a lot of reading.
But I can make your journey a lot easier and this series of posts is exactly this: the final summary of my journey trying to dockerize our Symfony applications here at Serendipity HQ.
Unless you have lived on Mars in the last months, you have heard of Docker for sure.
And if you are a curious person, maybe the idea of trying it came to your mind.
But starting to use Docker is not an easy task, at least it wasn’t easy for me.
I read a lot of articles about how to set up a Symfony app to use Docker and all that I found was a bunch of short articles with some code to copy and paste without any explanation about what each line did and why it was there.
So I switched to analyse other Symfony projects that already use Docker (you will find a list of them at the end of this post), but the problem was the opposite: very complex configurations, impossible to understand nor to modify without an understanding of what each line did.
And also searching for each instruction didn’t help as what I lacked was the general picture.
So I decided to start from scratch, solving a problem after problem each time one arose.
And this series of posts is the result of this journey: configuring a Symfony app to use Docker.
Before starting to dockerize your Symfony application, it is better to explain what precisely Docker is.
Docker is:
… An open platform for developers and sysadmins to build, ship, and run distributed applications, whether on laptops, data center VMs, or the cloud.
Its main concept is the “container”:
Docker containers wrap up software and its dependencies into a standardized unit for software development that includes everything it needs to run: code, runtime, system tools and libraries. This guarantees that your application will always run the same and makes collaboration as simple as sharing a container image.
The main advantages of using Docker are:
- Onboard faster and stop wasting hours trying to set up development environments, spin up new instances and make copies of production code to run locally.
- Enable polyglot development and use any language, stack or tools without worry of application conflicts.
- Eliminate environment inconsistencies and the “works on my machine” problem by packaging the application, configs and dependencies into an isolated container.
- Alleviate concern over application security
So, now that we have a clear picture of what is Docker and which problems it solves, let’s start to go a bit deeper in the topic.
The first thing I needed to learn was Docker Compose, the builder of our entire stack.
Our stack uses a docker-compose.yaml
file put in the root of the application.
But now I’m going too fast: Let’s try to do one step at time.
Doing this, at the end of this series, you will have:
- A clear picture of how each building block fits into the bigger picture;
- Which are those building blocks;
- The tools you need to debug Docker and better understand what is going on;
- A basic configuration from which to start building your next dockerized application;
- The tools required to expand this basic configuration.
For this series of posts, we are going to build a Dockerized application based on Symfony 4.2.
But keep in mind that the Symfony version is really irrelevant: once you have understood the basics you will be able to dockerize any version of Symfony, present, past and future.
So, please, before starting, create a new Symfony project to dockerize: having a test application makes things easier as it permits you to focus on the basics, without having to deal with your custom code or custom configurations, external services, etc.
As already told, once you will have the basics, you can dockerize any Symfony project!
Then, install Docker searching on Google “How to install Docker“: Google is really better than me at identifying your OS and giving you the right steps to install Docker, so, follow him!?
We are ready!
Just one note before really starting: in this series of post I will make you follow the exact trial-and-arr approach I followed the first time.
This way you will really understand which are the problems you will face when dockerizing any app and you will deeply understand the logic behind a dockerized infrastructure.
When someone starts using Docker not all concepts are obvious and the approach someone may use or the logic (s)he will follow or the assumptions (s)he will have may be wrong.
For this reason, I prefer to make you able to make errors: so, in this series of posts we will make errors (very bad?) but we will also fix them learning really how Docker works and ” thinks”!?
Introducing docker-compose
Ok, now that you have a fresh Symfony app to dockerize and have installed Docker on your computer, let’s start understanding the first building block of our Docker infrastructure: Docker Compose.
Compose is a tool for defining and running multi-container Docker applications.
With Compose, you use a YAML file to configure your application’s services.
Then, with a single command, you create and start all the services from your configuration.
To learn more about all the features of Compose, see the list of features.
Ok, I know, this can be a bit obscure at the beginning: after all, you may have no idea of what a container is concretely, what it does and of which parts is composed of.
Don’t worry: keep reading and you will understand everything!
For the moment, you only need to understand and remember that Docker Compose uses a docker-compose.yaml
file put in the root folder of the project to know what services it has to create to make your app able to work.
The Compose file is a YAML file defining services, networks, and volumes for a Docker application.
…
There are several versions of the Compose file format – 1, 2, 2.x, and 3.x
Mmm, services, networks, volumes, file versions: these are exactly the kind of things that make difficult to understand how to use Docker at the beginning. ?
Again, don’t worry and keep reading: I will explain to you everything in details. More often than not, it is easier to do things first and understand them later, instead of trying to understand them first and then doing them later.
So, how you write your Compose file, depends on the version of Docker you are running.
To know which version of Docker you are running, use the docker version
command:
Aerendir$ docker version
Client:
Version: 18.06.0-ce
API version: 1.38
Go version: go1.10.3
Git commit: 0ffa825
Built: Wed Jul 18 19:05:26 2018
OS/Arch: darwin/amd64
Experimental: false
Server:
Engine:
Version: 18.06.0-ce
API version: 1.38 (minimum version 1.12)
Go version: go1.10.3
Git commit: 0ffa825
Built: Wed Jul 18 19:13:46 2018
OS/Arch: linux/amd64
Experimental: false
As you can see, I’m now running Docker version 18.06.0-ce
.
This means that I can use the Compose file format version 3.7.
How do I know this?
Because Docker provides a matrix where, for each Docker version, there is the corresponding Compose version supported.
Very easy, isn’t it? ?
Creating our docker-compose.yaml
file
For a Symfony application we basically need three things:
- A web server (Apache or Nginx);
- PHP;
- A database (MySQL, PostgreSQL, MongoDB, Redis, etc.).
Last but not least, we also need an operating system: MacOS, Windows, *nix, etc.
The combination of those technologies is our “stack”.
The stack we will build will have these characteristics:
- It has to be easily configurable;
- It has to have the very basic requirements needed to run Symfony;
- It has a high probability, so we can use it in other projects without Docker to start dockerize them.
So, our stack will be this:
- Operating system: Ubuntu;
- PHP Version: 7.2 (this is the best choice, simply);
- Database: MySQL;
- Web server: Apache.
I’m going to use Apache because if you develop PHP applications, it is 99% probable that you are familiar with it.
Nginx is less common and requires another series of posts to be understood?.
Let’s maintain things simple: use Apache!
The same for MySQL: for more complex applications you may need MongoDB, but for a basic configuration and, mostly, to understand the basics of Docker, it is better to use something simple and familiar as MySQL instead of MongoDB of which you may have never heard of (do you live on Mars??).
To get this stack up and running we have to do two things:
- Create a
docker-compose.yaml
file in the root folder of our project; - Define the components of our stack.
So, let’s start with PHP creating a docker-compose.yaml
file in the root folder of the project:
# docker-compose.yaml
version: '3.7'
services:
language:
image: php:7.2
Now we can run docker-compose up -d
and we will have a service with PHP up and running:
Creating network "app-test-www_default" with the default driver
Creating app-test-www_language_1 ... done
Attaching to app-test-www_language_1
language_1 | Interactive shell
language_1 |
app-test-www_language_1 exited with code 0
Docker will start downloading a lot of things and the process may take a bit of time: be patient!?
When the exit code is 0
, then all gone well.
NOTE: the -d
flag means “detached”: this makes the console available again after Docker Compose finishes. If you don’t pass it, you will need to open a new console window to continue to use the command line.
Things to note here
version
: is the version of the compose file.
As told above, the version you use in your compose file depends on the Docker version you are running. Check the compatibility matrix for more information.services.language
: “language” here is the name of the service we are creating. It is an arbitrary string we use to name the service.
Change this to “php” if you like: I used “language” just to show you how does it work and that it can be changed and is not related in any way to the image you are using.
You can call the service as you like, alsomy_astonishing_and_super_power_service
(but I warmly suggest you to use really shorter names!?)service.language.image
: defines the image we want to use to build the service.
Here we are using the image of PHP 7.2. Later in this series of posts, I will explain to you how to find the images you may need and how to understand how to use them: be patient!?
So, we now have PHP configured: we still need other components to make our Symfony app run on Docker.
We need:
- The operating system;
- The web server;
- A database.
Let’s configure them:
version: '3.7'
services:
# "php" was "language" in previous example
php:
image: php:7.2
# Configure the database
mysql:
image: mysql:5.7
# Configure Apache
apache:
image: httpd:2.4
Now, running again docker-compose up -d
reports this (note that Docker may first download images, so you will see a lot of logging before the lines below):
Aerendir$ docker-compose up -d
Starting app-test-www_mysql_1 ... done
Starting app-test-www_php_1 ... done
Creating app-test-www_apache_1 ... done
At this point, we have all the services up and running.
To verify this, other than the previous messages with “done”, you can do this:
Aerendir$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6d9e627c81f5 httpd:2.4 "httpd-foreground" 18 minutes ago Up 18 minutes 80/tcp app-test-www_apache_1
As you can see, currently I have only one container running and it is the Apache one.
If you are asking yourself, yes, I run this command 18 minutes later because in the meantime I gone out with Loki, my pet. ♥️
(Ok, he’s not a cat, but he is anyway a really pretty dog!?)
Another thing you should note: there is only one service, the Apache one: where are the others? There seems that MySQL and PHP aren’t running ?
But anyway there is a container running: Let’s go exploring it without taking care of the others (for the moment!?).
Exploring the Apache container
To explore the container running Apache, use the command docker exec -it [container_id] bash
:
Aerendir$ docker exec -it 6d9e627c81f5 bash
root@6d9e627c81f5:/usr/local/apache2# ls
bin build cgi-bin conf error htdocs icons include logs modules
NOTE: the -i
flag stands for “–interactive” and makes possible to interact with the console that we called with the -t
flag, that allocates a pseudo-tty console.
So, instead of using docker exec -i -t ...
, we condensed the command into docker exec -it ...
. More info here and here.
As you can see, we have some folders: from here you can go around with the commands cd
to change the directory (ex.: cd bin
) and with the command ls
to see what’s inside the current folder (ex.: ls
).
root@6d9e627c81f5:/usr/local/apache2# cd bin
root@6d9e627c81f5:/usr/local/apache2/bin# ls
ab apachectl apxs checkgid dbmmanage envvars envvars-std fcgistarter htcacheclean htdbm htdigest htpasswd httpd httxt2dbm logresolve rotatelogs suexe
To come back to your user, out of Docker, simply type exit
:
root@6d9e627c81f5:/usr/local/apache2/bin# exit
exit
MacBook-Pro-di-Aerendir:app-test-www Aerendir$
Now that we know how to enter a container, it’s time to understand where are our MySQL and PHP: are they lost?
Debugging MySQL and PHP in Docker
We defined three services in our docker-compose.yaml
file: Apache, MySQL and PHP.
But our container seems have lost MySQL and PHP: where are they gone?
If you go to some paragraphs above, you notice we used the command docker-compose up -d
: the -d
flag means “detached”, so we have again the console available.
Try to not use the -d
flag to run the command (first, run docker-compose down
!):
Aerendir$ docker-compose down
Stopping app-test-www_apache_1 ... done
Removing app-test-www_mysql_1 ... done
Removing app-test-www_php_1 ... done
Removing app-test-www_apache_1 ... done
Removing network app-test-www_default
And now we can up
again to see what happens:
Aerendir$ docker-compose up
Creating network "app-test-www_default" with the default driver
Creating app-test-www_mysql_1 ... done
Creating app-test-www_apache_1 ... done
Creating app-test-www_php_1 ... done
Attaching to app-test-www_mysql_1, app-test-www_apache_1, app-test-www_php_1
mysql_1 | error: database is uninitialized and password option is not specified
mysql_1 | You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD
php_1 | Interactive shell
php_1 |
apache_1 | AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 192.168.96.3. Set the 'ServerName' directive globally to suppress this message
apache_1 | AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 192.168.96.3. Set the 'ServerName' directive globally to suppress this message
apache_1 | [Sun Aug 12 15:14:59.981146 2018] [mpm_event:notice] [pid 1:tid 140196935767936] AH00489: Apache/2.4.33 (Unix) configured -- resuming normal operations
apache_1 | [Sun Aug 12 15:14:59.981380 2018] [core:notice] [pid 1:tid 140196935767936] AH00094: Command line: 'httpd -D FOREGROUND'
app-test-www_mysql_1 exited with code 1
app-test-www_php_1 exited with code 0
As you can see, using the detached mode all seems going well; but removing the -d
flag, some errors seem to arise:
mysql_1 | error: database is uninitialized and password option is not specified
apache_1 | AH00558: httpd: Could not reliably determine the server’s fully qualified domain name, using 192.168.96.3. Set the ‘ServerName’ directive globally to suppress this message
So we have two errors: one from Apache and one from MySQL: Let’s fix them!
Further configuring MySQL
MySQL returned this error:
mysql1 | error: database is uninitialized and password option is not specified
mysql_1 | You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD
So, it is obvious what we have to do to fix it: set the required environment variables!
So, add the node services.mysql.environment
and set the variable MYSQL_ROOT_PASSWORD
:
version: '3.7'
services:
# "php" was "language" in previous example
...
# Configure the database
mysql:
image: mysql:5.7
# Add the MYSQL_ROOT_PASSWORD to the environment variables
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-root}
# Configure Apache
...
NOTE: We are using a default value of root
if the env variable MYSQL_ROOT_PASSWORD
is not set; if it is set, instead, we use its value. This syntax is of Bash and is the one to set default values.
It is useful as it permits us to use an .env
file locally (read by the Symdony’s Dotenv Component) while using real environment variables on production.
Now, on your keyboard, press CTRL + C
to kill the current Docker container, then run docker-compose up
(without -d
):
Aerendir$ docker-compose up
Recreating app-test-www_mysql_1 ... done
Starting app-test-www_php_1 ... done
Starting app-test-www_apache_1 ... done
Attaching to app-test-www_php_1, app-test-www_apache_1, app-test-www_mysql_1
php_1 | Interactive shell
php_1 |
apache_1 | AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 192.168.96.3. Set the 'ServerName' directive globally to suppress this message
apache_1 | AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 192.168.96.3. Set the 'ServerName' directive globally to suppress this message
apache_1 | [Sun Aug 12 15:21:41.877927 2018] [mpm_event:notice] [pid 1:tid 139714071852928] AH00489: Apache/2.4.33 (Unix) configured -- resuming normal operations
apache_1 | [Sun Aug 12 15:21:41.878086 2018] [core:notice] [pid 1:tid 139714071852928] AH00094: Command line: 'httpd -D FOREGROUND'
mysql_1 | Initializing database
...
The console will continue to give you log messages that tell you what is happening for MySQL service.
It seems that the MySQL errors are gone: let’s verify the container started.
You are not in detached mode, so, open a new console window and use the docker ps
command to see the currently active containers:
Aerendir$: docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
baa0652ed2a6 mysql:5.7 "docker-entrypoint.s…" 4 minutes ago Up 4 minutes 3306/tcp app-test-www_mysql_1
f1da511ff5b5 httpd:2.4 "httpd-foreground" 4 minutes ago Up 4 minutes 80/tcp app-test-www_apache_1
As you can see, we now have two services running in our container: we have fixed MySQL!?
The last one remaining is PHP.
Further configuring PHP
Where is PHP? Our container only shows Apache and MySQL and there is no trace of PHP.
Let’s start from the beginning…
On any machine you run php
, to see if it is running, you can simply use the command php -v
: this will return the version of PHP currently used (the below command is run on my machine, not on any Docker container!):
Aerendir$ php -v
PHP 7.2.2 (cli) (built: Feb 1 2018 13:23:34) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
with Zend OPcache v7.2.2, Copyright (c) 1999-2018, by Zend Technologies
with Xdebug v2.6.0, Copyright (c) 2002-2018, by Derick Rethans
with blackfire v1.21.0~mac-x64-non_zts72, https://blackfire.io, by Blackfire
As you can see, on my machine I’m using version 7.2 of PHP and I also have OPcache, Xdebug and Blackfire.
But how to run this command in a Docker container?
Try this:
Aerendir$ docker run php -v
PHP 7.2.8 (cli) (built: Jul 21 2018 07:47:51) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
Olè! PHP is running!
The version is the 7.2.8, different than the version 7.2.2 that is run on my local machine, and there is no trace of OPcache nor of Xdebug or of Blackfire.
So, we have basically nothing to debug: we have all the three services up and running!
Yes, I know what you are asking: then, if PHP is running, why isn’t it shown in the list of running containers?
I will clarify this later in this series of posts, for the moment put apart this question.
SPOILER: We made an error configuring the PHP service!?…?
Now it is time to access the web server from the browser!
Accessing the web server from the browser
Before accessing our Symfony app from the browser, we have to first figure out how to simply access the web server from the browser.
Let’s run again the ps
command:
Aerendir$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
baa0652ed2a6 mysql:5.7 "docker-entrypoint.s…" 19 minutes ago Up 19 minutes 3306/tcp app-test-www_mysql_1
f1da511ff5b5 httpd:2.4 "httpd-foreground" 19 minutes ago Up 19 minutes 80/tcp app-test-www_apache_1
As you can see, the httpd:2.4
image is exposing the port 80
: this is the default port used by the Apache web server for TCP connections.
But this port is exposed on Docker, not on our computer.
In fact, if you try to access http://localhost:80
you will continue to see your local web server.
How can we access the container’s web server instead?
We need to bind the port 80
from the Docker container to one of the ports of our computer.
The Docker’s documentation about Container networking states this:
By default, when you create a container, it does not publish any of its ports to the outside world.
Then it suggests using the flag --publish
, but this is not useful in our case: we need to understand how to bind Docker ports to our machine ports (our machine is the Docker’s host), but using the Compose file.
This means a simple thing: you cannot access the services from your browser as Docker doesn’t make them “public”: they exist, but can be accessed only by other Docker’s services.
If you want to access the services from the world outside Docker, then you need to “bind” them.
The “binding” is a way of saying to Docker that all the traffic to the port of your computer has to be forwarded to the port it internally assigns to the service.
So, anticipating what we will do, we will bind the port 80
of the Docker service to the port 8100
of our computer. This will tell Docker that all the traffic to the port 8100
on our computer has to be forwarded to the port 80
he has mapped internally.
Docker offers two ways to create the “binding”.
So, let’s read the documentation of Compose file: there we can find two useful keys: expose
and ports
:
Expose
Expose ports without publishing them to the host machine – they’ll only be accessible to linked services. Only the internal port can be specified.
…
Ports
Expose ports.
Not so useful as descriptions, aren’t they? Honestly, they are also a bit confusing ?
Let’s try to clarify their meaning.
Basically, both expose
and ports
makes possible for a Docker container to make some ports reachable.
The main differences between the two are these:
expose
makes those ports reachable only by services/containers in the same Docker network, but not from outside of it;ports
, instead, makes the ports reachable from outside the network.
So, guess what, our best option is using ports
.
The Docker’s documentation about ports clearly states a caveat:
Note: When mapping ports in the
HOST:CONTAINER
format, you may experience erroneous results when using a container port lower than 60, because YAML parses numbers in the formatxx:yy
as a base-60 value. For this reason, we recommend always explicitly specifying your port mappings as strings.
This is relevant because you can do something like this:
version: '3.7'
services:
apache:
...
ports:
- 80
or you can do something like this:
version: '3.7'
services:
apache:
...
ports:
- '8100:80'
In the first case, we can simply use an integer, but in the second case, as we are using the format HOST:CONTAINER
, we need to set the port mapping as strings (using “‘” – an “apex”) and not as integers to avoid the before mentioned problems in parsing.
More, in the first case, the port 80
will be bound to a random port of the host machine (again, the host machine is our machine that is running Docker): this is not useful, as with a random port we cannot anyway access the Apache server running in the container! (each time we should first check which is the current port assigned – using docker ps
– and this is not useful).
So, our port binding of Apache ports is something like this:
version: '3.7'
services:
# "php" was "language" in previous example
php:
...
# Configure the database
mysql:
...
# Configure Apache
apache:
image: httpd:2.4
ports:
- "8100:80"
Run the command docker-compose up -d
to apply the new configuration.
Now, accessing the URL http://127.0.0.1:8100
we will see the default Apache index.html
page with the famous “It Works!” message:
But where is this file located?
Good, a bit of context.
Apache stores the files to be served in the so-called “document root“.
So we need to find this document root in our container.
As already shown above, run docker ps
to get the id of the container we want to enter:
Aerendir$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
718b8afc0626 mysql:5.7 "docker-entrypoint.s…" 34 minutes ago Up 5 seconds 3306/tcp app-test-www_mysql_1
57aa966336a2 httpd:2.4 "httpd-foreground" 34 minutes ago Up 5 seconds 8100->80/tcp app-test-www_apache_1
Then, we enter the container running this:
Aerendir$ docker exec -it 57aa966336a2 bash
Now we can start exploring the contents of the container with ls
:
root@57aa966336a2:/usr/local/apache2# ls
bin build cgi-bin conf error htdocs icons include logs modules
Hey, htdocs
is one of the default names of the Apache’s document root folders!
root@57aa966336a2:/usr/local/apache2# cd htdocs
root@57aa966336a2:/usr/local/apache2/htdocs# ls
index.html
Found! The file index.html
is the one that showed us the message “It works!”: this is our document root!
We spotted it!?
Conclusions and next steps
For this post I think we have covered a lot of things:
We learned:
- How to configure services with Docker;
- How to access them;
- Some basic debugging techniques;
- How to bind the ports of a Docker’s services to the ports of our computer;
- How to access the web server from our browser.
In doing this we also learned some basic concepts of Docker like what is a container, what is a service, some basic information about the networking capabilities of Docker (the binding of ports is “networking”!).
But now we also have some questions:
- Why the PHP service doesn’t appear in the list of services?
- And why, also if it doesn’t appear in such list, anyway PHP seems to be running?
And most important: now that we have our web server up and running, how can we move our Symfony files into it to start developing our app using Docker?
Duh, a lot of questions! (and many more will arise!?).
Well, trust me, I will answer all of them in the next post of this series.
For the moment, I will answer the last question: how to move the Symfony’s files into the web server run by Docker.
Remember to “Make. Ideas. Happen.”.
In the meantime, I wish you flocking users!
Next post: Moving Symfony files in the Docker container.
sultan says
really helpful post