Docker Compose Tutorial
We now have all our Docker images created, and our new Symfony files ready and waiting to run inside a Docker container.
We have three images:
And crucially, they all need to talk to each other in order for any of this to work.
We could try and co-ordinate this by hand. But in the real world, everyone uses
docker-compose for this task.
If you haven't already done so, please install Docker Compose before continuing.
I'm going to dive right into this:
# ./docker-compose.yml version: '3' services: db: image: mysql:5.7.19 hostname: db volumes: - "./volumes/mysql_dev:/var/lib/mysql" environment: - MYSQL_ROOT_PASSWORD=password - MYSQL_DATABASE=db_dev - MYSQL_USER=dbuser - MYSQL_PASSWORD=dbpassword nginx: image: docker.io/codereviewvideos/nginx.symfony.dev hostname: nginx volumes: - "./volumes/nginx/logs:/var/log/nginx/" - "./:/var/www/dev" ports: - 81:80 depends_on: - php php: # build: # context: ./ # args: # WORK_DIR: /var/wwww/dev image: docker.io/codereviewvideos/symfony.dev hostname: php volumes: - "./volumes/php/var/cache:/var/www/dev/var/cache/:rw" - "./volumes/php/var/sessions:/var/www/dev/var/sessions/:rw" - "./volumes/php/var/logs:/var/www/dev/var/logs/:rw" - "./:/var/www/dev" environment: - SECRET_KEY=SOME_NOTSO_SUPERSECRETKEYHERE - DB_HOST=mysql - DB_PORT=3306 - DB_DATABASE=db_acceptance - DB_USER=dbuser - DB_PASSWORD=dbpassword depends_on: - db
Wew, that's a lot of stuff.
To stress again, using environment variables here may not be the right choice for you.
Whilst fine in development, in production environment variables can lead to inadvertent security exposures. This is because environment variables are written to your log files when an error occurs.
Please consider this warning.
Anyway, one thing that sucks about this current config is the repeated use of the environment variables for both the
We can do better.
Let's move these to an
Then inside the
.env file, add the contents:
SECRET_KEY=SOME_NOTSO_SUPERSECRETKEYHERE MYSQL_HOST=mysql MYSQL_PORT=3306 MYSQL_DATABASE=db_acceptance MYSQL_USER=dbuser MYSQL_PASSWORD=dbpassword
Save and close.
We can now update the
docker-compose.yml file to use the
.env file instead:
# ./docker-compose.yml version: '3' services: db: image: mysql:5.7.19 hostname: db volumes: - "./volumes/mysql_dev:/var/lib/mysql" env_file: - ./.env nginx: image: docker.io/codereviewvideos/nginx.symfony.dev hostname: nginx volumes: - "./volumes/nginx/logs:/var/log/nginx/" - "./:/var/www/dev" ports: - 81:80 depends_on: - php php: # build: # context: ./ # args: # WORK_DIR: /var/wwww/dev image: docker.io/codereviewvideos/symfony.dev hostname: php volumes: - "./volumes/php/var/cache:/var/www/dev/var/cache/:rw" - "./volumes/php/var/sessions:/var/www/dev/var/sessions/:rw" - "./volumes/php/var/logs:/var/www/dev/var/logs/:rw" - "./:/var/www/dev" env_file: - ./.env depends_on: - db
./ indicates the current directory. In other words, the
.env file should live in the same directory as the
docker-compose.yml file for this line to work. It need not do, but be sure to update the path accordingly if not.
Make Your Life Easier
Before we go further, let's update the
Makefile to simplify our command line activities
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 dev: @docker-compose down && \ docker-compose build --pull --no-cache && \ docker-compose \ -f docker-compose.yml \ up -d --remove-orphans
Now, super important: a
Makefile uses tabs. Nasty tabs. So be careful if making changes.
We already covered
bp in the previous video.
The new command:
dev, can be run with
This command, as the name implies, makes our development environment come to life.
To begin with,
docker-compose down shuts down any running containers as described in our local
docker-compose.yml file. This will not shut down any other containers you may have running in other projects.
If you don't have any running containers already then this command doesn't do very much, but that's fine.
&& simply means also run the following command.
&& \ simply means run the following command, but for readability let me split my command over multiple lines :)
docker-compose build --pull --no-cache
This line would execute any
build instructions in our
As it happens we do have one, but it's commented out:
php: # build: # context: ./ # args: # WORK_DIR: /var/wwww/dev
If these lines were active then the build process would take place before our containers could be brought online. This may be useful to you, which is why I have left it in.
Personally, I very rarely use this.
docker-compose \ -f docker-compose.yml \ up -d --remove-orphans
This is effectively one big command split over three lines.
docker-compose is the command line utility needed to work with Docker Compose.
-f docker-compose.yml is how we tell Docker Compose which
docker-compose.yml file we want to run.
As it happens,
docker-compose.yml is the default file that will be used.
Why I make this explicit is because in a CI pipeline, it's very likely that you will need to override this, and the ordering does matter.
By way of example, a CI pipeline call might look more like this:
docker-compose \ -f docker-compose.yml \ -f docker-compose.ci.yml \ up -d --remove-orphans
Where the second file -
docker-compose.ci.yml - may contain overridden variables / volumes / ports / whatever needed for the CI environment.
up is the command we are running here -
-d is the detached flag, just like in the
docker run commands we have covered so far in this series. If we don't pass in this flag then your terminal window will be overtaken with log output.
--remove-orphans is a rather harsh sounding command :) What this will do is remove any containers that are no longer defined in your
docker-compose.yml file. This likely will not impact you, unless your projects start to grow, and you start to add and remove more containers.
Ultimately we just need to run
make dev and away we go.
For each of the three defined services (
php), one of the key areas of config is
db: volumes: - "./volumes/mysql_dev:/var/lib/mysql"
We're going to use bind mounts for simplicity.
Inside our project root, when we run
make dev (or the longer full
docker-compose...) command, a new directory will be created called
Inside this new
volumes directory, each container can store its data inside a subdirectory.
In this case, our
db service will store its data inside
Inside the running container, this data will map directly to
In other words, any database data from the container will really end up in our
volumes/mysql_dev directory. This way if the container is deleted, the database data remains in a directory local to our project.
This said, you very likely want to make the following change to your
# ./.gitignore # Docker /volumes/*
We very likely don't want our volume data ending up in our git repository.
Also, if using a
.dockerignore file, be sure to add this entry in there, too.
Our nginx config looks similar:
nginx volumes: - "./volumes/nginx/logs:/var/log/nginx/" - "./:/var/www/dev"
Accessing the log data can either be done via the CLI:
docker-compose exec nginx /bin/bash $ tail -f /var/log/nginx/symfony_access.log $ tail -f /var/log/nginx/symfony_error.log
Or substitute this out for whatever name you gave your logs in your nginx image.
Or you can view the logs locally by browsing to your
./volumes/nginx/logs directory. This can be handy as the logs hang around even if the container isn't running.
Also noticed here that we bind mount the project's root directory to
As covered, nginx will hand off (or pass
upstream) for PHP file requests, but the files still need to be locally available or nginx will have a meltdown.
PHP, of course, has the most volumes:
php: volumes: - "./volumes/php/var/cache:/var/www/dev/var/cache/:rw" - "./volumes/php/var/sessions:/var/www/dev/var/sessions/:rw" - "./volumes/php/var/logs:/var/www/dev/var/logs/:rw" - "./:/var/www/dev"
Cache, Sessions and Logs are all Symfony specific. If you were using this for a WordPress project you would have to expose different volumes here. Same for Laravel, or whatever.
You don't need to expose these directories for this process to work, but again, accessing this data after a container has been deleted can be extremely useful for debugging. This also impacts production, particularly the Sessions directory - unless you want everyone to be logged out when a new build goes live.
./:/var/www/dev ensures the local project files that we work on during development are those that are run by the container. This is in place of the files we really copied over during our
There are two further important parts of the
hostname seems redundant given that we need it for each of the defined
services, and its the same as the service name:
db: image: mysql:5.7.19 hostname: db
If we don't specify a hostname then Docker will generate one for us.
And it won't be pretty.
This is because by default,
docker-compose will create a network for us. This is very useful - so much so that we are using it without explicitly stating this fact. We will get to networking later.
The network that
docker-compose creates for us has a funky name.
It takes the name of the current directory and then concatenates it with the service name, and then an index.
Assuming our current directory name is
db service would end up with the hostname of:
Not what we are after, and importantly, this will break things.
Our nginx config for example, expects to be able to pass upstream to
hostname fixes this.
The order in which containers start up is important.
depends_on ensures the containers start in the order we define.
For example, if
nginx started but couldn't see
php, it may exit. If it exits, then because we haven't specified a
restart configuration then it would stay exited. Likely not what we want.
depends_on we can control when containers come online, which should hopefully alleviate this problem. Or just use
restart: always, and to heck with it :D (no don't do this).
At this point we have a working Dockerised Symfony stack. This is just the beginning, but you should now be able to connect on:
And hit your Dockerised Symfony stack. Nice.