Adding a Web Server to Our Simple WordPress App

If you’d just like to dive into the code for adding a web server to the simple WordPress app from the previous post you can skip ahead to Putting It All Together.

Introduction

This is Part 3 in a series describing a project to create a local WordPress development environment using Docker-Compose. The series is structured as follows:

In Part 3 I’ll add a web server to our simple WordPress app. A web server will allow us to access our services without specifying a port in our URL. In Part 6 we’ll make modifications to the web server will allow us to access our services with meaningful URLs similar to those used when accessing a public website. This was one of the goals I set in Part 1.

We’ll also briefly discuss Docker volumes in this Part. Docker uses volumes as a means to persist data and make it accessible between services. We’ll cover more data storage options in Docker in Part 4.

The Problem with Ports

Well, ports really aren’t a problem by themselves but having to specify them when accessing our services isn’t very appealing and it is counter to the goal of accessing my local development environment in a similar way to my public website. For example, accessing the Adminer database management tool that we added in Part 2 would be more intuitive with

http://<host-name>/adminer

rather than

http://<host-name>:8080

It’s a lot easier to remember the former especially if we have a lot of difference services in our app. Also, you can see we’re getting much closer to the goal of using a simple URL to access our services. This will make transitioning between the development environment and public site easier.

One way to accomplish this is to put our app behind a web server acting as a reverse proxy. A reverse proxy sits between a client and one or more servers, directing requests from clients to the servers and returning the responses to the client. In this way the client only needs to interact with the reverse proxy rather than individual servers as we’ve been doing so far by specifying the port for each service we want to access. With our reverse proxy using default communication ports we eliminate having to specify a port when accessing our services. The reverse proxy will handle all of the details of communicating with the servers behind it, all invisible to us, at least once it’s up and running.

We’ll be using nginx as our reverse proxy. It has a Docker Official Image and is used by the password manager that we’ll be incorporating later in this series, so getting to know something about it ahead of time is a plus. The documentation for nginx is fairly extensive but I found that some configuration options weren’t fully explained or sometimes conveniently organized. Luckily, it’s pretty easy to find solutions to many configuration problems on the web.

Adding Nginx to Our Simple WordPress App

We’ll continue with the barebones spirit of our simple WordPress app and use a minimal specification for our Compose file nginx service, let’s call it nginx. It’s only 6 lines long and since we’ll be communicating with the nginx service rather than individually with the WordPress and database services, we can eliminate 4 lines of code from those services, for a net addition of just 2 lines of code.

A simple Compose nginx service

  nginx:
    image: nginx
    ports:
      - "80:80"
    volumes:
      - ./nginx:/etc/nginx/conf.d

The Compose code above does the following:

  • image: specifies the “latest” nginx image as our web server. As mentioned previously, using the latest image is fine for our simple app, but specifying a specific image is likely a better way to go for a more serious app.
  • ports: here we specify that we’ll access the nginx service over port 80. Since this is the default communications port for the http protocol, we won’t have to specify a port when accessing the nginx service. Note that by default nginx exposes port 80 for communications among other Compose services. That represents the second 80 in the host:container pair format above.
  • volumes: specifies a path to mount in host:container format. Here the host path, ./nginx (relative to the Compose file), is mounted and bound to the container directory /etc/nginx/conf.d. This is the directory where nginx will look for a configuration file where we’ll specify how it will communicate with our other services and the client. Any changes we make in the ./nginx directory will be reflected and available to the nginx service in its /etc/nginx/conf.d directory. Note that this directory is not accessible outside the container. We’ll discuss in Part 4 how to access information within containers. Volumes can also be named. We’ll explore these more in Part 4 as well.

The Nginx Configuration File

We specify how communications between the client and our Docker services pass through our nginx web server with a configuration file, which is named by default, nginx.conf. As specified in our Compose configuration above we’ll create this file in the ./nginx directory. Nginx configuration files can be quite complex, but we’ll be keeping things simple for now. Below is the simple nginx configuration file we’ll use for our WordPress app for now.

A simple nginx configuration file

server {
  proxy_set_header Host $host;

  location / {
    proxy_pass http://wp;
  }

  location /adminer {
    proxy_pass http://adminer:8080;
  }
}

