Creating A Light Dockerized LAMP/LEMP Stack

I’ve created a light Dockerized LAMP/LEMP Stack to share with you.

Docker has undoubtably and fundamentally changed the way we think of serving and hosting a website. It’s even had a momentous effect on how we develop websites locally, while keeping how we’re going to ship this once we’re ready.

I’ve found it difficult to go back to the old days of using tools like Laragon, XAMPP, WAMP/MAMP or even just a bare HTTP server like Apache. Docker is here to stay and it’s the future.

If you’re still using old-school tools like that you’re likely going to have a difficult time wrapping your head around the concept of Docker – which I won’t go into here. If you’re looking for a simple, yet easy to understand Dockerized solution to replace/upgrade your local development environment, allow me to share my lightweight solution.

I’d also like to take you through the internals and why I approached it they way I did. You can find my Docker LAMP solution here – free to use and dissect on GitHub.

Just a note: You will need both Docker and Git installed on your system. Usage instructions on how to get this project setup on your local system are listed in the GitHub repo’s readme.md file. Instructions can also be found in the same repo on how to use it once it’s up and running.

Before we begin, here is an overview of the project’s file structure in it’s purest form:

.
├── app (this is where your project files will live)
│   ├── composer.json
│   ├── composer.lock
│   └── index.php
├── mysql (safely ignore - configuration file(s))
│   └── my.cnf
├── nginx (safely ignore - configuration file(s))
│   └── default.conf
├── php (safely ignore - configuration file(s))
│   ├── php.ini
│   └── www.conf
├── .env.sample
├── .gitignore
├── Dockerfile
├── docker-compose.yml
└── readme.md

.env.sample

With the included .env.sample, we’ll ultimately be making a copy of this file (calling it simply .env in the project root) – this is where I’ve given the flexibility on a few things – namely the project’s name (used when building Docker images), both the ability to control the Nginx and MariaDB versions. Additionally, the ability to set the Database connection information (never include this information production!)

# Please use DockerHub to retrieve the corresponding versions - https://hub.docker.com/
# Please specify version numbers in Semver (eg. x.x.x) - https://semver.org/

# Use folder name here
PROJECT_NAME="sample-project"

# Versions
# PHP_VER=Specified in the Dockerfile
NGINX_VER="1.23.0-alpine"
MARIADB_VER="10.6.8"

# Database
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=db
DB_USERNAME=db
DB_PASSWORD=db

docker-compose.yml

The docker-compose.yml file is the first-part of the meaty goodness in getting our project up and running. Below you’ll see this commented out into 5 sections in our docker-compose spec file; Webserver, PHP container, Database, Networks and Volumes.

The variables in our spec file such as :${NGINX_VER} and ${PROJECT_NAME} are the values we set in our .env file (see above) and Docker is intelligent enough to look at that file when executing our docker-compose.yml file.

# Spec 3.8 as of Docker engine 19.03.0 (2019-07)
version: '3.8'
services:

  # --- Webserver: Nginx
  nginx:
    image: nginx:${NGINX_VER}
    container_name: ${PROJECT_NAME}-nginx
    restart: unless-stopped
    tty: true
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./app:/var/www
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
    networks:
      - app-network

  # --- PHP container: Project files
  php:
    build:
      context: .
      dockerfile: Dockerfile
    image: php:latest
    container_name: ${PROJECT_NAME}-php
    restart: unless-stopped
    tty: true
    working_dir: /var/www
    volumes:
      - ./app:/var/www
      - ./php/php.ini:/usr/local/etc/php/conf.d/php.ini
      - ./php/www.conf:/usr/local/etc/php-fpm.d/www.conf
    networks:
      - app-network

  # --- Database: MariaDB
  db:
    image: mariadb:${MARIADB_VER}
    container_name: ${PROJECT_NAME}-db
    restart: unless-stopped
    tty: true
    ports:
      - ${DB_PORT}:3306
    environment:
      MYSQL_ROOT_USER: 'root'
      MYSQL_ROOT_PASSWORD: 'root'
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${DB_USERNAME}
      MYSQL_PASSWORD: ${DB_PASSWORD}
    volumes:
      - dbdata:/var/lib/mysql/
      - ./mysql/my.cnf:/etc/mysql/my.cnf
    networks:
      - app-network

# --- Docker Networks
networks:
  app-network:
    driver: bridge

# --- Volumes
volumes:
  dbdata:
    driver: local

