Apache + PHP FPM + Mysql with docker

June 24, 2021

It was about time, I decided to update my stack and use docker on my servers.

For the ones that follow me, I usually prefer to set up a bare metal server with Debian Apache PHP and MySQL installed on it.
My excuses — if I even need some — were a mix between performances and simplicity.

But today I'm experimenting with multiple small projects, and they all are on the same server. It's still a bare metal server, and I'm not — yet — changing this for a cloud provider, we can talk about this in another blog post.

Multiple projects with a single server can lead to some incompatible dependencies, in my specific scenario I need some PHP extension that is obsolete and works only with older PHP version, but I don't want to carry this limitation to all my projects.

Why Apache and not Nginx? No reason ©.

Let's see how I built my LAMP stack with Docker (should we say DAMP?)

PHP

I used the php:7.4-fpm base image and added some extensions, those vary depending on the current project.

I added composer because it's always handy to have it there, plus, and this may be up for discussion, I added NodeJS + PM2 in this image too.

Node is often needed for the front end (compiling asset and such) and I did not want the complexity of another image (but I'm open for comment/PR on this one) as well as PM2 that I use for my background scripts. For example like this: pm2 start --name php_messenger_consume php -- bin/console -n messenger:consume async --limit=10 --memory-limit=128M --time-limit=3600

It's very handy to have it on the same image as PHP, and does not look even possible otherwise (let me know in the comments).

file:///docker/php/Dockerfile


FROM php:7.4-fpm

# PHP and related
RUN apt-get update && apt-get install -y --no-install-recommends \
        locales \
        apt-utils \
        git \
        g++ \
        libicu-dev \
        libpng-dev \
        libxml2-dev \
        libzip-dev \
        libonig-dev \
        libxslt-dev \
        unzip \
        libfreetype6-dev \
        libjpeg62-turbo-dev \
    && docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-configure intl \
    && docker-php-ext-install \
        pdo \
        pdo_mysql \
        opcache \
        intl \
        zip \
        calendar \
        dom \
        mbstring \
        gd \
        xsl \
    && pecl install apcu \
    && docker-php-ext-enable apcu \
\
    &&  curl -sS https://getcomposer.org/installer | php -- \
    &&  mv composer.phar /usr/local/bin/composer

RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"

# Node 14 and related
RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash - \
    && apt-get install -y --no-install-recommends nodejs \
    && npm install [email protected] -g \
    && pm2 install pm2-logrotate \
    && pm2 set pm2-logrotate:compress true

Apache

This on is a bit more tricky.

First on my production server I won't use it, the server will have a real Apache with vhosts configured properly forwarding to each different projects. Those final vhosts will look similar to the one below that I use in the development image.

So for the dev env (hence the docker-compose-dev.yml coming up below) it will have Apache.

The goal is to redirect the query that hit the server to PHP FPM.

For this, I first tried to use the base image httpd:2.4 it seemed logic to me but for some reason I never succeeded to make it work, (have you? PR welcome). I ended up using a Debian base debian:buster-slim and installed Apache on my own (probably not optimal, but we are on a dev environment).

It has mod proxy fast CGI and mod rewrite activated as we will use it in our config.

file:///docker/apache/Dockerfile


FROM debian:buster-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
    apache2 \
    && a2enmod proxy_fcgi \
    && a2enmod rewrite

CMD /usr/sbin/apache2ctl -D FOREGROUND

MySQL

I'm using the base image mysql with some options like MYSQL_ALLOW_EMPTY_PASSWORD and cap_add: SYS_NICE. The later prevents the error message "mbind: Operation not permitted" as seen on stackoverflow.com

Connecting everything together

The idea now it to put everything together:

  • Apache will be exposed to some port and redirect requests to PHP-FPM.
  • PHP will be able to connect to the database.
  • And also we can use PHP as a command line on our server to execute PHP commands.

I added comments on the file directly, so you have the context.

Note: the name of the project is "bookmark" in this example

Production

file:///docker-compose.yml


version: "3.3"
services:

    database:
        image: mysql
        container_name: bookmark_mysql
        restart: always
        # We set up a docker volume to store our database data
        volumes:
            - bookmark-mysql-data:/var/lib/mysql
        environment:
            MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
        networks:
            - bookmark-network
        # This option fix the "mbind: Operation not permitted" error that pops in the logs
        # https://stackoverflow.com/a/55706057/696517
        cap_add:
            - SYS_NICE

    php:
        # Comes from the Dockerfile we talked about earlier
        build: docker/php
        container_name: bookmark_php
        # We will mount our application code in this directory
        volumes:
            - ./:/var/www
        restart: always
        # Connect this to the common network
        networks:
            - bookmark-network

# Common network among all the machines
networks:
    bookmark-network:

volumes:
    bookmark-mysql-data:

This is the version used in production, so without Apache.

Now the version for local dev

Development

As you probably now, docker-compose config files cascade (like CSS), and we can supersede options Again I added comment strait in the file for context.

file:///docker-compose-dev.yml


version: "3.3"
services:

    database:
        # For local development I expose the database
        # Expose port 3306 (mysql default) and bind it to 3307 on the local machine
        ports:
          - "3307:3306"
        # Not auto-restarting the container in local
        restart: "no"

    # I love this project for dev purpose, I let you check more about it here: https://github.com/maildev/maildev
    # it's optional and I won't go in detail about it now
    mail:
        image: maildev/maildev
        container_name: bookmark_maildev
        command: bin/maildev --web 80 --smtp 25 --hide-extensions STARTTLS
        ports:
          - "8083:80"
        restart: "no"
        networks:
            - bookmark-network

    php:
        # Not auto-restarting the container in local
        restart: "no"

    apache:
        # For local development I set up an Apache server
        # It comes from the Dockerfile we talked about earlier
        build: docker/apache
        container_name: bookmark_apache
        # Start after those image
        depends_on:
            - php
            - database
        # Expose port 80 and bind it to 9001 on the local machine
        ports:
            - "9001:80"
        volumes:
            # We will mount our application code in this directory
            - ./:/var/www
            # This one is to allow us to set up apache vhosts (see below)
            - ./docker/apache/conf:/etc/apache2/sites-enabled
        restart: "no"
        networks:
            - bookmark-network

Glue in between

Be it in production or development, having Apache on the server or in a docker image, you now have to redirect web request to PHP FPM

This is done with an Apache config file (here called vhosts.conf for historical reasons)

This config file is similar between a production environment and a dev environment, main differences will be explained in inline comments

Note: the base version here is the dev environment.

file:///apache/conf/vhosts.conf


ServerName localhost
# Listening on port 80, I use cloudflare, and they handle the SSL part,
# otherwise you will need a second similar section for SSL
<VirtualHost *:80>
    # Follow important Auth headers
    SetEnvIfNoCase ^Authorization$ "(.+)" HTTP_AUTHORIZATION=$1

    # Define our app directory
    DocumentRoot /var/www/public

    # Rule to redirect request to PHP files to our PHP docker image
    # in production environment you would have to expose the PHP docker image and update the fcgi uri below with the right hostname and port
    <FilesMatch \.php$>
        SetHandler proxy:fcgi://php:9000
    </FilesMatch>

    # Rules for our main directory
    <Directory /var/www/public>
        # Will rewrite requests
        RewriteEngine On

        # If the request is a file or a directory, skip the rewrite
        RewriteCond %{REQUEST_FILENAME} !-f
        RewriteCond %{REQUEST_FILENAME} !-d

        # Remove the index.php from the URL
        RewriteRule ^(.*)$ index.php [QSA,L]

        # Other quite basic config
        # see apache doc
        AllowOverride None
        Require all granted
        Allow from All
        FallbackResource /index.php
    </Directory>

    # These are basic configuration option (optional)
    # see apache doc
    <Directory /var/www>
        Options FollowSymlinks
    </Directory>

    <Directory /var/www/public/build>
        DirectoryIndex disabled
        FallbackResource disabled
    </Directory>

    CustomLog /proc/self/fd/1 common
    ErrorLog /proc/self/fd/2

</VirtualHost>