One of the most popular software combinations for developing and hosting PHP web applications is the LAMP stack, consisting of Linux, Apache2 (aka httpd), MySQL and PHPMyAdmin. While local installations for development have been used for decades, newer technologies like docker compose allow for more streamlined and reliable development environments across team members.
Why dockerize the stack?
Traditionally, to do web development, you would have to install the entire stack locally. While this is straightforward to do on most operating systems, using docker compose files comes with some serious advantages:
- Reproducible: The
docker-compose.yml
can be stored alongside the project's source code, making the exact environment the application was built in reproducible across different machines, developers or far into the future. - Isolation: Every project will have it's own environment, and changes made to one project's
php.ini
for example will not have side-effects on others. - Collaboration: Onboarding new team members is a breeze, as all they have to do is run a single docker command. Debugging errors in a team becomes more streamlined, as all members have the same development environment, so errors are easy to reproduce for team members.
Apache2 and PHP
To write PHP applications, we first of all need a web server and a PHP interpreter. Luckily, PHP's official image has a version that fits this need, bundling the apache2 web server with mod_php
enabled: php:8-apache. Setting this up in a docker-compose.yml
will look something like this:
version: "3.8"
services:
apache2:image: php:8-apache
volumes:
- $PWD/src:/var/www/html
ports:
- "127.0.0.1:8080:80"
This will expose the local directory src/
on http://127.0.0.1:8080/, with PHP support enabled.
While this will get us started with a basic PHP installation, it is missing some common extensions, especially image-centric ones such as gd, imagick and exif, as well as MySQL drivers mysqli and pdo_mysql.
To get these installed, we need to build our own docker image based on php:8-apache and install the extensions we need into it. While the image ships with a convenience script called docker-php-ext-install and some extensions ready to be installed, some require additional packages be installed and imagick is missing from this list completely, leading to a few more lines in our Dockerfile:
FROM php:8-apache
RUN apt update && apt install -y libpng-dev libfreetype6-dev libpng-dev libjpeg-dev libpng-dev libmagickwand-dev libc-client-dev libkrb5-dev --no-install-recommends && rm -rf /var/lib/apt/lists/*
RUN a2enmod rewrite
RUN mkdir -p /usr/src/php/ext/imagick && curl -fsSL https://github.com/Imagick/imagick/archive/06116aa24b76edaf6b1693198f79e6c295eda8a9.tar.gz | tar xvz -C "/usr/src/php/ext/imagick" --strip 1
RUN docker-php-ext-configure gd --with-jpeg=/usr/include/ --with-freetype=/usr/include/
RUN docker-php-ext-configure imap --with-kerberos --with-imap-ssl
RUN docker-php-ext-install bz2 exif gd imagick imap mysqli opcache pdo_mysql
This Dockerfile
first installs the libraries we need to install the modules, enables the apache2 module mod_rewrite
(to enable url rewriting from .htaccess
files) and downloads the third-party imagick
extension. Finally, we configure gd
to support jpeg and freetype fonts, and imap
with kerberos and ssl support. The last step simply installs all missing extensions.
MySQL and PHPMyAdmin
The last part of our LAMP stack is the MySQL database and the PHPMyAdmin administration interface. Both have official docker images we can use, but we will replace MySQL with the drop-in replacement MariaDB, as it tends to be better optimized and guarantees to support all MySQL features:
version: "3.8"
services:
mariadb:
image: mariadb:11
volumes:
- $PWD/volumes/mysql:/var/lib/mysql
environment:
- MYSQL_DATABASE=sampledb
- MYSQL_ROOT_PASSWORD=123
phpmyadmin:
image: phpmyadmin
environment:
- PMA_HOST=mariadb
- PMA_PORT=3306
ports:
- "127.0.0.1:8081:80"
This exposes PHPMyAdmin on http://127.0.0.1:8081 (one port above the apache2 server for convenience). The MySQL database is intentionally not exposed, as you will interact with it through PHPMyAdmin. Note that PHPMyAdmin is using mariadb
as the host for the database. This is intentional, as docker will place all the services in our docker-compose.yml into a network of their own. WIthin this network, they can reach each other by name, but may not be accessible from the outside through this method.
The MySQL database is configured to use the local directory volumes/mysql/
to store it's files, so that the contents of your database survive shutdowns or updates. You may have to create and give write privileges to this directory.
Merging all services into a single file
Since having 3 seperate files may be inconvenient in the future, let's combine them all into a single file. The contents of the Dockerfile
can be added to our apache2 service through the key dockerfile_inline
.
version: "3.8"
services:
apache2:
build:
dockerfile_inline: |
FROM php:8-apache
RUN apt update && apt install -y libpng-dev libfreetype6-dev libpng-dev libjpeg-dev libpng-dev libmagickwand-dev libc-client-dev libkrb5-dev --no-install-recommends && rm -rf /var/lib/apt/lists/*
RUN a2enmod rewrite
RUN mkdir -p /usr/src/php/ext/imagick && curl -fsSL https://github.com/Imagick/imagick/archive/06116aa24b76edaf6b1693198f79e6c295eda8a9.tar.gz | tar xvz -C "/usr/src/php/ext/imagick" --strip 1
RUN docker-php-ext-configure gd --with-jpeg=/usr/include/ --with-freetype=/usr/include/
RUN docker-php-ext-configure imap --with-kerberos --with-imap-ssl
RUN docker-php-ext-install bz2 exif gd imagick imap mysqli opcache pdo_mysql
volumes:
- $PWD/src:/var/www/html
ports:
- "${LAMP_HTTPD_PORT:-127.0.0.1:8080}:80"
mariadb:
image: mariadb:11
volumes:
- $PWD/volumes/mysql:/var/lib/mysql
environment:
- MYSQL_DATABASE=${LAMP_MYSQL_DATABASE:-sampledb}
- MYSQL_ROOT_PASSWORD=${LAMP_MYSQL_PASSWORD:-123}
phpmyadmin:
image: phpmyadmin
environment:
- PMA_HOST=mariadb
- PMA_PORT=3306
ports:
- "${LAMP_PHPMYADMIN_PORT:-127.0.0.1:8081}:80"
You may have noticed that we also replaced some environment variable values. This was done to allow for even more flexibility: The values for the ports of Apache2/PHPMyAdmin and the database name and password for MySQL became environment variables. If they are not set (or empty), they will simply fall back to their default values. But if you wanted to change them, you could either temporarily export them from your terminal using
export LAMP_DATABASE_PASSWORD=secret
Or permanently define them in a file called .env
in the same directory as docker-compose.yml
.
Using the docker compose file
To start the LAMP stack, simply run
docker compose up -d
To stop it:
docker compose down -v
Apache2 will serve your PHP application at http://127.0.0.1:8080, PHPMyAdmin is available at http://127.0.0.1:8081. Your PHP application can reach the MySQL database at mariadb:3306.
Local configuration can be set in .env
. Place the file alongside your application's source code to ensure all developers (or you in the future) work in the same environment.