Docker nginx PHP Tutorial


In the very first video in this series we saw a way of using nginx with Docker Compose to bring together a Symfony, nginx, and MySQL stack.

If you recall, we had this in our configuration:

# docker-compose.yml

version: '2'

services:

    # other stuff removed for clarity

    nginx:
        image: codereviewvideos/nginx.dev
        ports:
          - "81:80"
        restart: always
        volumes:
          - "./volumes/nginx/logs:/var/log/nginx/"
        volumes_from:
            - php
        networks:
          crv_dev_network:
            aliases:
              - nginx

A little later on, we started using nginx as our base image when learning about Docker images vs Docker containers. There we saw that we could very quickly bring up a working nginx web server, in much the same way as we saw how easy it is to bring up a working MySQL server in the previous video.

Taking a look back at the nginx config from the docker-compose.yml file above, at this stage the vast majority of this should now be much more clear than it was just a few short videos ago.

We haven't got on to Docker Compose just yet, so don't worry if the specifics of the config aren't clear at this point. In particular, we haven't covered networks or Docker Networking at all.

I mentioned back in the first video how we would start with Docker Compose version 2 syntax, as it simply makes the whole volumes concept much easier to get started with, in my opinion.

Since then, we have learned the preferred way to work with Docker Volumes, and when we get to covering Docker Compose we will upgrade to version: '3' syntax.

Docker nginx PHP

Our goal is to build a Dockerised version of nginx that allows us to run our PHP code. In our case this will be a Symfony 3 code base, but the general principles are the same no matter whether running Laravel, or Zend, or Yii, or whatever else.

To do this we will need to split our Docker nginx and PHP images apart.

This seems a little confusing, but keep with me. In many ways this will make your life that much easier, particularly if you want to use your Docker nginx setup with other stacks - maybe Node JS, or Python, or even PHP with WordPress.

Notice in the original docker-compose configuration above, I didn't use nginx:stable for my image. Instead, I used a custom variant called codereviewvideos/nginx.dev.

Let's look at the Dockerfile for this image:

FROM nginx:stable

EXPOSE 80
EXPOSE 443

COPY ./conf.d/upstream.conf /etc/nginx/conf.d/
COPY ./conf.d/symfony.conf /etc/nginx/conf.d/

You can find the full source code for the codereviewvideos/nginx.dev Docker image on GitHub.

What's happening here then?

Well, we base our image from the official nginx:stable image.

This means whenever we run a docker build command against this Dockerfile (more on which shortly), the resulting image will be based on whatever version of nginx was considered stable at the time the docker build command is run. If this is a problem for you, make sure to use a more specific image tag, such as nginx:1.12.1. You can find the latest tags via the Docker Hub nginx page.

We EXPOSE 80 and EXPOSE 443.

This is actually unnecessary, as nginx itself already exposes both of these ports. For clarity, port 80 is the standard port for http, and port 443 is the standard port for https.

I expose both because I like the explicitness, and also I struggle to see where port 443 is exposed in the official Docker nginx image.

The next two lines are the meat of this Dockerfile. Let's explore each in further depth.

COPY important stuff please

The important instruction:

COPY ./conf.d/upstream.conf /etc/nginx/conf.d/

You can read more about the COPY instruction here.

Essentially it does as you likely expect: it copies a file, or folder, from a source path to a destination path.

The source path is something from your local machine (or a locally accessible path).

The destination is the location the file or folder will be available at inside the resulting Docker image.

In this case we are copying from the local directory, in a subdirectory called conf.d/, and a specific file called upstream.conf:

ls -la

total 32
drwxrwxr-x  4 chris chris 4096 Aug 26 11:11 .
drwxrwxr-x 80 chris chris 4096 Sep 19 12:05 ..
drwxrwxr-x  2 chris chris 4096 Aug 21 15:52 conf.d
-rwxrwxr-x  1 chris chris  183 Aug 21 15:52 Dockerfile
-rwxrwxr-x  1 chris chris   13 Apr  3 19:42 .dockerignore
drwxrwxr-x  8 chris chris 4096 Sep 23 11:55 .git
-rw-rw-r--  1 chris chris  208 Aug 19 13:44 Makefile
-rw-rw-r--  1 chris chris    3 Aug 19 13:48 Readme.md

conf.d is a sub-directory containing the files upstream.conf, and symfony.conf.

This is config in a format that nginx can work with.

ls -la conf.d

