Article summary
Getting started with Docker can be a bit overwhelming. There are images, containers, Dockerfiles, docker-compose, etc. In this post, I’ll give a quick introduction to using docker-compose with PHP and MySQL.
Motivation
Docker helps development by bundling an application’s code with the environment where it will run. This means that if you give the codebase to another developer on your team, they won’t need to worry about installing new software or upgrading/downgrading software you’ve already installed (such as Apache or MySQL), and they won’t have to worry about changing versions.
The Files
In this post, I’m going to talk about three files:
- A Dockerfile called
Dockerfile
in the root of the project - A YAML file called
docker-compose.yaml
- A PHP file called
index.php
in thesrc/
directory
1. The Dockerfile
Here’s the whole Dockerfile we’ll be discussing:
FROM php:7.2-apache
WORKDIR /var/www
RUN apt-get update \
&& apt-get install -y wget \
&& rm -rf /var/lib/apt/lists/*
RUN apt-get update \
&& apt-get install -y git
RUN docker-php-ext-install pdo pdo_mysql mysqli
RUN a2enmod rewrite
# php unit
RUN wget -O /usr/local/bin/phpunit https://phar.phpunit.de/phpunit-8.phar
RUN chmod +x /usr/local/bin/phpunit
COPY ./src /var/www/html
The name Dockerfile
is a convention. Unless otherwise specified, Docker will look for a file with this name. The Dockerfile is used to create an “image,” which you can think of as a snapshot of the state of really stripped-down Linux OS. It will typically be built on top of a pre-existing Docker image, and it will consist of instructions to install more needed software.
In this case, we start with a base of php:7.2-apache. This is a community-supported image that bundles PHP and Apache. When we build our image, Docker will pull this from Docker Hub automatically.
Next, we run a series of commands to install anything else we’ll need for our server, including extensions for MySQL and PHPUnit.
Finally, we copy over the contents of our src/
directory to /var/www/html
. This is where Apache will look to serve files, as documented by the Docker Hub page on the image.
The types of tasks done here are pretty standard for Docker files. They’re mostly about installing software and getting code from your local environment to the container.
At this point, you could run Docker commands to build and run the image (here’s another post if you want to see them), but I’m going to skip those steps and go straight to using Docker Compose, because I find it more convenient for local development.
2. docker-compose.yaml
Docker Compose allows for building and running multiple containers using the docker-compose up
and docker-compose down
commands.
Here’s the Docker Compose file:
version: '3'
services:
mysql_database:
image: mysql:5.7
ports:
- '3001:3306'
volumes:
- ./mysql_data:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=root_pa55w0rd
- MYSQL_DATABASE=my_database
- MYSQL_USER=user
- MYSQL_PASSWORD=pa55w0rd
my_php:
depends_on:
- mysql
build: .
ports:
- '3000:80'
volumes:
- ./src:/var/www/html
- ./tests:/var/www/tests
environment:
- MYSQL_DATABASE=my_database
- MYSQL_USER=user
- MYSQL_PASSWORD=pa55w0rd
You can see that we specify two services, one called mysql_database, and one called my_php. You could call them anything you want. We’ll come back to them in a bit.
Specifying images
You can see that mysql_database specifies an image of mysql:5:7
. Like the PHP image we used as a base in the Dockerfile, this will get pulled from Docker Hub the first time our container is built. The my_php service doesn’t specify an image. Instead, it provides a build directory of the project. Docker Compose will look to our local Dockerfile to build the image.
Ports
Each service exposes ports that it will use. For MySQL, this is 3306. For Apache, it’s 80. In order to bind those to the host machine’s ports, you specify port mappings in the form - '<HOST_PORT><EXPOSED_IMAGE_PORT>'
under ports.
Volumes
I specify a few host volumes for each service. Volumes are a method for persisting data from containers. In mysql_database, for example, I use my local directory mysql_data
as the location for the MySQL container to store data. This means that even if I destroy the container, I still have a local copy of the database. In my_php, I use a volume for my src
directory. Because the container shares the data with my host machine, this means I can essentially write code to /var/www/html
in the container and have it take effect without rebuilding the image.
You can read more about the types of volumes here.
Environment variables
Lastly, I pass in environment variables to both containers. The ones used for MySQL are based on documentation for the MySQL image, and the ones for PHP will be pulled out of $_ENV
in the PHP code.
3. src/index.php
I don’t want to get into too many details about writing PHP, but I do want to show how to connect your PHP container to the MySQL one. Here’s the start of a file:
<?php
$user = $_ENV["MYSQL_USER"];
$password = $_ENV["MYSQL_PASSWORD"];
$database = $_ENV["MYSQL_DATABASE"];
$conn = new mysqli("mysql_database", $user, $password, $database);
// etc...
?>
This pulls out the environment variables that were specified in the docker-compose
file and uses them to connect to the database. What’s really cool here is that the first argument to mysqli
is the string “mysql_database,” rather than a URL. Docker handles the mapping for you.
Running It
With the above three files, you should be able to start the containers using docker-compose up --build
, and the server should be available at localhost:3000. The first time you run this, it may take a bit to download the images and set up MySQL. Hitting ctrl-C should stop and remove the containers.