Deploying Nginx + PHP + git-sync on Kubernetes

In my previous post, I explained how to setup a simple nginx instance that could be used to sync to a private Git repo. The only drawback is that this setup will only serve static pages. What if you wanted to be able to run a server with dynamic code like PHP? I’m glad you asked! In this post, we’ll update our config to include a php-fpm instance to allow us to serve PHP pages.

I have planned these articles out so that they build on each other. With that in mind, I’m assuming you have followed my articles to date and therefore we’ll be simply extended the current deployment.

If you’re impatient like me, just scroll to the bottom and download the full files.

Setting Up The PHP-FPM Instance

First we need to get our PHP-FPM yaml setup. By default, php-fpm runs on port 9000. This means we need a service definition to expose this to the cluster. This will also need access to the git repo we created so we’ll add in the git container spec. Instead of running the nginx image, we’ll run the php-fpm image. In order to make life easy on ourselves, I’m going to use the webserver.yaml from my previous post as a template. I’m going to make the following changes to it:

  1. Replace any reference of “webserver” with “phpfpm”.
  2. Change the following in the service definition
    1. change the port name from http to phpfpm
    2. change the port number from 80 to 9000
  3. Remove the ConfigMap
    1. Remove the definition of it from the top of the file
    2. Remove the references to it in the spec volumes and the container volumeMounts
  4. Change the image of the second container from nginx:latest to php:fpm
  5. Change the containerPort from 80 to 9000

If we’ve done this all correctly, we should have a yaml that looks similar to the below:

apiVersion: v1
  
 kind: Service
 metadata:
   name: phpfpm
   labels:
     tier: backend
 spec:
   selector:
     app: phpfpm
     tier: backend
   ports:  
   - name: phpfpm
     port: 9000
 ---
 apiVersion: apps/v1
 kind: Deployment
 metadata:
   name: phpfpm
   labels:
     tier: backend
 spec:
   replicas: 1
   selector:
     matchLabels:
       app: phpfpm
       tier: backend
   template:
     metadata:
       labels:
         app: phpfpm
         tier: backend
     spec:
       securityContext:
         fsGroup: 65533 # to make SSH key readable
       volumes:
       - name: dir
         emptyDir: {}
       - name: git-secret
         secret:
           secretName: github-creds
           defaultMode: 288
       containers:
       - env:
         - name: GIT_SYNC_REPO
           value: git@github.com:<some user>/mysamplerepo.git
         - name: GIT_SYNC_BRANCH
           value: master
         - name: GIT_SYNC_SSH
           value: "true"
         - name: GIT_SYNC_PERMISSIONS
           value: "0777"
         - name: GIT_SYNC_DEST
           value: www
         - name: GIT_SYNC_ROOT
           value: /git
         name: git-sync
         image: k8s.gcr.io/git-sync:v3.1.1
         securityContext:
           runAsUser: 65533 # git-sync user
         volumeMounts:
         - name: git-secret
           mountPath: /etc/git-secret
         - name: dir
           mountPath: /git
       - name: phpfpm
         image: php:fpm
         ports:
         - containerPort: 9000
         volumeMounts:
         - name: dir
           mountPath: /usr/share/nginx 

We can now save this yaml and apply it to our cluster:

# kubectl apply -f phpfpm.yaml 
 service/phpfpm unchanged
 deployment.apps/phpfpm configured

Assuming all went well, we should now have our webserver and phpfpm containers up and running:

# kubectl get pod
 NAME                         READY   STATUS    RESTARTS   AGE
 phpfpm-b46969c5f-zzh6d       2/2     Running   0          103s
 webserver-8fb84dc86-7xw4w    2/2     Running   0          10s

That’s just lovely but what next?

Configuring Nginx for PHP

