Setting up a LAMP stack for development in docker

Table of contents

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.

More articles

Responsible web scraping considerations

Web scraping within legal limits, explained for humans

Documenting HTTP APIs with OpenAPI

Make your API documentation more user-friendly and streamline the writing process

Software versioning explained

Discussing pros and cons of different approaches to handling software versions

Protecting linux servers from malware with ClamAV and rkhunter

Finding malicious files the open source way

Creating an effective disaster recovery plan for IT infrastructure

Know what to do when things go wrong ahead of time

Dealing with pagination queries in SQL

The difficult choice behind chunking results into simple pages