Dockerizing a php application

Dockerizing a PHP Application

Learn how to leverage Docker’s advantages to easily develop and deploy a PHP application to Heroku, using Semaphore for continuous deployment.

Brought to you by

Semaphore

Introduction

In this tutorial, you will learn what Docker is and how you can use it to create sophisticated working environments. If you already have experience using VMs such as VirtualBox, Vagrant, etc., you'll grasp the concept quickly.

To make things more concrete, we will use a demo application which interacts with the 500px API to list popular photos, view, upvote and comment on them. The application is built using Laravel 4, but this shouldn't present an issue in our case. Let's get started.

Docker Logo

What is Docker?

Most developers use the (W|L|M)AMP stack as a starting point, but this environment can become overwhelming very quickly. Once you start feeling this pain, you'll start using VirtualBox to keep your host computer clean, and the projects separated. To make VirtualBox machines portable and easy to share and reproduce, Vagrant comes into play. Vagrant makes the virtual machines that we can share with our project members distributable, which helps developers reproduce the same configurable environment as the other developers in their team.

Docker is a cutting-edge solution to this problem. It provides us with containers that have all the virtualization capabilities we need, while also being more lightweight than the traditional virtual machines.

Prerequisites

Docker can be installed on any platform. You can install it from a binary executable, or by using the official installer.

Docker runs natively on Linux platforms. OSX and Windows users need to access Docker through a VM. The below pictures from the documentation illustrate the difference.

Docker on Linux Docker on OSX

Installing Docker

Follow one of the installation guides below for your operating system:

After installing Docker on our host, we need to run the docker-machine ls to see the list of available VMs. A default VM is created by default.

$ docker-machine ls
NAME        ACTIVE   DRIVER       STATE     URL                         SWARM
default     *        virtualbox   Running   tcp://192.168.99.100:2376
dev                  virtualbox   Stopped

We can manage our VMs using the start, stop, restart and status options. (docker-mahine start|stop|restart|status <VM name>)

$ docker-machine start default
Starting VM...
Started machines may have new IP addresses. You may need to re-run the `docker-machine env` command.

The command output is telling us to set the necessary environment variable so that the docker command knows which VM is currently active and what's the reserved IP address for it. This means that we can have multiple VMs running at the same time and switch between them by setting the appropriate environment variables.

$ docker-machine env default
export DOCKER_TLS_VERIFY="1"
export DOCKER_HOST="tcp://192.168.99.100:2376"
export DOCKER_CERT_PATH="/Users/admin/.docker/machine/machines/default"
export DOCKER_MACHINE_NAME="default"
# Run this command to configure your shell:
# eval "$(docker-machine env default)"
$ eval "$(docker-machine env default)"

Docker Images

Docker is based on the concept of building images which contain the necessary software and configuration for applications. We can also build distributable images that contain pre-configured software like an Apache server, Caching server, MySQL server, etc. We can share our final image on the Docker HUB to make it accessible to everyone.

Working with Docker Images

We can list the available images on our machine by running the docker images command.

$ docker images
REPOSITORY                 TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
ubuntu                     14.04               91e54dfb1179        5 months ago        188.4 MB
nimmis/apache-php5         latest              bdd370e4f83b        6 months ago        484.4 MB
eboraas/apache-php         latest              0501b3fdd0c2        6 months ago        367 MB
mysql                      latest              a128139aadf2        6 months ago        283.8 MB
ubuntu                     latest              d2a0ecffe6fa        7 months ago        188.4 MB
eboraas/laravel            latest              407e2d00b528        12 months ago       404.5 MB

To browse the available images, we can visit the Docker HUB and run docker pull <image> to download them to the host machine.

Docker Containers

An image can be considered a class definition. We define its properties and behavior. Containers are instances created from this class. We can create multiple instances of the same image. The docker ps command prints the list of containers running on the machine. We don't have any containers at the moment, so let's create a new one:

$ docker run -d nimmis/apache-php5
0fc9b23e436d285d474477b8ea095b31530c93c3dd354d515534be5b1c30ecce

