PHP and Apache (or Nginx) in separate Docker containers using Docker Compose
In many cases, PHP and Apache are run in the same container and based on a single image.
For example, the PHP image has tags which combine Apache and Debian; Similarly for the Drupal CMS image.
If you are currently managing a project which uses php:apache-buster
on the linux/arm64/v8
architecture, the image’s uncompressed size as of this writing is 131.16 MB.
This might be fine for you; however maybe you want to move to Alpine, or Nginx; in such cases you might not be able to find a well-maintained base image to suit your needs.
That’s where the idea of using separate containers based on separate images comes in.
In this post we will examine how to do that.
Example of a single-container solution
Let’s look at the simplest possible single-container solution. We’ll have two files:
Our first file is docker-compose.yml
:
---
version: '3'
services:
php:
image: php:apache-buster
volumes:
- ".:/var/www/html"
ports:
- "8888:80"
Our second file will be index.php
:
<?php
print('<html><body><h2>This page was genrated on ' . date('Y-m-d H:i:s') . '</h2></body></html>');
Let’s build this and start the containers:
docker-compose up -d --build
Now when you visit http://0.0.0.0:8888, you should see something like:
This page was genrated on 2022-03-25 12:40:03
How to make this a two-container solution
First, I’d like to thank all the users who posted answers to Alpine variants of PHP and Apache/httpd in Docker on Stack Overflow, who pointed me in the right direction for this example. Most of the code is lifted directly from the article “Containerize This! How to use PHP, Apache, MySQL within Docker containers”, linked in the Resources section at the end of this post.
Let’s start by setting it up, and we’ll look at how it works after.
First, we need to create a conf file for Apache, in ./php.apache.conf:
ServerName localhost
LoadModule deflate_module /usr/local/apache2/modules/mod_deflate.so
LoadModule proxy_module /usr/local/apache2/modules/mod_proxy.so
LoadModule proxy_fcgi_module /usr/local/apache2/modules/mod_proxy_fcgi.so
<VirtualHost *:80>
# Proxy .php requests to port 9000 of the php-fpm container
ProxyPassMatch ^/(.*\.php(/.*)?)$ fcgi://php:9000/var/www/html/$1
DocumentRoot /var/www/html/
<Directory /var/www/html/>
DirectoryIndex index.php
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
# Send apache logs to stdout and stderr
CustomLog /proc/self/fd/1 common
ErrorLog /proc/self/fd/2
</VirtualHost>
We now need to create our own image in Dockerfile, which uses the above conf file, in Dockerfile-httpd:
FROM httpd:alpine
COPY php.apache.conf /usr/local/apache2/conf/php.apache.conf
RUN echo "Include /usr/local/apache2/conf/php.apache.conf" \
>> /usr/local/apache2/conf/httpd.conf
Finally, let’s rewrite our docker-compose.yml file:
version: '3'
services:
php:
image: php:fpm-alpine
volumes:
- ".:/var/www/html"
server:
build:
context: .
dockerfile: Dockerfile-httpd
volumes:
- ".:/var/www/html"
ports:
- "8888:80"
Let’s test it:
docker-compose up -d --build
Now when you visit, once again, http://0.0.0.0:8888, you should, again, see something like:
This page was genrated on 2022-03-25 12:49:03
Whoah, whoah, whoah, what’s going on here?
Part of the magic here is using PHP-FPM. Because we’re using FPM, the image broadcasts on TCP port 9000. You can confirm this by running:
docker-compose ps
You will notice that, in the PORTS column:
9000/tcp
This is how our Apache container will get the result from the PHP container even if PHP is not installed on the Apache container.
The second part of the magic happens in the php.apache.conf file, which directs Apache to fetch the result from an upstread server php at port 9000.
Finally, thanks to cytopia for cluing me into the necessety for the source files to be accessible as a volume on both PHP and Apache.
What about Nginx?
I tested an answer by Rafael Quintela, edited by Potherca, to the StackOverflow question “How to correctly link php-fpm and Nginx Docker containers?”, which works perfectly. I won’t reproduce it here, but I did make a few minor adjustments:
- I used the alpine tags because they’re much smaller in size, which makes me happy;
- You don’t need to expose port 9000 in docker-compose.yml, as user Seer pointed out in the comments.
The advantages of multiple containers
A different container for each process is really the Docker way, and allows for easier maintenance.
In addition, it gives us more leeway in selecting which webserver we want: swapping out Apache for Nginx, for instance, is easier if it’s completely separate from our PHP container.
Finally, it allows us to use Alpine images, hence reducing the compressed size of required resources:
Recall that our php:apache-buster
image was 131.16 MB; httpd:alpine
and php:fpm-alpine
, taken together, are 42.94 MB for the linux/arm64/v8
architecture, a 67% decrease in compressed size. If you’re using Nginx instead of Apache, the size is even smaller, at a combined 36.72 MB, a 72% size decrease over our first example in this post.