Our configuration file has the following components:

  • server: sets out the block for the configuration of a virtual server. By default, our server will be listening on port 80. We’ll discuss this more in Part 6 – Securing network communications with self-signed certificates. Our server configuration has a single simple directive, proxy_set_header, and two location block directives.
  • proxy_set_header Host: by default, nginx modifies the request header passed to the proxied server to reflect the corresponding location block and proxy_pass parameters. This directive however overrides this behavior and substitutes the $host variable instead. For our simple proxy_pass directives the result is as if the request came from the host itself rather than passing through the proxy server. Without this directive our WordPress directed requests would seem to be coming from /wp and would fail, at least in part. Interestingly, Adminer directed requests would succeed because they would be seen as coming from /adminer, which is expected.
  • location / or location /adminer: nginx uses location blocks to determine how to handle requests from the client. In our simple server configuration, we have two location blocks. The location / block acts as a default and will be utilized if no other location blocks matching the requested URI are found. So, for our server above, any requests to http://<host>/adminer will be handled by the location /adminer block and passed via its proxy_pass directive to the adminer service. All other requests will be handled by the location / block and passed to the WordPress service. Pretty neat, right?
  • proxy_pass: sets the protocol and address of the proxied service. Both of our services are proxied using the http protocol. We’ll add configuration details for https in Parts 6 and 7b. We’ve also used the name of the service for the requested URI to be sent to. Docker will replace the service name, wp or adminer in our case, with the appropriate IP address of the service. By default, nginx will pass the http request on port 80. Because the WordPress service exposes that port for internal communications, we don’t have to specify it here. Adminer, however, exposes port 8080 for internal communications, so we have to specify a port to pass the request to for that service.

More information about nginx configurations can be found in the Beginner’s Guide. Also, useful might be How nginx processes a request which explains how nginx selects between different server and location blocks.

Putting it All Together

We added 6 lines to our Compose file but lost 4 lines from the database and WordPress services combined. Our Compose file now looks like the following. I’ve created mine in a folder named part-3.

docker-compose.yml file for a simple WordPress app with database management tool and web server

version: '3.7'

services:
  db:
    image: mariadb
    environment:
      MYSQL_ROOT_PASSWORD: simplewordpress
      MYSQL_DATABASE: wordpress

  wp:
    image: wordpress 
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: root
      WORDPRESS_DB_PASSWORD: simplewordpress

  adminer:
    image: adminer

  nginx:
    image: nginx
    ports:
      - "80:80"
    volumes:
      - ./nginx:/etc/nginx/conf.d

Comparing this to the Compose file from Part 2 you can see we’ve added the nginx service and deleted the ports keys from the WordPress and Adminer services. We can do this because both services expose a port by default for communications between Docker containers. We’ll discuss this in a bit after we’ve start up our app.

We’ve also added the nginx configuration file discussed above to our /part-3/nginx directory. I’ve named it nginx.conf, however, nginx will utilize all files ending in .conf so be careful when creating other files in this directory.

/part-3/nginx/nginx.conf

server {
  proxy_set_header Host $host;

  location / {
    proxy_pass http://wp;
  }

  location /adminer {
    proxy_pass http://adminer:8080;
  }
}

You can start up your app with the docker-compose up -d command. Take a look at our running containers with the docker ps command which will show something like listing the below.

CONTAINER ID     IMAGE         COMMAND                  CREATED             STATUS           PORTS                NAMES
d698bda06915     nginx         "nginx -g 'daemon of…"   3 minutes ago       Up 3 minutes     0.0.0.0:80->80/tcp   part-3_nginx_1
f47407810f36     adminer       "entrypoint.sh docke…"   3 minutes ago       Up 3 minutes     8080/tcp             part-3_adminer_1
939036252c5a     wordpress     "docker-entrypoint.s…"   3 minutes ago       Up 3 minutes     80/tcp               part-3_wp_1
9761451dfeca     mariadb       "docker-entrypoint.s…"   3 minutes ago       Up 3 minutes     3306/tcp             part-3_db_1

You should see that containers for our four services are up and running. If you seem to be missing one, you can use the docker ps -a command to show all containers including ones that aren’t currently running. You can use the docker logs <container_name> command to review the logs of any container. If one of your containers didn’t start its logs might indicate the cause. We’ll be discussing these commands more in future posts.

You can now access your WordPress app from a computer on your local network with the following commands.

http://<host-ip>

or

http://<host-name>

You can access the Adminer service from a computer on your local network with the following commands.

http://<host-ip>/adminer

or

http://<host-name>/adminer

From the computer hosting the app you can access the WordPress and Adminer services as follows

http://localhost

for WordPress or

http://localhost/adminer

for Adminer. As you can see, we no longer have to specify the port for either service.

Wrapping Up

The addition of a web server to our simple app has made access to our WordPress and Adminer services more intuitive by eliminating the need to specify the port were communicating over. In Part 6 – Securing network communications with self-signed certificates we’ll take this a step further allowing us to access each service with a custom domain name. Next up though in Part 4a – Where’s My Data! Exploring Anonymous Volumes in Docker, we’ll begin our dive into how Docker handles data storage. This is just the first of four posts in Part 4 looking at data storage in Docker. In Part 4b we’ll explore bind mounts, in Part 4c we’ll explore named volumes, and in Part 4d we’ll work with data storage in our WordPress app.