We created a new container from the nimmis/apache-php5 image, and we used the -d flag to run the job in the background. The output hash is our container id, we can use it to access the container and play around with it. Let's print our containers first:

$ docker ps
CONTAINER ID        IMAGE                COMMAND             CREATED             STATUS              PORTS               NAMES
0fc9b23e436d        nimmis/apache-php5   "/my_init"          2 minutes ago       Up 2 minutes        80/tcp              serene_yalow

We can see from the output that the container has an ID and a name. It's really helpful and time saving to name the container. Let's re-create another container and name it:

$ docker run -d --name="apache_server" nimmis/apache-php5
bc75f6df4d825e836734fa163f2b2ee33f5eaf0e1544304ad755cf8eaf060f04

$ docker ps
CONTAINER ID        IMAGE                COMMAND             CREATED             STATUS              PORTS               NAMES
bc75f6df4d82        nimmis/apache-php5   "/my_init"          5 seconds ago       Up 4 seconds        80/tcp              apache_server

Container instances are created almost instantly, you won't notice any delay.

We can now access our container by executing the bash command and attaching it to our terminal:

$ docker exec -it apache_server bash
root@bc75f6df4d82:~# /etc/init.d/apache2 status
* apache2 is running
root@bc75f6df4d82:~#

To avoid polluting our VM with unused containers, make sure to delete unused ones:

# Delete container using ID or name
docker rm -f <container>

# Delete all available containers
docker rm -f $(docker ps -aq)

Since our container is an Apache server, it makes sense to have a way to access it through a browser. When creating an image, we need to make sure to expose it through a specific port so that the other containers, browsers, etc. can access it. We will cover this in more detail in the Dockerfiles section.

# Expose default ports
docker run -tid -P  --name apache_server nimmis/apache-php5

# Specify a different post <host port>:<container port>
docker run -tid -p 80:80 --name apache_server  nimmis/apache-php5

We can get our VM machine's IP by running docker-machine ip default, or by printing the environment variables.

$ docker-machine ip default
192.168.99.100

$ docker-machine env default
export DOCKER_TLS_VERIFY="1"
export DOCKER_HOST="tcp://192.168.99.100:2376"
export DOCKER_CERT_PATH="/Users/admin/.docker/machine/machines/default"
export DOCKER_MACHINE_NAME="default"
# Run this command to configure your shell:
# eval "$(docker-machine env default)"

The last part is to map the Apache server to run our application instead of the default Apache homepage. This means that we need to keep our application folder synced with the server root folder (/var/www). We can do that using the -v option. You can read more about container volumes in the Docker documentation.

docker run -tid -p 80:80 --name="apache_server" -v /Users/admin/Desktop/www/500pxAPI_Test:/var/www nimmis/apache-php5

It's always a good idea to take a look at the image description on the Docker HUB and read the instructions about the proper to create containers from the image.

Working with Dockerfiles

We mentioned earlier that everyone can make a Docker image and share it on the Docker HUB, and that Dockerfiles are the main tool to achieve this. We're going to see how we can configure our own image and make it fit our needs. You can check the documentation for the list of available commands.

We've already explained that images are like class definitions. We can expand an existing image and add more functionality to it.

# Dockerfile
FROM nimmis/apache-php5

The MAINTAINER instruction should contain the developer or company name.

# Dockerfile
FROM nimmis/apache-php5

MAINTAINER SemaphoreCI <dev@example.com>

The nimmis/apache-php5 image set the Apache public folder to /var/www/html. However, in this case we need to set it to the /var/www/public folder. One way to achieve this is by copying our virtual host configuration to the machine. Here is an example:

# 000-default.conf

<VirtualHost *:80>
  ServerAdmin webmaster@localhost
  DocumentRoot /var/www/public

  ErrorLog ${APACHE_LOG_DIR}/error.log
  CustomLog ${APACHE_LOG_DIR}/access.log combined

  <Directory /var/www>
    Options Indexes FollowSymLinks
    AllowOverride All
    Require all granted
  </Directory>
