Using Docker for a PHP, MariaDB and Nginx project
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!).
- (Digital Ocean) How to install and use Docker on Ubuntu 18.04
- (Digital Ocean) How To Install Docker Compose on Ubuntu 18.04
- (Digital Ocean) How to install and use Docker on Ubuntu 20.04
- (Digital Ocean) How To Install and Use Docker Compose on Ubuntu 22.04
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 calledjonsdocs
- 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 managementpecl
is a PHP extension managerdocker-php-ext-install
installs and enables extensions for PHPdocker-php-ext-enable
enables PHP extensions within the container (Imagick is not available viadocker-php-ext-install
)curl
is used to downloadcomposer
(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:
- Open a terminal
- Change to the directory containing your
docker-compose.yml
file - 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.