Using Docker for a PHP, MariaDB and Nginx project

Collage of the Docker, PHP, MariaDB and Nginx logos.

Those of you that are regular readers will notice that my posting is waaaay behind schedule, and that my goal of posting once a week has failed again so far.  Sorry about that!  Hopefully this post will be useful to you.

I use my blog primarily for two things: because I enjoy writing (and hope you enjoy reading) and as a notebook of sorts documenting things I had to find out.  Posts in the how to category are hopefully useful to others and will save you time.  In this post I'm going to describe a Docker setup I put together recently to support my work on eVitabu (and a bunch of other projects).  I got a lot of assistance from other posts (and a friend), so there's nothing unique here, but this is a quick reference to build the same stack.

Just give me the docker-compose.yml!

Just want the code?  You can get it from my public GitHub repository for Docker examples.  The stack is described in the rest of this post.  You will need more than just the docker-compose.yml file though, so make sure you get all of the files.

This blog post refers to the contents of the PHP8-1_MariaDB10-9_Nginx directory.

Caveat: I'm not a Docker expert!

I'm providing this blog post "as is", without any warranty of any kind.  I'd strongly advise against using this in a published, production, environment without performing checks and security hardening.  There are definitely improvements that can be made here (for a start, the MariaDB root password [1] is saved in the docker-compose.yml file in these examples, which isn't wise).

What is Docker?

Docker allows us to run environments inside containers, self-contained boxes (if you like) that have their own configuration and running services, potentially isolated from the host machine.  Containers allow for reproducible builds, as everything needed for the environment is defined including application versions.

Usefully it means that if you're using containers, code that works on your machine (in your container) will run on someone else's so long as they use the same container.

Because the environment is self-contained you can run multiple containers each running different versions of applications (e.g. PHP 5.6, PHP 7.1, PHP 8.2) on the same machine without any conflicts.  This also leads to a cleaner development machine as your host isn't contaminated.

How do I get Docker?

There are a lot of good tutorials available already on how to get Docker and Docker Compose, so I won't repeat them here.  Instead, links are provided for the reader.  In my case I was setting this up on my dev / gaming laptop which is was running elementaryOS 5.1 (Hera), based on Ubuntu 18.04.

I find Digital Ocean's tutorials really helpful.  They're available to all, even non customers, so I've linked some options below.  Digital Ocean also sponsor eVitabu (thanks!).

What's in this stack?

I mostly develop using PHP, MariaDB (or MySQL), and Nginx.  My chosen PHP framework is Yii2, and I use a variety of libraries and PHP extensions (including Imagick) in my projects.  Each component (PHP runtime, web server, database server) runs in its own container.  For this I built a stack of:

  • PHP
  • MariaDB
  • Nginx

I've included the following PHP extensions, as these are needed by my projects.  Some aren't immediately available by docker-php-ext-install so there are additional steps.

  • curl
  • gd
  • imagick
  • mbstring
  • mysqli
  • pdo
  • pdo_mysql
  • xml

Prerequisites and versions

For this to work you will need both Docker and Docker Compose.  I'm also assuming you have the administrator rights necessary for your host operating system to start containers and bind their specific ports.

On my Ubuntu 18.04 based laptop I am using the following versions:

  • Docker: 23.0.1, build a5ee5b1
  • Docker Compose: 1.17.1

File structure

Almost certainly this isn't the best file structure, which for me is in a subdirectory of my project, I largely copied it from a tutorial I was following, but here's the layout of my files.  I'll describe the contents of each file below, but remember the caveat that I'm not an expert!

|_ project
    |_ <My PHP code>
    |_ docker
        |_ docker-compose.yml
        |_ php-dockerfile
        |_ php-logging.conf
        |_ mysqldata
            |_ <database files>
        |_ nginx-conf
            |_ nginx.conf

docker-compose.yml

When running docker-compose it's this file, docker-compose.yml that provides the "recipe" to be followed.  In the file below I define:

  • Config syntax version, based on the version of docker-compose (3.5 was the latest I could use based on my version)
  • A name for the "collection" of containers (if you don't specify this, the collection will be called docker by default).  In this example the collection is called jonsdocs
  • Three "services", each with their volumes, network links, and what the service depends on
  • A volume to store the database files

A service is essentially a Docker container, and the PHP service is probably the most complicated of this set.  Because I'm adding additional extensions to the PHP environment there's a separate file, php-dockerfile that describes that environment in more detail.

By using the volumes section I can pass files into the container.  These files aren't copied, they're mapped straight in, which means any changes I make on my host will be reflected in the container.  This is helpful for my PHP code because it means I can update a file and it will be used without me having to restart the containers.

Under the nginx and mariadb services there is an image specified.  These are prebuilt and pulled from a central repository online.  The version can be specified, or the word latest to get the most recent image.

Using the ports directive I can map a port on my host machine to a port inside the container.  For example, to access port 80 of the web server from my host machine I would point my browser at http://127.0.0.1:8080.  Links between the containers are given via the links directive, and we can see the nginx container has a link into the php one.  Dependencies between containers are specified by depends_on.

version: '3.5'
name: jonsdocs

# Services
services:

  # PHP FPM Service
  php:
    container_name: php
    build:
      dockerfile: php-dockerfile
      context: .
    volumes:
      - '../:/var/www/html'
      - './php-logging.conf:/usr/local/etc/php-fpm.d/zz-log.conf'
    depends_on:
      - mariadb

  # Nginx Service
  nginx:
    container_name: web
    image: nginx:latest
    ports:
      - 8080:80
    links:
      - 'php'
    volumes:
      - '../:/var/www/html'
      - './nginx-conf:/etc/nginx/conf.d'
    depends_on:
      - php

  # MariaDB Service
  mariadb:
    container_name: db
    image: mariadb:10.9
    environment:
      MYSQL_ROOT_PASSWORD: YOURPASSWORDHERE
    volumes:
      - './mysqldata:/var/lib/mysql'

# Volumes
volumes:
  mysqldata:

php-dockerfile

This dockerfile gives instructions on how to make a container by taking an image (defined using the FROM directive) and then running additional commands (given using the RUN directive).  The php:8.1-fpm image seems to be Debian or Ubuntu based and commands are run to set up the environment as I need it.

  • apt-get related commands are for Debian / Ubuntu package management
  • pecl is a PHP extension manager
  • docker-php-ext-install installs and enables extensions for PHP
  • docker-php-ext-enable enables PHP extensions within the container (Imagick is not available via docker-php-ext-install)
  • curl is used to download composer (a PHP package manager) that's needed by my projects
FROM php:8.1-fpm

# Installing dependencies for the PHP modules
RUN apt-get update && \
    apt-get install -y zip curl libcurl3-dev libzip-dev libpng-dev libonig-dev libxml2-dev
    # libonig-dev is needed for oniguruma which is needed for mbstring

# Installing additional PHP modules
RUN docker-php-ext-install curl gd mbstring mysqli pdo pdo_mysql xml

# Install and configure ImageMagick
RUN apt-get install -y libmagickwand-dev
RUN pecl install imagick
RUN docker-php-ext-enable imagick
RUN apt-get purge -y libmagickwand-dev

# Install Composer so it's available
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

php-logging.conf

This file gets pulled into the PHP container and adjusts the PHP-FPM configuration to disable displaying PHP errors and sending them to the log instead.  This can probably but merged into the php-dockerfile at a later date, but right now it works 🙂️.

php_admin_flag[log_errors] = on
php_flag[display_errors] = off

nginx-conf\nginx.conf

Probably the most familiar file in this project for those that have experience with the Nginx web server.  Docker simply maps this file into the Nginx container and the web server uses it to run.  My only particular change here was to support Yii2's URL manager pretty URLs.

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    
    server_name localhost;

    root /var/www/html/web;
    index index.php index.html;

    # Support Yii2 pretty URL routing
    location / {
            try_files $uri $uri/ =404;
            if (!-e $request_filename){
                    rewrite ^/(.*) /index.php?r=$1 last;
            }
    }

    location ~* \.php$ {
        fastcgi_pass php:9000;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param SCRIPT_NAME $fastcgi_script_name;
    }

    # Prevent additional headers like TRACE, DELETE, PUSH
    if ($request_method !~ ^(GET|HEAD|POST)$ )
        {
            return 405;
        }
}

Starting the stack

We'll use docker-compose to run the stack we've just defined.  To do this:

  1. Open a terminal
  2. Change to the directory containing your docker-compose.yml file
  3. Run docker-compose up

On the first time this is run, Docker will download the images for the nginx and mariadb services.  It will also pull down the PHP image and customise it per our php-dockerfile.  Once the images are built, Docker will start the containers, passing in our code and config files.

Accessing the web pages

Simply browsing to http://127.0.0.1:8080 should show the web pages.

Troubleshooting

No build context specified

ERROR: The Compose file is invalid because:
Service php has neither an image nor a build context specified. At least one must be provided.

If you don't provide a context directive under your build section then Docker Compose won't know the working directory for files.  That's how it seems to me anyway.  This is fixed by adding "context: .".

Further reading and thanks

The following people, blogs and sites helped me to get this working:

  • Long time coding partner and friend Adam, who cracked the "context" issue
  • Linuxiac's article
  • Digital Ocean articles (linked above)

Banner image: A collage of the Docker, PHP, MariaDB and Nginx logos, copyright their respective authors.

[1] This example password isn't used by me anywhere.

Update 2024-01-16: The docker-compose.yml was updated to specify a name for the collection.