</VirtualHost>

Now, we need to override the /etc/apache2/sites-available/000-default.conf with our new file. We can do it using the COPY command:

# Dockerfile
FROM nimmis/apache-php5

MAINTAINER SemaphoreCI <dev@semaphoreci.com>

COPY 000-default.conf /etc/apache2/sites-available/000-default.conf

We mentioned earlier that the developer can expose the container to a specific port, this is done through the EXPOSE command. We'll expose the HTTP 80 port and the 443 SSL connection:

# Dockerfile
FROM nimmis/apache-php5

MAINTAINER SemaphoreCI <dev@semaphoreci.com>

COPY 000-default.conf /etc/apache2/sites-available/000-default.conf

EXPOSE 80
EXPOSE 443

The last thing we need to do is to run the Apache server in the background. The CMD command should be used only one time in a Dockerfile, and it needs to have the following form:

CMD ["executable","param1","param2"]

Our Dockerfile is now complete and ready to be built:

# Dockerfile
FROM nimmis/apache-php5

MAINTAINER SemaphoreCI <dev@semaphoreci.com>

COPY 000-default.conf /etc/apache2/sites-available/000-default.conf

EXPOSE 80
EXPOSE 443

CMD ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"]

Useful Commands

Although our image is ready, we'll go through some commands that could be useful for many projects.

What if we wanted to install Node.js to manage our front-end assets, run build commands, etc.?

RUN apt-get update && \
    apt-get install nodejs && \
    apt-get install npm

This will install Node.js and the npm manager in our image. We can use the RUN command many times inside the same Dockerfile, because Docker keeps a history for our image creation. Every RUN command is stored as a commit in the versioning history.

Another useful command is ENV. It lets us set an environment variable through the build process, and will also be present when a container is created. Be sure to check the full list of supported commands in the documentation.

ENV MYSQL_ROOT_PASSWORD=root
ENV MYSQL_ROOT_USER=root

Building the Image

If you've previously pulled the base image, it will be loaded from your computer instead of being downloaded again. This means that the build process won't take much time.

Our folder contains a Dockerfile and a 000-default.conf file. The docker build . command will build the Dockerfile inside the current directory:

$ docker build .
Sending build context to Docker daemon 3.072 kB
Step 1 : FROM nimmis/apache-php5
 ---> a344ca61f528
Step 2 : MAINTAINER SemaphoreCI <dev@semaphoreci.com>
 ---> Running in 0b089e22551d
 ---> b2a632744b32
Removing intermediate container 0b089e22551d
Step 3 : COPY 000-default.conf /etc/apache2/sites-available/000-default.conf
 ---> 891bd209e366
Removing intermediate container 686dc329a7ec
Step 4 : EXPOSE 80
 ---> Running in 34522030f0b3
 ---> c6cb1b978009
Removing intermediate container 34522030f0b3
Step 5 : EXPOSE 443
 ---> Running in b7b5f5ce9fbb
 ---> 9fc2bf1c6b10
Removing intermediate container b7b5f5ce9fbb
Step 6 : CMD /usr/sbin/apache2ctl -D FOREGROUND
 ---> Running in 1b65a90347f5
 ---> 2096d6b48d77
Removing intermediate container 1b65a90347f5
Successfully built 2096d6b48d77

If we list our Docker images now, we'll see our new built image:

$ docker images
REPOSITORY                 TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
<none>                     <none>              sha256:2096d        3 seconds ago       484.3 MB
nimmis/apache              latest              sha256:1f64f        2 weeks ago         348.5 MB
ubuntu                     14.04               sha256:0109b        6 months ago        188.3 MB
nimmis/apache-php5         latest              sha256:a344c        6 months ago        484.3 MB
eboraas/apache-php         latest              sha256:b39df        6 months ago        367 MB
mysql                      latest              sha256:0460d        7 months ago        283.8 MB
ubuntu                     latest              sha256:2a274        7 months ago        188.3 MB
eboraas/laravel            latest              sha256:34e3f        12 months ago       404.5 MB

