Dale M. Picou, Jr.

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.19.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.
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 I build the image

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.19.7-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;
  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.19.7-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;
  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.19.7-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.html index.htm index.php;
  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.19.7-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.html index.htm index.php;
  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 it 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


# Buidjpic/phpld 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"
      #- "traefik.http.services.djpicdemo.loadbalancer.server.port=443"
      #- "traefik.http.services.djpicdemo.loadbalancer.server.scheme=https"
    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.

I welcome all constructive input. If anything in 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.