Docker: a simple and practical example for a PHP/MySQL setup

Published on Jan 22, 2018

[if you are just looking for a quick way to get a LAMP-stack running in docker without needing any explanation, check out this repository]

Getting started with docker can be daunting at first, just like getting started with any technology you're not yet familiar with. Remember your first steps with vagrant, node, npm, mongodb, sass, js ... or any other tool/technology you thought you didn't need? You'll feel the same about docker.

Let me save you some time and explain the basic concepts to get you started as quickly as possible.

Heads up: I'm recording a free 1-hour video course on how to use Docker. It's in Dutch and you can sign up here.

What is docker and why do I need it for dev's sake?

Docker has been around for a couple of years now and you've probably seen the term containerization pop up in your favourite dev blogs. Well, docker can be seen as exactly that: containers with pieces of your app inside of them. Take a classic LAMP-stack for example. One popular approach would be to set up a VPS, install Linux on it and configure the operating system with all the packages your app needs, like MySQL, Apache and PHP.

Now imagine you've installed PHP7.2 on your server and you're thinking about deploying an additional app to the same server because it has resources enough to power both. Only caveat is that this other app was written a couple of years ago and it really isn't ready for PHP7 yet so you need PHP5.6 to run the damn thing. You're in a pretty pickle now because messing with PHP versions in a production environment isn't something you'd typically want to do, especially when that change will impact your complete system and all apps running on it.

If you were using Docker, you could simply spin up another container, set the PHP version to 5.6 and put it right next to your other container that has PHP version 7-shinynew on it, running on the same server with no problems whatsoever. That flexibility, portability and the option to more efficiently use your server resources are what make Docker so compelling to use.

But I use Vagrant, do I really need Docker?

You don't. I mean, it's all up to you. I've been a great fan of using Vagrant because it's super simple but it's not exactly what you would call lightweight. It needs a full VM to operate, uses Gigs of disk space and can easily take a couple of minutes to boot up. Docker images can be booted in seconds and are much smaller, because they don't need a full operating system by themselves. They are able to use the underlying operating system and share that.

On top of that, using Vagrant isn't the same as using your production server. The smallest difference in versions or packages can turn your carefully developed app into an error puking piece of crap.

But it works on my computer?! Sure it does, but that's not really helping all the cool kids wanting to use your app from their computers does it? Docker has got you covered my friend, because running a container locally gives you the exact same environment as running your container in production.

Ok, just tell me how I can get started

First, make sure you've installed Docker on your machine. Next, we'll need to go over the basic Docker concepts and put them to work. At the very least you should understand the difference and relation between image and containers.

Images vs. Containers vs. Builds

Let's get some basic terminology out of the way. 

