Nick Staroba Web Developer

Leveraging Docker to Create a Dev Environment

This isn't a primer on Docker, containers, virtual machines, system resources, or anything like that. All of that has been written about extensively elsewhere. This is about getting local development environments up and running, fast.

When I stumbled upon Docker, at first I thought it was something like Composer or NPM because I could "install with Docker." So, I looked it up: "Docker is an application build and deployment tool [that] can package your code with dependencies into a deployable unit called a container."

Docker gives me something similar to what my laptop gives me: agility. I can work just about anywhere with my laptop. It's my setup. It has all sorts of configurations for my current and potential workflows. I feel confident with it.

Docker creates predefined (and/or customizable), contained software stacks. This means I can learn new technologies confidently without much "setup investment" and without interfering with other projects already hosted on my machine.

For example, getting a basic, no-frills Apache server with PHP running is an easy one-liner:

docker run -d -p 8080:80 -v .:/var/www/html php:apache

Let's dissect this command:

  • docker run creates a container and then runs the processes we designate.
  • -d detaches the process so it can run in the background.
  • -p 8080:80 exposes the container's internal port 80 and maps it to an external port. In this case, our app will become available at http://localhost:8080.
  • -v "$PWD":/var/www/html mounts a volume so it can be edited locally and changes seen live. It is essentially replacing a directory inside the container (right of the colon) with a local directory (left).
  • php:apache is the process we're running. Left of the colon is the process, and to the right is a tag that designates which PHP docker image to use to create the container. Here, we're using PHP's pre-configured PHP/Apache image. Other images using other servers, no servers, or different versions of PHP are also available.

Run this command from a directory containing a simple PHP file with phpinfo() and we should see the current configuration. And that's it, really. Edit the PHP file, refresh http://localhost:8080 and the changes will be there.

"But I need Apache configured with x, y and z." Don't worry, all we have seen is the nibbling of the rabbit, not the rabbit itself, nor the rabbithole. The configuration used to create the php:apache image we ran can be dissected from its configuration fileā€“its Dockerfile. This particular configuration is complex, but the reality is, we don't have to understand it to get going with Docker, just know that it's there and can be used to define more complex systems if needed.

Docker has many other repositories that are officially managed by their respective owners: nginx, Alpine, Redis, Ubuntu, httpd, MySQL, Node.js, etc. Not only are these images straight from their creators, we're also able to see every Dockerfile for each tag so we can know exactly what the build is and how it's done.

So how can additions be made to this simple PHP setup? How can a MySQL container get working with this PHP container? Use multiple docker run commands? Sure, but it can get messy. Instead, use Docker Compose.

Bundled with Docker's Mac and Windows installers (manual install on Linux), Docker Compose uses YAML to breakdown Docker commands into a single, human-friendly configuration file.

But first, the php:apache image needs PHP's PDO extension installed so a connection can be made from the PHP application to test MySQL. Place this Dockerfile in the root of the project:

FROM php:apache

RUN docker-php-ext-install pdo_mysql

Simple. It uses the php:apache image, runs the command to install PDO, and creates a new image from which containers can be created.

The Docker Compose file (docker-compose.yml):

version: '3.1'

    build: .
    container_name: php_test_app
    image: php_apache_pdo
      - 8080:80
      - .:/var/www/html

    container_name: php_test_mysql
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: php_test
    image: mysql:5.7
      - ./data:/var/lib/mysql

The docker-compose.yml breakdown:

  • version: '3.1' - Choosing a version of Docker Compose compatible with the version of the currently installed Docker Engine.
  • services: - Defining the containers and their configurations.
  • app: - Chosen name of the PHP container definition (could just as well be application, webapp, etc.).
  • build: . - Directs Docker Compose to build an image from a Dockerfile located in the same directory as the docker-compose.yml file.
  • container_name: php_test_app - Designating a name for the container itself. Otherwise, a random name will be generated.
  • image: php_apache_pdo - If building a new image, this will become the tag, otherwise use the standard image:tag combination (i.e., php:apache).
  • Exposing port 80 inside the container and mapping it to 8080 locally.

      - 8080:80
  • Mapping the current directory to the public directory inside the container.

      - .:/var/www/html
  • db: - Chosen name of the MySQL container definition.
  • container_name: php_test_mysql - Important to note in this case is that this container name becomes our host when connecting to MySQL through the application.
  • Some images require certain environment variables to be set. In MySQL's case only the first variable is required, but the second sets up an empty database that will be used for a connection test in this example.

      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: php_test
  • image: mysql:5.7 - Using MySQL's version 5.7 image. No new images need to be built here.
  • Setting up a volume for persistent data so that even after a container is destroyed, the database files will remain intact in the data directory (created on the fly if necessary) in the root of our project.

      - ./data:/var/lib/mysql

Toss a PHP script that tests the database connection into the project's root directory as index.php:

$db = 'php_test';
$user = 'root';
$pass = 'password';
$host = 'php_test_mysql';

try {
  $db = new PDO("mysql:host=$host;dbname=$db", $user, $pass);
  echo "Connected successfully";
} catch (PDOException $e) {
  echo "Connection failed: " . $e->getMessage();

$req = $db->query("SELECT schema_name FROM information_schema.schemata");
foreach ($req->fetchAll(PDO::FETCH_ASSOC) as $record) {
  $results[] = $record;
echo '<pre>'; var_dump($results); echo '</pre>';

Now fire up the server: docker-compose up -d.

Bringing up http://localhost:8080 should result in "Connected successfully" and an array of databases.

When done with the setup, run docker-compose down. Docker Compose will terminate all containers and remove them automatically. Clean and easy.

There's so much more to Docker, but as of now, this itself has saved me many hours of frustration when setting up multiple and varied environments on a single machine.