Docker Image: NGINX with PHP FPM
Introduction
NGINX is one of the most popular lightweight web servers available making it the perfect candidate for containerization. I was using Apache for years but made the switch to NGINX when migrating to docker because of its small footprint and great performance characteristics. Features of this customized NGINX image are a custom directory structure, FastCGI Process Manager (FPM) enablement for Hypertext Preprocessor (PHP), and Local Transport Layer Security (TLS).
What is the image?
This docker image includes NGINX built on the Docker Official NGINX alpine image. The current build source is 1.20.x. For the exact version being used, see my Docker Hub: djpic/nginx. All modifications for this image’s variants are in the configuration file. Other than the configuration file, the NGINX service is the same as the official image.
How to use the image
The image is made to be used in tandem with djpic/php and djpic/traefik. Anything that is meant to be publicly served should be copied and/or mounted to /app/public/ directory. All image variants recognize index.html and index.htm as index files.
When the PHP FPM variants detected a PHP file is requested, the request is proxied to a PHP container on port 9000. It uses the domain name php to find the container. The PHP FPM variants will also recognize the index.php as an index file.
For more information on my djpic/php image, see the article Docker Image: PHP with MySQLi. For more information on my djpic/traefik image, see my other article Traefik v2 Secure TLS and Header Configuration with Docker Provider.
Image Tags
There are a total of 4 different variants: standard, tls, phpfpm, phpfpm-tls. Due to myself using the phpfpm variant the most, it is marked as the latest.
- standard
- The standard variant just includes the configuration options to make index.html and index.htm the index files. Also sets the default directory to /app/public/. This is a great option for static sites. There is no PHP-FPM configuration in this variant.
- tls
- The TLS variant includes the same as the standard but with localhost certificates installed for TLS enablement. This variant is best used behind a load balancer or reverse proxy such as Traefik. Some security departments require back end communications to be secured which is what this variant is built for.
- phpfpm
- This variant has the same settings as the standard image but with the added configuration enabling FPM for php and adding the additional index file index.php. The requests are sent to a container with DNS name of php on port 9000. This works great with the djpic/php image.
- phpfpm-tls
- The final variant is a combination of both the phpfpm and tls variants. Again designed to be used when security departments require back end communications to be secured.
How the image is built
This image starts like all others, as a Dockerfile. What is unique is the fact that there are 4 different Dockerfiles; one for each version.
The first Dockerfile builds the standard image by copying the configuration file:
FROM nginx:1.20.1-alpine
COPY ./default.conf /etc/nginx/conf.d/default.conf
WORKDIR /app/
The configuration file, default.conf, that is used for the standard image is as follows:
server {
listen 80;
index index.html index.htm index.xml;
server_name localhost;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /app/public;
}
For the tls version, the Dockerfile is very similar, but copies in the certificate files as well as the configuration file:
FROM nginx:1.20.1-alpine
COPY localhost.crt /certs/localhost.crt
COPY localhost.key /certs/localhost.key
COPY default.conf /etc/nginx/conf.d/default.conf
WORKDIR /app/
Here is the configuration file, default.conf, for the TLS version:
server {
listen 80;
listen 443 ssl;
ssl_certificate /certs/localhost.crt;
ssl_certificate_key /certs/localhost.key;
server_name localhost;
index index.html index.htm index.xml;
root /app/public;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
}
The phpfpm docker file is identical to the standard variant as that can been seen here:
FROM nginx:1.20.1-alpine
COPY ./default.conf /etc/nginx/conf.d/default.conf
WORKDIR /app/
The magic for the phpfpm variant happens in the configuration. When the request is sent for a PHP file, the request is proxied to the PHP container.
server {
listen 80;
index index.php index.html index.htm index.xml;
server_name localhost;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /app/public;
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
}
The fourth and final Dockerfile is for the phpfpm-tls variant. The Dockerfile for this variant is identical to the Dockerfile from the tls variant. Copies the certificate files and configuration files into the Docker Image.
FROM nginx:1.20.1-alpine
COPY localhost.crt /certs/localhost.crt
COPY localhost.key /certs/localhost.key
COPY default.conf /etc/nginx/conf.d/default.conf
WORKDIR /app/
The configuration for the phpfpm-tls variant combines both the tls variant and phpfpm configuration files into one.
server {
listen 80;
listen 443 ssl;
ssl_certificate /certs/localhost.crt;
ssl_certificate_key /certs/localhost.key;
server_name localhost;
index index.php index.html index.htm index.xml;
root /app/public;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
}
Last but not least, is the build script. The script starts by removing any stale images stored on the build machine. The script continues by building the standard variant followed by the phpfpm variant. After the standard and phpfpm variants are complete, the script generates the private key and self-signed certificate file. The private key is unique for each build. In addition, the certificate is set to expire in 10 years from the time the image was built. The private key is shared with anyone that downloads the public image. If your security policies state the private key must be truly unique, I would suggest building your own image with a unique private keys.
#!/bin/bash
# Clean old images
docker rmi djpic/nginx:standard djpic/nginx:tls djpic/nginx:phpfpm djpic/nginx:latest djpic/nginx:phpfpm-tls
# Build standard ngnix image
docker build --tag djpic/nginx:standard .
# Build phpfpm NGINX image
cd phpfpm
docker build --tag djpic/nginx:phpfpm .
docker tag djpic/nginx:phpfpm djpic/nginx:latest
# Create Certificate
cd ../
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes -keyout localhost.key -out localhost.crt -extensions san -config <(echo "[req]"; echo distinguished_name=req; echo "[san]"; echo subjectAltName=DNS:localhost) -subj /CN=localhost
cp localhost.key tls/localhost.key
cp localhost.crt tls/localhost.crt
mv localhost.key phpfpm/tls/localhost.key
mv localhost.crt phpfpm/tls/localhost.crt
# Build tls NGINX image
cd tls
docker build --tag djpic/nginx:tls .
rm localhost.crt localhost.key
# Build phpfpm NGINX image with tls
cd ../phpfpm/tls
docker build --tag djpic/nginx:phpfpm-tls .
rm localhost.crt localhost.key
# Push Images
docker push djpic/nginx:standard
docker push djpic/nginx:tls
docker push djpic/nginx:phpfpm
docker push djpic/nginx:latest
docker push djpic/nginx:phpfpm-tls
# Remove Images
docker rmi djpic/nginx:standard djpic/nginx:tls djpic/nginx:phpfpm djpic/nginx:latest djpic/nginx:phpfpm-tls
Sample docker-compose file
Since both my djpic/php and djpic/nginx images are designed to work together, I provided an example of a docker-compose file using both images. The two containers use the back end network to communicate. I do use a custom image of Traefik, so this docker-compose file does include djpic/traefik labels.
version: "3.2"
services:
nginx:
image: djpic/nginx:phpfpm
networks:
- FrontEnd
- BackEnd
depends_on:
- php
volumes:
- ./application/:/app
labels:
- "traefik.enable=true"
- "traefik.http.routers.djpicdemo-web.rule=Host(`demo.djpic.net`)"
- "traefik.http.routers.djpicdemo-web.entrypoints=web"
- "traefik.http.routers.djpicdemo-web.middlewares=https-redirect@file"
- "traefik.http.routers.djpicdemo-tls.rule=Host(`demo.djpic.net`)"
- "traefik.http.routers.djpicdemo-tls.entrypoints=websecure"
- "traefik.http.routers.djpicdemo-tls.tls.certresolver=letsencrypt"
- "traefik.http.routers.djpicdemo-tls.middlewares=secure-headers@file, compress-content@file"
restart: always
php:
image: djpic/php:mysqli
networks:
- BackEnd
volumes:
- ./application/:/app
memcached:
image: library/memcached:alpine
networks:
- BackEnd
command: ["-m", "64m"]
restart: always
networks:
FrontEnd:
external: true
BackEnd:
Conclusion
This djpic/nginx image paired with djpic/php image serve as the basis for all my web development. Be sure to read my article on php image to get the entire picture. All the Dockerfiles, scripts, and supporting files to build these images are available on Gitlab. Feel free to use these files to build your own but just remember the change the image names.
Follow my twitter account @djpic_llc for updates to the this article and other announcements. I also welcome all constructive input. If anything is incorrect or needs further explanation, feel free to contact me.