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. 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 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. The NGINX container uses the domain name php within Docker to find the PHP 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 Secure TLS and Header Configuration with Docker Provider.
Image Tags
There are a total of four different variants: standard, tls, phpfpm, phpfpm-tls. Due to myself using the phpfpm variant the most, it is marked as the latest.
- static
- The static 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 static 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 static 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 variant.
The first Dockerfile builds the standard image by copying the configuration file:
ARG nginx_version
FROM nginx:${nginx_version}-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;
expires 5m;
add_header Cache-Control "must-revalidate";
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;
location ~* \.(?:css|js|html|htm)$ {
expires 5m;
add_header Cache-Control "no-cache";
}
}
For the tls variant, the Dockerfile is very similar, but copies in the certificate files as well as the configuration file:
ARG nginx_version
FROM nginx:${nginx_version}-alpine
COPY localhost.crt /certs/localhost.crt
COPY localhost.key /certs/localhost.key
COPY default.conf /etc/nginx/conf.d/default.conf
WORKDIR /app/
Similar to the static default.config, the TLS variant includes the listening on port 443 and applies the SSL certificates:
server {
listen 80;
listen 443 ssl;
ssl_certificate /certs/localhost.crt;
ssl_certificate_key /certs/localhost.key;
expires 5m;
add_header Cache-Control "must-revalidate";
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;
location ~* \.(?:css|js|html|htm)$ {
expires 5m;
add_header Cache-Control "no-cache";
}
}
The phpfpm docker file is identical to the static variant as that can be seen here:
ARG nginx_version
FROM nginx:${nginx_version}-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;
expires 5m;
add_header Cache-Control "must-revalidate";
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$ {
add_header Cache-Control "no-store";
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;
}
location ~* \.(?:css|js|html|htm)$ {
expires 5m;
add_header Cache-Control "no-cache";
}
}
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.
ARG nginx_version
FROM nginx:${nginx_version}-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$ {
add_header Cache-Control "no-store";
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;
}
location ~* \.(?:css|js|html|htm)$ {
expires 5m;
add_header Cache-Control "must-revalidate";
}
}
Next, using the version passed via the command line argument, the build.sh bash script builds and pushes the container images to the registries.
To start, the script generates the localhost TLS certificates and private keys using OpenSSL which will expire in one year. These certificates and private keys will be used in both the static TLS variants. A special note when using the TLS variants: Since the private key is shared with anyone that downloads the pre-built djpic/nginx TLS variant images, it is HIGHLY recommended to generate unique private keys and certificates.
The build script continues to build each variant using the correct Dockerfile. After all the variants are built for the selected NGINX version, they are pushed to both Gitlab and DockerHub.
#!/bin/bash
# Pull the command line argument included when the script is executed
nginx_version=$1
if [[ -v nginx_version ]]; then
# Create Certificate
openssl req -x509 -nodes -days 365 -newkey rsa:4096 -keyout localhost.key -out localhost.crt -config openssl.conf -extensions 'v3_req'
cp localhost.key static/tls/localhost.key
cp localhost.crt static/tls/localhost.crt
mv localhost.key phpfpm/tls/localhost.key
mv localhost.crt phpfpm/tls/localhost.crt
# Build standard ngnix image
cd static || exit 1
docker build --build-arg nginx_version=$nginx_version --tag djpic/nginx:$nginx_version-static --tag $CI_REGISTRY_IMAGE/nginx:$nginx_version-static .
# Build tls NGINX image
cd tls || exit 1
docker build --build-arg nginx_version=$nginx_version --tag djpic/nginx:$nginx_version-static-tls --tag $CI_REGISTRY_IMAGE/nginx:$nginx_version-static-tls .
# Build phpfpm NGINX image
cd ../../phpfpm || exit 1
docker build --build-arg nginx_version=$nginx_version --tag djpic/nginx:$nginx_version-phpfpm --tag $CI_REGISTRY_IMAGE/nginx:$nginx_version-phpfpm .
docker tag djpic/nginx:$nginx_version-phpfpm djpic/nginx:latest
docker tag $CI_REGISTRY_IMAGE/nginx:$nginx_version-phpfpm $CI_REGISTRY_IMAGE/nginx:latest
# Build phpfpm NGINX image with tls
cd tls || exit 1
docker build --build-arg nginx_version=$nginx_version --tag djpic/nginx:$nginx_version-phpfpm-tls --tag $CI_REGISTRY_IMAGE/nginx:$nginx_version-phpfpm-tls .
# Push Images
docker push $CI_REGISTRY_IMAGE/nginx:$nginx_version-static
docker push $CI_REGISTRY_IMAGE/nginx:$nginx_version-static-tls
docker push $CI_REGISTRY_IMAGE/nginx:$nginx_version-phpfpm
docker push $CI_REGISTRY_IMAGE/nginx:$nginx_version-phpfpm-tls
docker push $CI_REGISTRY_IMAGE/nginx:latest
docker push djpic/nginx:$nginx_version-static
docker push djpic/nginx:$nginx_version-static-tls
docker push djpic/nginx:$nginx_version-phpfpm
docker push djpic/nginx:$nginx_version-phpfpm-tls
docker push djpic/nginx:latest
else
echo "Nginx Version Missing"
exit 1
fi
Finally bringing everything together is the .gitlab-ci.yml file. Each NGINX version gets its own job allowing the jobs to run in parallel vs. in series, reducing the total build time. Each job executes in a docker-in-docker (DinD) container making cleanup easy.
The jobs are straightforward and follow the same pattern. First using the before_Script, bash is installed into the deployed DinD container followed by the registry logins. In this scenario, the images are uploaded to both DockerHub and GitLab container registries. The 'build.sh' script is granted the necessary permissions and executed with the NGINX version to be built provided as a command-line argument.
The GitLab pipeline is scheduled to run once a week to keep the containers up to date with the latest bug fixes.
# Gitlab CI to build docker images
stages:
- build
- release
services:
- docker:dind
# Build NGINX Version 1.26
nginx-1.26:
stage: build
tags:
- DockerExe
before_script:
- apk update
- apk add bash
- docker login --username $dockerhub_username --password $dockerhub_password
- docker login --username $CI_REGISTRY_USER --password $CI_REGISTRY_PASSWORD $CI_REGISTRY
- chmod 755 build.sh
script:
- ./build.sh 1.26
# Build NGINX Version 1.27
nginx-1.27:
stage: build
tags:
- DockerExe
before_script:
- apk update
- apk add bash
- docker login --username $dockerhub_username --password $dockerhub_password
- docker login --username $CI_REGISTRY_USER --password $CI_REGISTRY_PASSWORD $CI_REGISTRY
- chmod 755 build.sh
script:
- ./build.sh 1.27
include:
# Only Run the production CI when a production tag is added.
- local: .release.gitlab-ci.yml
rules:
- if: '$CI_COMMIT_TAG =~ /^prod-.*$/'
when: always
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.
project_name: "samplecomposefile"
services:
nginx:
image: djpic/nginx:phpfpm
networks:
- Traefik
- 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:
Traefik:
external: true
BackEnd:
Conclusion
This djpic/nginx image paired with djpic/php image serves 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 remember the change the image names.
Follow my Twitter account @djpic_llc for updates to this article and other announcements. I also welcome all constructive input. If anything is incorrect or needs further explanation, feel free to contact me.