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>