Currently, our image has no name, and it isn't tagged. The -t option let us specify the image repository and tag.

We need to remove the previously built image to avoid polluting our host machine (docker rmi -f sha256:2096d).

$ docker build -t younesrafie/apache-php:v0.1 .
Sending build context to Docker daemon 3.072 kB
Step 1 : FROM nimmis/apache-php5
 ---> a344ca61f528
Step 2 : MAINTAINER SemaphoreCI <dev@semaphoreci.com>
 ---> Running in cd0d095c656f
 ---> ceb5a19276a6
Removing intermediate container cd0d095c656f
Step 3 : COPY 000-default.conf /etc/apache2/sites-available/000-default.conf
 ---> ce11e81a324f
Removing intermediate container bab1e20ef2de
Step 4 : EXPOSE 80
 ---> Running in 100d226a7218
 ---> 7901c815467a
Removing intermediate container 100d226a7218
Step 5 : EXPOSE 443
 ---> Running in d25f1d010721
 ---> de61dc08c719
Removing intermediate container d25f1d010721
Step 6 : CMD /usr/sbin/apache2ctl -D FOREGROUND
 ---> Running in 4d916fc35c0b
 ---> b447c85a80ae
Removing intermediate container 4d916fc35c0b
Successfully built b447c85a80ae
$ docker images
REPOSITORY                 TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
younesrafie/apache-php     v0.1                sha256:b447c        24 seconds ago      484.3 MB

Our image is now labeled and tagged. The final step is to push it to the Docker HUB. This step is optional, but it's still useful if we're planning on sharing the image and and helping others with their development environment.

After logging into our Docker HUB account, we need to click on the Create Repository link on the dashboard.

Create Repository

Next, we need to run docker login in the terminal and type in our login credentials. After a successful login, we can push our image to the Docker HUB:

$ docker push younesrafie/apache-php:v0.1

Docker Compose

Using terminals and remembering commands is not very practical for creating application containers and getting started quickly. Docker Compose uses YAML files to configure and run containers. This means that we can ship our application Dockerfile to build the environment and use a docker-compose.yml to run the containers.

The first step is to install Docker Composer on our machine. Follow the instructions in the Docker documentation before proceeding with the following steps.

The docker-compose.yml file allows us to configure multiple services inside the same file, and specify after that if the image needs to be built or if we can use a predefined image.

# docker-compose.yml
server:
  build: .

This will build our image using the Dockerfile inside the same folder. If you already have your image built locally or on the Docker HUB, you can use the image property.

# docker-compose.yml
server:
  image: younesrafie/apache-php:v0.1

In the the container section, we specified the exposed ports, mounting volumes, and optionally some environment variables.

# docker-compose.yml
server:
  image: younesrafie/apache-php:v0.1
  ports:
    - "80:80"
    - "443:443"
  volumes:
    - .:/var/www
  environment:
    - API_TOKEN=xxxx

Now, we can run docker-composer up to create our container. The command will attach the container output to ours, and we'll need to press ctrl+c to quit. We can avoid this by using the -d option (docker-composer up -d). If we have multiple services, we can specify which one (docker-composer up server).

$ docker ps -a
CONTAINER ID        IMAGE                         COMMAND                  CREATED             STATUS              PORTS                                      NAMES
d342655c0093        younesrafie/apache-php:v0.1   "/usr/sbin/apache2ctl"   4 seconds ago       Up 3 seconds        0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp   semaphoredocker_server_1

Use the docker-compose stop|rm to manage your container.

Using Docker with Heroku

Heroku provides a plugin called heroku-docker for building our development environment and deploying it easily. After installing the Heroku toolbelt on our machine, we need to install the Docker plugin. Check the documentation for the instructions.

heroku plugins:install heroku-docker

The next step is to create an app.json file which describes our application:

