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 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.

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 version.

The first Dockerfile builds the standard image by copying the configuration file:

ARG nginx_version

FROM nginx:${nginx_version}

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 version, 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/

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;

  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";
  }
}

Finally the build script to bring everything together. Update the variable "current_nginx_version" to the correct NGINX version that will be used. The script then continues by generating the localhost private key and certificate with OpenSSL that will be used in all the TLS variants. The certificate is set to expire in 10 years from the image build date. The both the static and static tls variants are created first followed by phpfpm variants.

A special note on the TLS versions of this image. The private key is shared with anyone that downloads the docker image. For security, I highly recommend generating your own certificates with unique private keys.

#!/bin/sh

# What base image of NGINX to use for all builds
current_nginx_version=1.24.0

# Create Certificate
openssl req -x509 -nodes -days 3650 -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
docker build --build-arg nginx_version=$current_nginx_version --tag djpic/nginx:$current_nginx_version-static .

# Build tls NGINX image
cd tls || exit
docker build --build-arg nginx_version=$current_nginx_version --tag djpic/nginx:$current_nginx_version-static-tls .

# Build phpfpm NGINX image
cd ../../phpfpm || exit
docker build --build-arg nginx_version=$current_nginx_version --tag djpic/nginx:$current_nginx_version-phpfpm .
docker tag djpic/nginx:$current_nginx_version-phpfpm djpic/nginx:latest

# Build phpfpm NGINX image with tls
cd tls || exit
docker build --build-arg nginx_version=$current_nginx_version --tag djpic/nginx:$current_nginx_version-phpfpm-tls .

# Push Images
docker push djpic/nginx:$current_nginx_version-static
docker push djpic/nginx:$current_nginx_version-static-tls
docker push djpic/nginx:$current_nginx_version-phpfpm
docker push djpic/nginx:$current_nginx_version-phpfpm-tls
docker push djpic/nginx:latest

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:
      - 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:
  Treafik:
    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.

Disclaimer:
The contents of this article are of my opinion and based on my experience. Before applying any of the concepts or suggestions in this article, complete your own independent research and testing. By reading this article, the reader agrees that Dale M. Picou, Jr. shall not be held liable for any negative impact when applying these tutorials, concepts, and/or suggestions.