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:
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
:
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.
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 thedocker-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 runcompose up
again.
Have you noticed that you get request log messages while accessing the application?
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
Have you noticed that
ps fax
was issued inside thephp
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: