A Gentle Introduction to Local Development with Docker Compose

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 the src/ 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.