At this point, we basically have two unassociated containers that are living independently in the same cluster. The only common bond is that they have the same set of files synched from the Git Repo. Next, we need to tell nginx to handle PHP requests and where to send them. This will require us to update our Nginx configMap. We do this by adding a location statement to handle php files like so:

      location ~ .php$ {
           try_files $uri =404;
           fastcgi_split_path_info ^(.+.php)(/.+)$;
           fastcgi_pass phpfpm:9000;
           fastcgi_index index.php;
           include fastcgi_params;
           fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
           fastcgi_param PATH_INFO $fastcgi_path_info;
       }

There’s lots going on here in this file but some important items to note. Nginx acts like a reverse proxy when handling PHP files. It simply takes the request and sends to php-fpm. The php-fpm service finds the request file locally, executes PHP on it, and sends the resulting processed output from PHP back to Nginx. Here is the full updated configMap:

apiVersion: v1
 kind: ConfigMap
 metadata:
   name: webserver-config
   labels:
     tier: backend
 data:
   config :
     server {
         listen       80;
         server_name  localhost;
  
         location / {
             root   /usr/share/nginx/www/html;
             index  index.html index.htm;
         }
  
         error_page   500 502 503 504  /50x.html;
         location = /50x.html {
             root   /usr/share/nginx/www/html;
         }
  
       location ~ .php$ {
           root /usr/share/nginx/www/html;
           try_files $uri =404;
           fastcgi_split_path_info ^(.+.php)(/.+)$;
           fastcgi_pass phpfpm:9000;
           fastcgi_index index.php;
           include fastcgi_params;
           fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
           fastcgi_param PATH_INFO $fastcgi_path_info;
       }
  
     }

Let’s apply this to our cluster:

# kubectl apply -f configmap.yaml 
 configmap/webserver-config configured

With the new configuration running, we’ll need Nginx to reload it. There’s a number of different ways we could do this but I’m going to use a hack that will allow us to test the config and then restart. First step, I want to make sure the new config will work for us:

# kubectl exec -it webserver-8fb84dc86-7xw4w -c webserver -- /usr/sbin/nginx -t
 nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
 nginx: configuration file /etc/nginx/nginx.conf test is successful

It looks like the configuration is acceptable so let’s reload Nginx.

# kubectl exec -it webserver-8fb84dc86-7xw4w -c webserver -- /usr/sbin/nginx -s reload
 2019/12/28 14:01:32 [notice] 2804#2804: signal process started

We should now be ready to commit a PHP file to our repo and test.

Testing Our Configuration

Let’s create a simple PHP file in the html directory of our. 

We’ll jump onto the web server, install curl and test:

# kubectl exec -it webserver-8fb84dc86-7xw4w -c webserver -- /bin/bash
 root@webserver-8fb84dc86-7xw4w:/# apt update
 Hit:1 http://deb.debian.org/debian buster InRelease
 Hit:2 http://deb.debian.org/debian buster-updates InRelease
 Hit:3 http://security-cdn.debian.org/debian-security buster/updates InRelease
 Reading package lists... Done
 Building dependency tree       
 Reading state information... Done
 All packages are up to date.
 root@webserver-8fb84dc86-7xw4w:/# apt install curl
 Reading package lists... Done
 Building dependency tree       
 Reading state information... Done
 curl is already the newest version (7.64.0-4).
 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
 root@webserver-8fb84dc86-7xw4w:/# curl localhost/index.php
  
 hello world from php

A great question to ask is how does php-fpm know which file and where that file exists? Like I said, great question.

This is handled by the fastcgi_param SCRIPT_FILENAME entry. This means that Nginx is going to tell php-fpm that it should try to load the $document_root$fastcgi_script_name file for the request. If you look at our configMap, we define document root as /usr/share/nginx/www/html. Assuming a request comes for index.php into Nginx, Nginx will tell php-fpm to also load /usr/share/www/html/index.php. In an environment where Nginx + PHP live on the same host, this doesn’t appear to be a problem because that file will exist for sure. In our configuration, we running two separate hosts aka containers. So we need to make sure the file exists on both servers in the same location. That’s the easy part! It does! Reason being, we’re using gitsynch on both containers and mounting that synched directory to the same location!

Full Working Configs

In case you want to just cheat and load the configurations, feel free to download them and play around: