So you have your php application running smoothly on your local machine. That’s really good! But preparing it for deployment can be tricky. Here’s some things I learned by doing it:

What we’ll do is to create a docker image with our application, so we can just docker run it.

If you are not familiarised with Docker, I recommend learning some before continuing since nowadays it is a must-know for developers.

Setting everything up

The application will run in only one container with PHP-FPM, NGINX and Supervisor.

I’ve seen lots of tutorials and articles about running PHP-FPM and NGINX in different containers. This is not good for deployment because both PHP-FPM and NGINX need to access the code and the only way to achieve this is to share the source code through a volume (named or local), which is not very good: we want our code to be immutable, encapsulated within the docker image.

Let’s create some files for our build process:

  • Project root
    • build/
      • composer.sh
      • site.conf
      • supervisor.conf
    • Dockerfile
    • docker-compose.yml
    • .dockerignore

The Dockerfile will be in our root-level dir because we want to copy our source code to the image, and we cannot copy anything outside the build context.

# Dockerfile
FROM phusion/baseimage
ENV DEBIAN_FRONTEND noninteractive

# Install PHP
RUN add-apt-repository -y ppa:ondrej/php && apt-get update
RUN apt-get install -y \
# Only install the php extensions you need.
php7.1-fpm \
php7.1-mcrypt \
php7.1-intl \
php7.1-mbstring \
php7.1-xml \
php7.1-dom \
php7.1-zip \
php7.1-curl

# Create socket directory
RUN mkdir -p /var/run/php

# Don't clear env variables
# This is very important since it will allow us to read environment variables from the container.
RUN sed -e 's/;clear_env = no/clear_env = no/' -i /etc/php/7.1/fpm/pool.d/www.conf

# Install nginx
RUN apt-get install -y nginx

COPY ./build/site.conf /etc/nginx/conf.d/default.conf

# Install supervisor
RUN apt-get install -y supervisor

ADD ./build/supervisor.conf /etc/supervisor/conf.d/my-app.conf

# Copy application files
COPY . /var/www/html

# Install and run composer
RUN apt-get install -y wget git zip
RUN cd /var/www/html && chmod +x ./build/composer.sh && ./build/composer.sh
RUN cd /var/www/html && php composer.phar install --no-scripts && rm composer.phar

# Allow php to write cache and logs. Note that the cache and data are outside the application directory because we don't want this data to be immutable.
# You can add the directories you want to store data to persist through deployments.
RUN mkdir -p /data/cache
RUN mkdir -p /data/logs
# Remember to allow php-fpm to write on those.
RUN chown -R www-data:www-data /data/cache
RUN chown -R www-data:www-data /data/logs
RUN chown -R www-data:www-data /var/www/html

WORKDIR /var/www/html

# Run supervisor
CMD ["supervisord", "-n"]

This will be the configuration for our NGINX

# build/site.conf
server {
    listen 7102;
    server_name ~.;
    root /var/www/html/public;

    location / {
        try_files $uri /index.php$is_args$args;
    }

    location ~ ^/index\.php(/|$) {
        fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
        internal;
    }

    location ~ \.php$ {
        return 404;
    }
}

The supervisor configuration file

;build/supervisor.conf
[supervisord]
logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log)
pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
childlogdir=/var/log/supervisor            ; ('AUTO' child log dir, default $TEMP)
nodaemon=true

[program:nginx]
command=/usr/sbin/nginx -g 'daemon off;'
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/nginx.err.log
stdout_logfile=/var/log/supervisor/nginx.out.log

[program:php-fpm]
command=/usr/sbin/php-fpm7.1 -F
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/php-fpm.err.log
stdout_logfile=/var/log/supervisor/php-fpm.out.log

This composer installation file is taken from Composer’s documentation.

#!/bin/sh

# build/composer.sh
EXPECTED_SIGNATURE="$(wget -q -O - https://composer.github.io/installer.sig)"
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
ACTUAL_SIGNATURE="$(php -r "echo hash_file('SHA384', 'composer-setup.php');")"

if [ "$EXPECTED_SIGNATURE" != "$ACTUAL_SIGNATURE" ]
then
    >&2 echo 'ERROR: Invalid installer signature'
    rm composer-setup.php
    exit 1
fi

php composer-setup.php --quiet
RESULT=$?
rm composer-setup.php
exit $RESULT

Remember to add volumes for other data you want to keep.

# docker-compose.yml
version: '3'
services:
  web:
    container_name: my_php_app
    build: .
    ports:
      - "80:80"
    volumes:
      - cache:/data/cache
      - logs:/data/logs
volumes:
  cache:
  logs:

Here we will set some file we don’t want to COPY to our image:

#.dockerignore
# Example for a Symfony 4 app
var/*
.env
# We will re-download the third-party libraries in the build, so we won't copy this folder
vendor/*

The environment

It is important that your application knows the environment it’s running on by reading an environment variable. We’ll call it APP_ENV. You should be able to read it in your app with

getenv('APP_ENV');

This variable should be available inside the docker container. The idea is to build only one image that you can deploy in different environments.

docker build -t my-image .

That should get you the final image. To run it, you can

docker run -v "cache:/data/cache" -v "logs:/data/logs" -e "APP_ENV=dev" -p 80:80 my-image:latest

Here, we say: “Ok docker, run the image i just built, create the volumes to persist data outside the image, expose the port 80 and set environment to dev”.

You can read more about volumes here.

Here’s a nice article that helped me a lot: https://vsupalov.com/docker-arg-env-variable-guide/.

The idea is to keep it fairly simple: don’t get tangled with ENV and ARG keywords. If you get to that, take a walk and start from scratch: it will come around :)

Edit (2018-05-10)

Added this article on how to install New Relic within this very Dockerfile.