Docker Development Environment for PHP Apps

Docker Development Environment for PHP Apps

In this article, I will do a pragmatic approach about how to run a generic PHP application on three Docker containers: nginx, php-fpm, and mysql.

Motivation

I want to run my PHP application on a standardized environment, on any computer, with the minimum possible effort to set this environment, and without messing up to my Operational System.

You must have thought of virtual machines and environments in Vagrant, like the Homestead project. Don't get me wrong, those VM environments are viable solutions, but when compared to containers, VM seems to be a big waste of resources.

Requirements

  • We need the Docker Engine installed to run containers.
  • If you have no knowledge about Docker or Containers at all, I suggest the Docker Get Started Guide. Read that guide before or after this article, but read that.

The Application

As a proof of concept, we are going to run a simple php script that queries for a few records from a MySQL database. This application is very simple so we can focus on the Docker workflow.

Cloning the application from a git repository

As you are a new developer, getting through an onboarding process, or just want to get your hands on a new popular project, the first step will be cloning this application to your machine:

git clone https://github.com/rodrigoSyscop/dockerarticle

Let's get into each file that was created by the above command:

dockerarticle
├── app
│   └── index.php
├── docker-compose.yml
├── mysql
│   └── initial_data
│       └── blog_2017-11-18.sql
├── nginx
│   └── nginx.conf
└── php
    └── Dockerfile
  • The docker-compose.yml file is the main file that describes the containerized structure of our application. Which services we have, which networks, volumes, and so on.
  • The app/ directory is where the php code of our application lives.
  • We also have a folder for each service container, which we will run in a minute: nginx, php, and `mysql.

Note that the application code is kept in the app/ folder, all the remaining folder and files are related to the infrastructure, but both are versioned by git. See Infrastructure as Code.

Having the infrastructure code next to application code is considered a good practice introduced by the DevOps culture. This way both teams, dev team, and operations team, will get to know when either, the infra or the app, gets updated.

Docker Compose

The compose is a Docker tool that allows us to define an application as a composition of multiple services, each one executed in its own container.

The default application name is the name of the directory that docker-compose.yml is stored, which is docker article in our case.

Lets dive into the content of the docker-compose.yml file:

version: "3" 
services:
  # Web service layer
  nginx:
    image: nginx:1.13
    volumes:
      - "./app:/var/www/html"
      - "./nginx/nginx.conf:/etc/nginx/nginx.conf"
    ports:
      - "80:80"
    depends_on:
      - php

  # Application service layer
  php:
    build:
      context: ./php
    volumes:
      - "./app:/var/www/html"
    ports:
      - "9000:9000"
    depends_on:
      - mysql
    environment:
      - MYSQL_USER=root
      - MYSQL_PASS=123.456

  # Data persistence service layer
  mysql:
    image: mysql:5.7.20
    volumes:
      - "db_data:/var/lib/mysql"
      - "./mysql/initial_data:/docker-entrypoint-initdb.d"
    ports:
      - "3306:3306"
    environment:
      - MYSQL_ROOT_PASSWORD=123.456

volumes:
db_data:

When running docker-compose up, in the same folder that docker-compose.yml is, we get a lot of messages related to the build of the images used by the services declared in the docker-compose.yml file. Next time you run this same command, you will see only a few messages like these:

5Qny9i1ex.png

It's because the images were already built and they don't have to be rebuilt if there were no changes in our Dockerfiles.

During the first run of the mysql container, the initial database structure will be created at /var/lib/mysql:

F7Uh_7Eoh.png

Having the /var/lib/mysql folder into our container image is not recommended, even for development purposes. That happens due to the way that the auks works, a CoW - Copy on Write - schema. For this reason, it's common to use a named volume to put those data in. In our case, we are creating the db_data named volume, defined at the very end of the docker-compose.yml file and used by the db container.

If everything is going well so far, you should see this page below when accessing the http://localhost addresses on your browser.

Screen Shot 2019-04-04 at 13.56.16.png

Note: a common issue here is the nginx container won't be able to start due to the port 80 is been used by another process in your OS. In this case, just change the port mapping from "80:80" to something like "8080:80" in the docker-compose.yml file. Finally, try to access http://localhost:8080. The same problem can occur for the other containers as well, just change the port mapping and then run compose up again.

Have you noticed that you get request log messages while accessing the application?

ytW7lYBhp.png

All your containers' logs are displayed here, and each one will get a colorful prefix accordingly to its name.

Diving into the docker-compose.yml file

We start specifying the reference version for the compose file, this way Docker is able to know what version of Docker Engine is necessary to understand the instructions we've used in the docker-compose.yml file.

Then we defined our three application's services:

nginx

For nginx service, we are using the official nginx image from Docker Hub, at its 1.13 version. If this image is not yet downloaded, Docker will get it automatically.
We are also using two volumes, one for the application source code be mounted at /var/www/html, and other for nginx.conf, so we can update our nginx settings and just restart the container, without doing a full rebuild of it. Last, we specify the 80 port of our host to be mapped to the 80 port of our container, and de dependency that the nginx container has on php container.

mysql

The mysql container is based on the official mysql image from Docker Hub. The news here is a named volume called db_data, which is mapped to /var/lib/mysql, besides the blog_2017-11-18.sql file, which is in the initial_data folder of the project and is mapped to the docker-entrypoint-initdb.d/ of the container. This image has instructions that verify for content in this folder, if any .sql file is present they will be imported when the container run for the first time.

blog_2017-11-18.sql:

CREATE DATABASE `blog`;
USE `blog`;

# Dump of table posts
# ------------------------------------------------------------

DROP TABLE IF EXISTS `posts`;

CREATE TABLE `posts` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `title` varchar(200) NOT NULL DEFAULT '',
  `body` text NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `posts` WRITE;
/*!40000 ALTER TABLE `posts` DISABLE KEYS */;

INSERT INTO `posts` (`id`, `title`, `body`)
VALUES
    (1,'First Post','This is the content of the first post'),
    (2,'Second Post','This is the content of the second post'),
    (3,'Third Post','This is the content of the third post');

/*!40000 ALTER TABLE `posts` ENABLE KEYS */;
UNLOCK TABLES;

Useful commands

You have to be in the dockerarticle folder to run the commands below:

# list all running containers for dockerarticle app
docker-compose ps

# Access  bash inside the php container
docker container exec -it dockerarticle_php_1 bash

# Stop all containers for the app
docker-compose stop

# Stop and remove the containers
# it will keep the volumes unless you use the  "-v" flag
docker-compose down

qwZCna4eZ.png

Have you noticed that ps fax was issued inside the php container? All processes displayed are isolated by Docker Engine.

Last thoughts

Docker is an awesome tool for standardization of development environments, but we still have a few improvements before using this in a production environment:

  • Customization of images.
  • Storing logs outside the container's filesystem.
  • Environment variables using .env files.
  • Swarm cluster for basic container orchestration.

I want to write about these topics very soon, so follow me here and also on social media.


References: