I've been using a combination of Vagrant, Virtualbox and Ansible for a while now when developing my Symfony (and other PHP) applications and it has worked out just fine. Ever since I heard about Docker a few years ago, it seemed like a good idea, but a bit of a pain to set up and manage. But when my colleague Jordy showed me Docker Compose last week, which seemed a lot simpler and faster than my current flow, I was eager to try that out.
Docker vs Vagrant
At work our projects are quite complex and require a lot of different services like RabbitMQ, Couchbase and Elasticsearch, but my personal project are much more traditional, requiring only a webserver, PHP and MySQL. Fact of the matter is that I could reuse the same Vagrant/Ansible configuration for all my projects, and it would work just fine.
However, there are a few problems with that. Every vagrant box I spawn needs a whole operating system to run, a disk image containing said OS and the CPU and RAM resources to run it. When I use Docker, I can use the same image per service (eg. MySQL or nginx) and they would only use that diskspace once. It also doesn't need a whole operating system to run, since it runs isolated on my already running Linux kernel.
Aside from that, starting up a few Docker containers is much quicker than booting a whole virtual machine with a lot of services (and some stuff I don't really need.)
My Docker Compose setup
In the root of my project I added a
docker-compose.yml file which contains something like this. (I stripped it down a bit and made it more generic...)
# docker-compose.yml version: '3' services: mysql: image: mysql:5.7 ports: - 3306:3306 volumes: - myproject_mysql:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: rootpassword MYSQL_DATABASE: myproject MYSQL_USER: myproject MYSQL_PASSWORD: myproject nginx: image: nginx:1.12-alpine ports: - 8080:80 volumes: - ./docker/nginx/vhost.conf:/etc/nginx/conf.d/default.conf:ro - ./:/code links: - php php: build: docker/php volumes: - ./:/code volumes: myproject_mysql: driver: local
I defined a volume called
myproject_mysql and mounted it in the
mysql service on
/var/lib/mysql. This is the path where MySQL stores all its data. By mounting the volume, we persist the data outside of the Docker container. This makes sure the data is saved when the container is shut down and started up again.
nginx services are quite straight forward and use images directly from the Docker Hub with some custom configuration. You can tell because it uses the
I also mounted the
./docker/nginx/vhost.conf file into the nginx container. I used the nginx vhost file provided in the Symfony documentation and changed
php service is different, because it needs to be built. The
build: directive tells Docker Compose where it can find the
Dockerfile containing the steps to build the container. In this case it is in
docker/php/Dockerfile, relative to the
It might contain the following:
# docker/php/Dockerfile FROM php:7-fpm RUN apt-get update && apt-get install -y \ git \ zlib1g-dev \ libicu-dev \ libxml2-dev \ libaio1 \ vim \ unzip RUN docker-php-ext-install \ pdo \ pdo_mysql \ opcache \ intl \ xml RUN docker-php-ext-enable \ pdo \ pdo_mysql \ intl \ xml RUN usermod -u 1000 www-data WORKDIR /code
FROM directive tells on which Docker image it should be based. These images are also the ones you can find in the Docker Hub. The
RUN directives run a command inside the container. As you probably can tell, the
RUN commands I defined install and enable some PHP extensions.
RUN usermod -u 1000 www-data is a bit a hack, by the way. Symfony writes cache and logs to the
var directory and to prevent permission problems, we'll just give the
www-data user in the container the same uid as the user I'm using on my PC. You should note that if your uid is not 1000, this will not work for you and you'll need to change it accordingly.
WORKDIR finally set the default working directory.
Running the containers
docker/nginx/vhost.conf files are in place, we can start up the containers.
You can use the following command to start all containers in foreground mode:
In foreground mode, the logs for all containers will be shown as if you were using
tail -f. If you press
CTRL-C, the containers will shut down. What I prefer to do is run the containers in detached mode:
docker-composer up -d
This will start the containers and return to your shell after that. If you want to see the latest logs, you can run:
Or you could follow (
tail -f) the logs by adding
Shut everything down using:
-v, you'll also delete the volumes you've defined.
Accessing the webserver, php or mysql
Now you've got your containers running, you might want to access the webserver to check if your project still runs correctly. Since we've added some port mappings in the
docker-compose.yml file, you'll easily be able to.
nginx we've mapped port
80. This means you can access
http://localhost:8080 and it maps to port
80 inside the nginx container.
We did the same on the mysql container, mapping
mysql://localhost to port
3306 inside the container. That will allow you to access your mysql server using something like DBeaver or MySQL Workbench.
You may also run your Symfony console commands from inside the php container. You can do this using this command:
docker-compose exec --user www-data php bin/console <arguments>
--user www-data argument ensures you don't get the permission issues I described earlier.
This is all a whole lot faster than what I was used to when running Vagrant boxes. I also found it quite easy to set up. Switching between versions of services has also been made much easier since we no longer depend on a base operating system anymore.
The only problem I was stuck on quite a while was the permissions problem, but settings the uid of the
www-data user to my local hosts user id and using
--user www-data when running php commands from inside the
php box seems to be a nice and simple solution.