In this series I’m gonna share all that I’ve learned while switching from a Vagrant powered environment – running all required software in a single VirtualBox instance – to a Dockerized setup where every process runs in a separate container. But what exactly is Docker? From the Docker site:
Docker containers wrap up a piece of software in a complete filesystem that contains everything it needs to run: code, runtime, system tools, system libraries – anything you can install on a server. This guarantees that it will always run the same, regardless of the environment it is running in.
Now that sounds great, doesn’t it? As a matter of fact it does, but you have to get a grip on the concept before it starts paying off. In these blog post series I’ll show you how to create a multi container Symfony application and how to get the full potential out of it. For now I’ll only focus on using Docker as develop environment. Perhaps a new series for Docker in production will follow in the near future :).
I’m a Mac OS X user so some problems I describe are related to the fact that I have to use a virtualisation layer to use Docker. If you’re happen to be on Linux, you can just skip those sections.
Installation
VirtualBox is required to run a Linux virtual machine so make sure you have a recent version installed. On the Docker site follow the installation instructions. When you’re done you should have docker
, docker-compose
and docker-machine
binary available to you.
Create Linux virtual box
With docker-machine
it’s fairly easy to create and manage a virtual machine for running Docker in. Let’s create a new instance:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$ docker-machine create docker-tutorial --driver virtualbox Running pre-create checks... Creating machine... (docker-tutorial) Creating VirtualBox VM... (docker-tutorial) Creating SSH key... (docker-tutorial) Starting VM... Waiting for machine to be running, this may take a few minutes... Machine is running, waiting for SSH to be available... Detecting operating system of created instance... Detecting the provisioner... Provisioning with boot2docker... Copying certs to the local machine directory... Copying certs to the remote machine... Setting Docker configuration on the remote daemon... Checking connection to Docker... Docker is up and running! To see how to connect Docker to this machine, run: docker-machine env docker-tutorial |
If you need a box with more memory (this can happen when you have a lot of containers) you can create one with more RAM with --virtualbox-memory "2048"
. You’ll receive a no space left on device error when that happens.
All left to do is setting correct environment variables so Docker daemon knows how to connect our box:
1 |
$ eval "$(docker-machine env docker-tutorial)" |
Now let’s try to see if it works by listing the containers:
1 2 |
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES |
Create Symfony project
Now we’re ready to create a new Symfony project:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$ symfony new docker-tutorial Downloading Symfony... 4.94 MB/4.94 MB ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 100% Preparing project... ✔ Symfony 3.0.1 was successfully installed. Now you can: * Change your current directory to /Users/richard/projects/docker-tutorial * Configure your application in app/config/parameters.yml file. * Run your application: 1. Execute the php bin/console server:run command. 2. Browse to the http://localhost:8000 URL. * Read the documentation at http://symfony.com/doc |
Configuring docker-compose
Docker-compose reads its configuration from a docker-compose.yml
file, so create a empty one in the root of your shiny new project.
You should end up with a directory structure like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
. ├── app ├── bin ├── src ├── tests ├── var ├── vendor ├── web ├── README.md ├── composer.json ├── composer.lock ├── docker-compose.yml └── phpunit.xml.dist |
Install php and nginx
We’re almost there, so hang on. Obviously we’re gonna need php and a webserver so let’s install php-fpm and nginx. Never reinvent the wheel, so when you need a service containerized, always search it on Docker hub. As with bundles: there’s a container for that. We’ll use the official php and nginx images for now.
Heads up: When adding more images to your configuration take note from which image they derive. Most images extends from debian:jessie, which you probably want for your images. Docker works with a layered file system, so if all your images derive from the same parent, that will speed up the build process and also consume less space (also during transfer!).
Edit your docker-compose.yml
like this:
1 2 3 4 5 6 7 |
nginx: image: nginx:latest ports: - "8080:80" php: image: php:7.0-fpm |
The root element is the name of the container, you can pick whatever you like. I always try to keep these short so it’s easier (less typing) when running commands against a specific container.
The image
field tells docker-compose
which image we want to use for our container. The ports
field allows us to expose ports on the container and forward port(s) from the container to the host, so we actually connect to the container. The value "8080:80"
means we’re exposing port 80 on the container and forward it to port 8080 on the host.
You should start the containers now:
1 |
$ docker-composer up -d |
Docker will pull the images from the registry, build and start them. The -d
flags tells Docker to run the containers daemonized in the background. I’ll get back on that later. When it’s done, verify if they’re both up and running:
1 2 3 4 5 |
$ docker-compose ps Name Command State Ports ------------------------------------------------------------------------------------- dockertutorial_nginx_1 nginx -g daemon off; Up 443/tcp, 0.0.0.0:8080->80/tcp dockertutorial_php_1 php-fpm Up 9000/tcp |
Connecting to the box
We’ve forwarded port 8080 to our host, but connecting to localhost:8080 doesn’t work (you did try the link, didn’t you? :)). Because Docker runs in a virtual machine, we need to figure out its IP so we can connect it. Of course this isn’t very difficult:
1 2 |
$ docker-machine ip docker-tutorial 192.168.99.100 |
Let’s try that IP on port 8080 and you’ll see it works: http://192.168.99.100:8080/. You’ll want to add an entry for that IP in your /etc/hosts
file. Let’s pick symfony3.dev
for now.
As you’ve probably discovered by now, we’re presented the default nginx page and not our shiny new Symfony application. To fix this we have to link the php container to the nginx container so they can communicate with each other. The php-fpm container needs access to our project’s php files in order to parse and serve them. Also, the nginx container requires a nginx configuration. We have to alter the Docker image and for that we need a Dockerfile.
Custom Dockerfile
The Dockerfile represents every step to be taken before the container is ready to use. Normally you would use a configuration management tool (Ansible, Puppet, Chef) to accomplish this, but in Docker you manage this via the Dockerfile.
It’s import to know that each line should contain one step. Each line creates a new layer and the number of layers is limited. One logical step per line improves the caching mechanism. For more information regarding this topic, refer to the best practices.
To configure nginx we’re going to use the nginx configuration supplied by the Symfony team. We have to copy it into the container. Create a new directory docker/nginx
in the project root and add the following Dockerfile:
1 2 3 |
FROM nginx:latest COPY symfony3.conf /etc/nginx/conf.d/symfony3.conf |
Create a symfony3.conf
file in that docker/nginx
directory as well and fill it with the following configuration:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
server { server_name symfony3.dev www.symfony3.dev; root /app/web; location / { # try to serve file directly, fallback to app.php try_files $uri /app.php$is_args$args; } # DEV # This rule should only be placed on your development environment # In production, don't include this and don't deploy app_dev.php or config.php location ~ ^/(app_dev|config)\.php(/|$) { fastcgi_pass php:9000; fastcgi_split_path_info ^(.+\.php)(/.*)$; include fastcgi_params; # When you are using symlinks to link the document root to the # current version of your application, you should pass the real # application path instead of the path to the symlink to PHP # FPM. # Otherwise, PHP's OPcache may not properly detect changes to # your PHP files (see https://github.com/zendtech/ZendOptimizerPlus/issues/126 # for more information). fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $realpath_root; } # PROD location ~ ^/app\.php(/|$) { fastcgi_pass php:9000; fastcgi_split_path_info ^(.+\.php)(/.*)$; include fastcgi_params; # When you are using symlinks to link the document root to the # current version of your application, you should pass the real # application path instead of the path to the symlink to PHP # FPM. # Otherwise, PHP's OPcache may not properly detect changes to # your PHP files (see https://github.com/zendtech/ZendOptimizerPlus/issues/126 # for more information). fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $realpath_root; # Prevents URIs that include the front controller. This will 404: # http://symfony3.dev/app.php/some-path # Remove the internal directive to allow URIs like this internal; } } |
In case you haven’t you should add symfony3.dev
to your /etc/hosts
file with the IP from docker-machine ip docker-tutorial
.
Now let’s put it all together and update our docker-compose.yml
accordingly:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
nginx: build: docker/nginx ports: - "8080:80" links: - php volumes: - ./:/app php: image: php:7.0-fpm volumes: - ./:/app working_dir: /app |
Take note of the changes we’ve applied: image
under nginx
is replaced with build: docker/nginx
which refers to the directory where the Dockerfile
resides. The nginx
container has a links
key where we link it to the php
container. Both containers have a volumes
key where we mount the current directory into the container under /app
path. This way the container has access to the project files.
Stop all containers and build them:
1 2 3 4 5 6 7 8 9 10 |
$ docker-compose stop $ docker-compose build php uses an image, skipping Building nginx Step 1 : FROM nginx:latest ---> 5328fdfe9b8e Step 2 : COPY symfony3.conf /etc/nginx/conf.d/symfony3.conf ---> Using cache ---> 19c62e44a1bb Successfully built 19c62e44a1bb |
Then start them again:
1 |
$ docker-composer up -d |
When you visit http://symfony3.dev:8080/app_dev.php in your browser, you’ll see the You are not allowed to access this file. Check app_dev.php for more information. message. Remove the access check from app_dev.php
and try again.
Unfortunately another well known error pops up: Failed to write cache file “/app/var/cache/dev/classes.php”..
Permissions
In my opinion the best solution to this problem is to run the console commands and php-fpm process under the same user. Without any modifications, the console commands are run under root and the php-fpm process runs under www-data. To accomplish this we also have to use a Dockerfile
for the php container.
Again, stop all containers:
1 |
$ docker-compose stop |
Create a new directory php-fpm
under the docker
directory. Add the following Dockerfile
:
1 2 3 4 5 6 7 |
FROM php:7.0-fpm RUN useradd -ms /bin/bash vagrant USER vagrant WORKDIR /app |
Also, add the following php-fpm.conf
file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
; This file was initially adapated from the output of: (on PHP 5.6) ; grep -vE '^;|^ *$' /usr/local/etc/php-fpm.conf.default [global] error_log = /proc/stderr daemonize = no [www] ; if we send this to /proc/self/fd/1, it never appears access.log = /proc/stdout ; this does the trick for changing the user user = vagrant group = vagrant listen = [::]:9000 pm = dynamic pm.max_children = 5 pm.start_servers = 2 pm.min_spare_servers = 1 pm.max_spare_servers = 3 clear_env = no ; Ensure worker stdout and stderr are sent to the main error log. catch_workers_output = yes chdir = /app/web |
Because I suck at naming new users I just use vagrant
as my development user. Think of it as a tribute to vagrant :). The docker
directory tree should be:
1 2 3 4 5 6 7 |
. ├── nginx │ ├── Dockerfile │ └── symfony3.conf └── php-fpm ├── Dockerfile ├── php-fpm.conf |
Now build and run the containers:
1 2 |
$ docker-compose build $ docker-composer up -d |
If you visit http://symfony3.dev:8080/app_dev.php now, you’ll see the Symfony welcome pages smileys at you. With this “hello world” for Symfony working we end this first post.
Next post I’ll show you how to speed up things if you’re on a Mac (the default Symfony app takes ~2000 ms to load in the current situation). Also, I’ll show you the possibilities to store your data when working with containers.