// app.json
{
  "name": "500px Semaphore demo",
  "description": "Just a demo app",
  "image": "nimmis/apache-php5"
}

We can now generate our Dockerfile and docker-compose.yml files using the heroku docker:init command. Then, we'll update the files with our own configuration.

# Dockerfile
FROM nimmis/apache-php5

MAINTAINER SemaphoreCI <dev@semaphoreci.com>

COPY 000-default.conf /etc/apache2/sites-available/000-default.conf

EXPOSE 80
EXPOSE 443

CMD ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"]
# docker-compose.yml

web:
  build: .
  command: 'bash -c ''vendor/bin/heroku-php-apache2 /app/public/'''
  working_dir: /app
  volumes:
    - '.:/app'
  environment:
    PORT: 8080
  ports:
    - '8080:8080'
shell:
  build: .
  command: bash
  working_dir: /app
  environment:
    PORT: 8080
  ports:
    - '8080:8080'
  volumes:
    - '.:/app'
# dev container
dev:
  build: .
  ports:
    - "80:80"
  volumes:
    - .:/var/www

The Heroku Docker plugin requires us to define a web service to run our application. We've also added the dev service, which will be used when working locally:

# on the development machine
docker-compose up dev -d

Now, we are ready to deploy our application to Heroku:

# Create a new application server called `semaphore-docker-demo`
heroku create semaphore-docker-demo --buildpack heroku/php

# Run the build process
heroku docker:release --app semaphore-docker-demo

# Add the Heroku remote to Git remotes
heroku git:remote -a semaphore-docker-demo

# Push the application to our new server
git push heroku master

Heroku Demo Application

Continuous Deployment Using Semaphore

Semaphore has made it easy to continuously deploy applications when ready. After creating an account, you'll be asked to create a new project.

Home Screen

Create Project

We can use our 500px Demo application for a test. After choosing a repository, we need to choose a branch:

Select Branch

Select Account

The next step is to configure our build environment. We can choose the proper PHP version, test commands, multiple parallel tests, etc.:

Select Test Environment

Click on Build With These Settings to run the tests:

Test Done

Now, return to the project dashboard page and click on the set Up Deployment button:

Project Dashboard

Check the Semaphore documentation for a detailed explanation of how to use Docker with Semaphore.

Select Provider

Deployment Strategy

There are two deployment methods available on Semaphore: automatic and manual deployment.

Automatic deployment means that a deploy will be triggered after every passed build on the selected branch. In addition, you can also manually deploy any build from any branch at any time.

For automatic deployment, you will be asked to select which branch will be automatically deployed after each passed build.

Manual deployment requires the manual selection of the builds to deploy.

We will choose Automatic deployment.

Note: You can change the deployment strategy at any time in server settings once the setup is complete.

Deployment Branch

Heroku API Key

You can find your Heroku API key under account information on the Heroku dashboard.

By providing the Heroku API key, we authorize Semaphore to configure and set SSH keys that are needed for deployment.

Select Heroku Application

Deployment Server Name

First Deploy

First Deploy Done

Demo Application Screenshot

Semaphore will now listen for push events on our repository and, depending on the deployment strategy, automatically trigger the deployment process to the production server. We'll be notified when the process is done.

Deployment Notification

Conclusion

In this tutorial, we learned the basics of using Docker and how to create our own Docker image. We deployed a demo application to Heroku, and we used Semaphore for continuous deployment to the production server.

Take a look at the final demo on Github. If you have any questions or comments, make sure to post them below, and we'll do our best to answer them.

P.S. Want to continuously deliver your applications made with Docker? Check out Semaphore’s Docker support.

862d65cadc47ead67218bcd6a148f03d
RAFIE Younes

Younes is a freelance web developer, technical writer and a blogger from Morocco. He's worked with JAVA, J2EE, JavaScript, etc., but his language of choice is PHP. He's also a regular author on SitePoint.

on this tutorial so far.
User deleted author {{comment.createdAt}}

Edited on {{comment.updatedAt}}

Cancel

Sign In You must be logged in to comment.