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.
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.
- 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.
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
docker-compose.ymlfile is the main file that describes the containerized structure of our application. Which services we have, which networks, volumes, and so on.
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:
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.
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
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:
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 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
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
80is been used by another process in your OS. In this case, just change the port mapping from
"80:80"to something like
docker-compose.ymlfile. 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
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
Then we defined our three application's services:
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
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.
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;
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 faxwas issued inside the
phpcontainer? All processes displayed are isolated by Docker Engine.
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
- Swarm cluster for basic container orchestration.
I want to write about these topics very soon, so follow me here and also on social media.
Maravilha de artigo, muito bem explicado, simples, era o que eu procurava.
I appreciate the content and discussions held on this site. All i found is the intellectually high and important information on https://www.myassignmentwriting.com.au/research-paper topic. As an easy writer it helps me a lot to think about new aspects and dimensions of a topic. I am very hopeful for you people and with you more power for future.