Dockerfile

The second-part of the bulk of this Docker project is the Dockerfile – this file is called automatically when the docker-compose process executes. This file is mostly responsible for setting up our environment for our project files as well as configuring the Alpine system, while installing system dependencies, PHP dependencies as tools we’ll need like Composer and Node.

This file will take the longest to execute in our build process as it manually needs to download and install each listed package and configure them (Alpine packages can easily be found at the Alpine Package Database). Just a note that Any failures in this file will cause the docker build process to fail. You will also note that because we are using Alpine Linux, the package install command is apk add rather than apt-get install (just something to note when working with Alpine based images).

FROM php:8.1.7-fpm-alpine3.16

# Set Composer version to install - https://getcomposer.org/download/
ARG COMPOSER_VERSION="2.3.7"
ARG COMPOSER_SUM="3f2d46787d51070f922bf991aa08324566f726f186076c2a5e4e8b01a8ea3fd0"

# Install system dependencies
RUN set -eux \
    && apk add --no-cache \
        ca-certificates \
        freetype \
        git \
        make \
        nano \
        nodejs \
        npm \
        openssl \
        tar \
        unzip \
        vim \
        yaml

# Install php extensions
RUN set -eux \
    && apk add --no-cache \
        curl-dev \
        freetype-dev \
        libjpeg-turbo-dev \
        libpng-dev \
        libwebp-dev \
        libzip-dev \
        openssl-dev \
        yaml-dev \
        zlib-dev

# Install Docker extensions
RUN set -eux \
    && docker-php-ext-install \
        exif \
        pcntl \
        pdo_mysql \
        zip

# Configure PHP extensions
RUN set -eux \
    && docker-php-ext-configure \
        gd --with-jpeg --with-webp --with-freetype

# Insall PHP gd extension
RUN set -eux \
    && docker-php-ext-install gd

# Install Composer
RUN set -eux \
    && curl -LO "https://getcomposer.org/download/${COMPOSER_VERSION}/composer.phar" \
    && echo "${COMPOSER_SUM}  composer.phar" | sha256sum -c - \
    && chmod +x composer.phar \
    && mv composer.phar /usr/local/bin/composer \
    && composer --version \
    && true

# Perform PHP-FPM testing
RUN set -eux \
    && echo "Performing PHP-FPM tests..." \
    && echo "date.timezone=UTC" > /usr/local/etc/php/php.ini \
    && php -v | grep -oE 'PHP\s[.0-9]+' | grep -oE '[.0-9]+' | grep '^8.1' \
    && /usr/local/sbin/php-fpm --test \
    \
    && PHP_ERROR="$( php -v 2>&1 1>/dev/null )" \
    && if [ -n "${PHP_ERROR}" ]; then echo "${PHP_ERROR}"; false; fi \
    && PHP_ERROR="$( php -i 2>&1 1>/dev/null )" \
    && if [ -n "${PHP_ERROR}" ]; then echo "${PHP_ERROR}"; false; fi \
    \
    && PHP_FPM_ERROR="$( php-fpm -v 2>&1 1>/dev/null )" \
    && if [ -n "${PHP_FPM_ERROR}" ]; then echo "${PHP_FPM_ERROR}"; false; fi \
    && PHP_FPM_ERROR="$( php-fpm -i 2>&1 1>/dev/null )" \
    && if [ -n "${PHP_FPM_ERROR}" ]; then echo "${PHP_FPM_ERROR}"; false; fi \
    && rm -f /usr/local/etc/php/php.ini \
    && true

# Set working directory
WORKDIR /var/www

# Copy existing application directory contents
COPY . /var/www

STOPSIGNAL SIGQUIT

# Expose port 9000 and start php-fpm server
EXPOSE 9000
CMD ["php-fpm"]

The next 3 files I’m going to breeze over are less important (however still required) as they are configuration files to tweak to get the most out of your Nginx, PHP and MariaDB performance.

  • mysql/my.cnf – your MySQL/MariaDB configuration settings that can be overridden.
  • nginx/default.conf – your Nginx server defaults that can be overridden.
  • php/php.ini – gets copied in during build time and used to change PHP defaults.
  • php/www.conf – used to tweak PHP-FPM settings.

This concludes the overview of my approach to setting this project up. If you have any feedback or suggestions on how to improve this, please feel free to let me know!

You can find the Docker LAMP solution here – free to use in your next project or dissect on GitHub.

Leave a comment