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:
- MySQL
- nginx
- Symfony
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.
docker-compose.yml
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 db
and php
services.
We can do better.
Let's move these to an env
file:
touch .env
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
Remember, ./
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 docker_build
, docker_push
, and bp
in the previous video.
The new command: dev
, can be run with make dev
.
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 docker-compose.yml
file.
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 - docker-compose up
.
-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.
Volumes
For each of the three defined services (db
, nginx
, php
), one of the key areas of config is volumes
.
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 volumes
.
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 volumes/mysql_dev
.
Inside the running container, this data will map directly to /var/lib/mysql
.
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
file:
# ./.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 /var/www/dev
.
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.
Kinda neat.
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.
Finally ./:/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 docker build
.
Connectivity
There are two further important parts of the docker-compose.yml
file.
Firstly, 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 docker-symfony-example
, our db
service would end up with the hostname of:
dockersymfonyexample_db_1
Not what we are after, and importantly, this will break things.
Our nginx config for example, expects to be able to pass upstream to php
. Not dockersymfonyexample_php_1
.
Anyway, 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.
By using 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:
127.0.0.1:81
And hit your Dockerised Symfony stack. Nice.