Running Symfony with Docker on Mac

I have being working with Symfony since 2012, and I never feel as confident developing and deploying like I feel now with Docker.

I use to develop at home with Ubuntu but I've Mac at the office, and was a little bit tricky at the beginning.

I'll not explain what is docker machine, docker engine or docker compose, his documentation is much better than I can explain, believe me.

Installing docker

First download Docker toolbox you will need VirtualBox or similar.

Be familiar with Docker Machine, is where docker will run, and you will need it on the future.

To start the machine:

docker-machine start default

To connect the client with docker-engine:

eval $(docker-machine env default)

It's a good idea add it to .bash_profile

Now you have docker installed and running. Great!

No.

The virtual machine it's super slow. To solve that we'll change the file system to nfs, thanks to https://github.com/adlogix/docker-machine-nfs.

To install it run:

curl -s https://raw.githubusercontent.com/adlogix/docker-machine-nfs/master/docker-machine-nfs.sh | 
sudo tee /usr/local/bin/docker-machine-nfs > /dev/null && \
sudo chmod +x /usr/local/bin/docker-machine-nfs

Exec on the terminal:

docker-machine-nfs default

Ok, you are done and the machine rebooted with nfs. Maybe you don't know it but this simple change improves a lot the development process.

Preparing the environment

Let's go with Symfony, I always use the installer:

symfony new demo
cd demo

Done, now the images.

mkdir -p etc/build/dev
cd etc/build/dev

Start with PHP

We'll need few things for our dev environment:

  • The fpm config
  • The ssh connection to configure the IDE for remote interpreter
  • Xdebug for, obviously, debug.

*/etc/build/dev/php7/php-fpm.conf

[global]

error_log = /app/var/logs/php-fpm
daemonize = no

[www]

; if we send this to /proc/self/fd/1, it never appears
access.log = /app/var/logs/php-fpm

; this does the trick for changing the user
user = jarco
group = jarco

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

/etc/build/dev/php7/php.ini

memory_limit=256M

[Date]
date.timezone = Europe/Madrid

/etc/build/dev/php7/Dockerfile

FROM php:7.0-fpm

### Install some requirements
RUN apt-get update && apt-get install -y git zlib1g-dev libmcrypt-dev supervisor openssh-server \
    && mkdir -p /var/log/supervisor \
    && mkdir -p /var/run/sshd \
    && rm -rf /var/lib/apt/lists/*

RUN docker-php-ext-install bcmath mbstring opcache pcntl zip mcrypt pdo_mysql \
    ## APCu
    && pecl install apcu \
    && docker-php-ext-enable apcu \
    ## Xdebug
    && pecl install xdebug-beta \
    && docker-php-ext-enable xdebug \
    && echo "xdebug.cli_color=1\nxdebug.remote_autostart=1\nxdebug.remote_connect_back=1" > /usr/local/etc/php/conf.d/xdebug.ini

#Add composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

COPY supervisor/supervisor.conf /etc/supervisor/conf.d/supervisord.conf

COPY php.ini /tmp/php.ini_extension
RUN cat /tmp/php.ini_extension >> /usr/local/etc/php/php.ini \
    && rm /tmp/php.ini_extension

COPY php-fpm.conf /usr/local/etc/php-fpm.conf

RUN useradd -ms /bin/bash jarco
RUN usermod -u 1000 jarco

# SSH configuration to connect on development environment with the interpreter
RUN echo 'root:jarcodev' | chpasswd
RUN sed -i 's/PermitRootLogin without-password/PermitRootLogin yes/' /etc/ssh/sshd_config \
    && sed '[email protected]\s*required\s*[email protected] optional [email protected]' -i /etc/pam.d/sshd
RUN echo "export VISIBLE=now" >> /etc/profile

CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

WORKDIR /app

Go for Nginx

Add symfony.dev to your hosts file.

/etc/build/nginx/symfony.conf

server {
    server_name symfony.dev www.symfony.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;

        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://symfony.dev/app.php/some-path
        # Remove the internal directive to allow URIs like this
        internal;
    }
}

/etc/build/nginx/Dockerfile

FROM nginx:latest

# Default nginx config
COPY symfony.conf /etc/nginx/conf.d/app.conf

The code

Docker uses volumes to share the files. Instead of copy inside the container as you can do when build the image to deploy, in dev you'll create a volume and share it to the containers that need it

/etc/build/dev/code/Dockerfile

FROM busybox:latest

VOLUME /app

Logs

I use to use Kibana to read the logs and debug. I just follow willdurand/elk and you can find the configuration for symfony here: https://github.com/jorge07/symfony3-docker-starter/tree/master/etc/build/dev/elk/logstash

The orchestration

Important! You will need Docker Compose > 1.6

docker-compose.yml

version: '2'
services:
  code:
    build: etc/build/dev/code
    container_name: symfony_code
    volumes:
      - ./:/app

  nginx:
    build: etc/build/dev/nginx
    container_name: symfony_nginx
    ports:
      - "80:80"
    links:
      - php
    volumes_from:
      - code
    volumes:
      - ./var/logs/nginx:/var/log/nginx

  php:
    build: etc/build/dev/php7
    container_name: symfony_php
    volumes:
      - ~/.ssh:/root/ssh
    volumes_from:
      - code
    ports:
      - "9000:9000"
      - "2222:22"
    links:
      - code
      - redis

  redis:
    image: redis:3.0.7-alpine
    ports:
      - 6379:6379
    container_name: symfony_redis

  elk:
    image: willdurand/elk
    container_name: symfony_elk
    ports:
      - 81:80
    volumes:
      - ./etc/build/dev/elk/logstash:/etc/logstash
      - ./etc/build/dev/elk/logstash/patterns:/opt/logstash/patterns
    volumes_from:
      - code

Ok, after all that work:

docker-compose up -d

Install dependencies:

docker exec -it symfony_php composer install

Open the proyect

And voila!

Now you know more or less all the components that run and how to run it together.

To make it easier I've this proyect on GitHub with the instructions to run it inside.

Pro tip

I suppose some of you can have private repositories, to install it with composer add to PHP Dockerfile:

# Make ssh dir
RUN mkdir /root/.ssh/

# Copy over private key, and set permissions
ADD id_rsa /root/.ssh/id_rsa

That's the resume of my experience, if you have any problem leave me a comment and I'll try to help you.