total 20
drwxrwxr-x 2 chris chris 4096 Aug 21 15:52 .
drwxrwxr-x 4 chris chris 4096 Aug 26 11:11 ..
-rwxrwxr-x 1 chris chris 2299 Aug 19 13:43 symfony.conf
-rwxrwxr-x 1 chris chris   44 Mar 23  2017 upstream.conf

When the docker build command is run, the COPY command will copy this file into the resulting Docker image. When we run the image (docker run {image name}), we should be able to cat /etc/nginx/conf.d/upstream.conf.

Ok, so what is inside upstream.conf?

upstream php-upstream {
  server php:9000;
}

This gets a little advanced.

In nginx an upstream directive allows us to define one or more servers to which we will proxy requests.

Huh?

Well, remember a little earlier when I said we would keep our nginx and PHP images apart? The reasoning for this is that nginx really shouldn't have to know anything about PHP. After all, nginx (a web server) can handle web requests for PHP applications, or NodeJS applications, or Elixir applications, or anything else.

Why should we tie it to PHP?

We shouldn't :)

However, we do need a way to process requests that use PHP (e.g. for our Symfony sites).

As such we will need to create a completely separate Docker PHP image (which we will do next), and then proxy / forward any PHP requests from our running nginx web server container, to a running PHP container where they will be actually processed.

Yes, it is more complex.

However, this is typically how I would have used nginx on a physical or virtual box anyway, only I would have installed PHP and nginx on the same box. Now things are separated, but conceptually they are the same.

To break this down then:

upstream php-upstream {

upstream is the directive.

It allows us to proxy / forward requests on to one or more servers defined in this group.

The group contents are anything inside the brackets {}.

php-upstream is the name of this upstream group. You can name this anything you like.

Our php-upstream group only has one entry:

server php:9000;

All entries must start with server.

There are other options you can pass in here if needed. We don't need anything else.

Our only server entry is php:9000.

This means we will have another server running somewhere that has the name of php. You could use an ip address here, but with Docker that is extra work. A DNS name is a better fit for our needs.

By default the base image we will use for our PHP image will have a pre-configured port of 9000 for PHP-FPM. We will cover this in the PHP docker image tutorial.

nginx Docker Symfony Config

The next important instruction is:

COPY ./conf.d/symfony.conf /etc/nginx/conf.d/

Again, from a high level this simply copies over the symfony.conf file into the required place inside the resulting Docker image file.

The contents of this file are almost a like-for-like copy of the nginx configuration from the official Symfony docs:

server {
    listen 80 default;
    server_name dev;
    root /var/www/dev/web;

    location / {
        try_files $uri /app.php$is_args$args;
    }

    # DEV
    location ~ ^/(app_.*|config)\.php(/|$) {
        # THIS LINE IS IMPORTANT
        fastcgi_pass php-upstream;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
    }

    # PROD
    location ~ ^/app\.php(/|$) {
        # THIS LINE IS IMPORTANT
        fastcgi_pass php-upstream;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
        internal;
    }

    location ~ \.php$ {
      return 404;
    }

    error_log /var/log/nginx/symfony_error.log;
    access_log /var/log/nginx/symfony_access.log;
}

There are some important things to note.

Firstly, our web server name is dev. We should expect to access dev from our browser once the nginx server is up and running. You can change this to be more meaningful to your own project, of course.

The site root is /var/www/dev/web.

As such our Symfony setup will need to follow this directory structure.

The key line to make this work is the inclusion of:

fastcgi_pass php-upstream;

This is included in both the dev and prod location blocks.

Without this things will not work.

The rest of this config is better documented in the Symfony docs.

One super important point is that we are including the development site configuration here.

What this means is that you will have one nginx configuration for development, and a separate nginx configuration for production.

Yes, this unfortunately means two repos to maintain. However, it also means there is a lesser chance of mistakes such as making your app_dev.php available in production. A simple Google for inurl:app_dev.php will show you how many people make this mistake.

Where Are My Files?

We want to run a Symfony application here, right?

After all, we just added a whole bunch of configuration for Symfony.

So, where are the expected files? Where are the controllers, the entities, the app_dev.php and parameters.yml that we all know and love?

Well, they are not part of this image.

Our configuration here defines all the setup to handle a request for those files, but as we have seen, it hands off / proxies / forwards to (however you want to think about it) to a separate PHP container entirely.

We haven't created that PHP container just yet. We are about to, in the very next video.

I admit that separating these concerns does seem a little overkill. However, the benefits of this approach will become apparent as we continue on through this Docker tutorial.

Code For This Course

Get the code for this course.

Episodes