Images are immutable files that have been created by building up multiple layers. An example is the following (which we'll expand upon).

FROM php:7.2-apache

COPY . /app
EXPOSE 80

What you see in the example above is a dockerfile describing what an image looks like when we build it. First we pull in one of the official php images. In the next step we'll copy our current files over to the docker image in /app and lastly we'll open up port 80 in order to be able to reach the Apache web server. Those layers can now be built into an image by using a command like the one below.

Don't worry too much about this command, we won't be using it in our setup. It just shows you that an image can be built based on the layers or steps you define in a dockerfile. 

docker build -t goodbytes:1.0 -f .docker/web.dockerfile .

When explaining these concepts to a programmer, the easiest way to look at an image is to compare it to a class. Containers on the other hand are instances of that same class. 

Right now, you could run the image above with the run command. That creates a writable layer on top of the image in which you can edit your code files, write data and files and whatever you please. When you destroy a container, this writable layer and all the data you created in it will be gone but the image will be intact and can be used to create new containers at will.

In case you'd like to run your image, give it a try by typing the following command.

docker run -p 8080:80 goodbytes:1.0

After running this command, a new container will be created from the image you built in the previous step and you'll be able to point your browser to http://localhost:8080. Depending on your application, not much else will happen but that's ok for now.

Docker Compose

The docker commands shown above are easy enough to write, but still, I personally really enjoy the ease of something like vagrant up, especially when your application needs more than one container. In our example, we'll spin up a php/apache container and an additional MySQL container. Instead of manually building and starting these containers one after the other, we can use Docker Compose to do all of that for us with just one command docker-compose up.

In order to do that, we'll need to create a docker-compose.yml file. Let's do so in the root of our project:

docker-compose.yml

# use version 3 of the docker compose syntax
version: '3'
services:
  # we named our first service 'web'
  web:
    # pass a hostname to the container (optinal)
    hostname: goodbytes.local
    
    # build a custom image 
    build:
      context: .
      dockerfile: .docker/web.dockerfile
    
    # a name for easier reference
    image: goodbytes.local

    # map host port 8080 to container port 80
    ports:
        - 8080:80
    
    # volumes are like shared folders
    # container will see your local code changes
    volumes:
      - .:/app
    
    # first load the 'db' service 
    depends_on:
      - db
    
    # make 'db' a known service/host inside of 'web'
    # use this to make a mysql connection to host 'db'
    links:
      - db
  
  db:
    # use a default image
    image: mysql:5.7

    # again, port mapping 
    # e.g. to use Sequel Pro on our mac
    ports:
      - 13306:3306

    # the mysql image uses these to create database and users
    environment: 
      MYSQL_ROOT_PASSWORD: something-secure-here
      MYSQL_DATABASE: bolt
      MYSQL_USER: bolt
      MYSQL_PASSWORD: something-secure-here

Remember, using docker-compose is optional, but it really makes booting up multiple containers as easy as running one simple command.

.docker/web.dockerfile

As you can see in the docker-compose.yml file above we are referencing a dockerfile in .docker/web.dockerfile. You can put that file wherever you want in your project, but I like to bundle things inside of a .docker folder. Anyway, this file contains your custom build/layering steps in order to build the image of your dreams. 

The code below is clear enough I think, but it goes like this:

  • grab the base php/apache image from the docker hub
  • copy all our project files over to a folder /app
  • copy a custom vhost over to the apache sites-available directory (this file is referenced below)
  • run extra commands in order to install the extensions your project requires (I like to keep this list as a reference)
  • in my case (this example is from a Bolt CMS project) pdo, mysqli and mod_rewrite were needed
FROM php:7.2-apache

COPY . /app
COPY ./.docker/vhost.conf /etc/apache2/sites-available/000-default.conf

WORKDIR /app
RUN apt-get update
RUN apt-get upgrade -y
RUN docker-php-ext-install pdo pdo_mysql mysqli
RUN a2enmod rewrite

If you are wondering why we aren't building a MySQL image as well, the answer is simple. If you can find an image online that does exactly what you need, you don't need to build your own. In our example, we wanted an additional Virtual Host and some PHP extensions so we needed to add some extra layers on top of the official and very basis PHP7.2 image.

.docker/vhost.conf

You've most likely created your share of VirtualHosts in Apache, so this should be pretty straightforward.

<VirtualHost *:80>
    DocumentRoot /app/public

    <Directory "/app">
        AllowOverride all
        Require all granted
    </Directory>
</VirtualHost>

Putting it all together

That magic moment is here to try out your new docker setup. Run the following from the root of your project, where your docker-compose.yml file sits.

docker-compose up

If all goes well, you'll notice that Docker will build the images the very first time you run this command. After that, your two containers will be started and you should be able to access your web app in your browser on http://localhost:8080. 

As always, results may vary and you may need to tweak a couple of things depending on your own software requirements. At the very least, this should give you a good starting point to try out Docker in your next project. 

Oh, one more thing. If you change your dockerfile and want to rebuild your image to reflect the changes, do so with docker-composer build

Good luck captain!

No comments? But that’s like a Gin & Tonic without the ice?

I’ve removed the comments but you can shoot me a message on LinkedIn to keep the conversation going.