WordPress in Docker, with NGINX reverse proxy

Despite it isn’t one of the most crucial services to have on a VPS, I chose to cover the setup of WordPress because it was the most complex to set up.

update

Update 2023-12-13

This blog now has its own domain: tavern.edotm.net! The old URL was edotmvps.mooo.com/blog. This is the reason I will explain how to put WordPress into the /blog directory.

The setup

I prefer not to write blog posts on topics extensively covered by others online. After extensive research, it seems this particular WordPress setup is surprisingly undocumented.

What I wanted to achieve can be summarized in three points:

  1. Container isolation: the WordPress instance must reside in its container, ensuring isolation from the rest of the system -- aligning with the core theme of this series.

  2. Proxy and HTTPS integration: accessibility should be restricted to my server’s NGINX-powered reverse proxy, with seamless support for HTTPS and certificate management.

  3. Directory Structure: WordPress should be housed in its dedicated directory on the site, not at the root (/) but in /blog. This organization allows me to manage different HTTPS services in separate directories -- for instance, hosting a GitLab instance in /gitlab.

While achieving the first point is straightforward given its commonality, points 2 and, especially, 3 proved more challenging to find documented solutions for on the web.

1. Run WordPress in its own container

As mentioned earlier, this is the straightforward part. Fortunately, an official WordPress Docker image is available on the Docker Hub, making the setup quite convenient. You can find the image here.

According to the Docker Hub page, the Docker Compose file should look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
version: '3.1'

services:
  wordpress:
    image: wordpress
    restart: always
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME: exampledb
      WORDPRESS_DB_USER: exampleuser
      WORDPRESS_DB_PASSWORD: examplepass
    volumes:
      - wordpress:/var/www/html

  db:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_DATABASE: exampledb
      MYSQL_USER: exampleuser
      MYSQL_PASSWORD: examplepass
      MYSQL_RANDOM_ROOT_PASSWORD: '1'
    volumes:
      - db:/var/lib/mysql

volumes:
  wordpress:
  db:

The highlighted lines can and must be changed, and it is recommended to choose a strong database password. It is crucial that the values of lines 11-13 match 1:1 with those of lines 21-23.

However, be cautious when running this container without adjustments. A known bug causes the MySQL Docker container to consume a considerable amount of RAM (~16 GB). To resolve this, include the following code snippet in the db service section:

  db:
    image: mysql:5.7
    # ...
    ulimits:
      nofile:
        soft: "65536"
        hard: "65536"

This sets a limit of 65,536 open file descriptors, preventing excessive memory usage.

You can customize the port from which the website will be accessed (line 8). For now, it’s advisable to leave it unchanged, as we’ll configure NGINX with its own HTTPS port later.

Additionally, you have the option to decide where to install WordPress components — the web server and the database. You can choose separate named volumes (the default) or sub-directories. For instance:

wordpress/
├── docker-compose.yml
├── www/
└── db/

To do this, under the volumes section of each service, change wordpress to ./www and db to ./db (or other folder names), respectively. You can also mix it. One advantage of doing this is to have easier access on the files used by the container; however, if you don’t plan to edit them, they will just be clutter.

To start WordPress, all you need to do is go into the directory which contains the docker-compose.yml file and run

docker compose up -d

2. Set up NGINX reverse proxy

In this blog post, I won’t delve into the specifics of setting up the NGINX container, configuring it, or establishing SSL certificates. I’ll cover these aspects in upcoming blog posts, and you can easily find comprehensive guides on the internet. Instead, I’ll focus on the crucial steps to make WordPress seamlessly integrate with a reverse proxy.

The challenge in setting up WordPress with a reverse proxy arises from the fact that the WordPress Docker image comes equipped with its own Apache proxy, which will do anything to make your life miserable. It defaults to specific configurations set during installation, leading to unexpected redirects.

But if you are using NGINX, why don’t you use the FPM image that does not have a bundled Apache web server?

The FPM version requires significant setup. NGINX must be configured manually with minimal errors to mitigate security risks. Therefore, the less risky -- and easiest -- way is to utilize the builtin Apache web server and its pre-configured ad-hoc setup, accessed through our favorite reverse proxy, NGINX.

My NGINX configuration file looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
server {
  listen  443 ssl;
  http2  on;
  server_name  edotmvps.mooo.com;
  ssl_certificate  /path/to/fullchain.pem;
  ssl_certificate_key  /path/to/privkey.pem;
  
  location / {
    proxy_redirect  off;
    proxy_set_header  X-Real-IP $remote_addr;
    proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header  X-Forwarded-Proto $scheme;
    proxy_set_header  Host $host;
    
    rewrite ^((?:/[^/\.?]+)*)$ $1/ break;
  
    proxy_pass http://localhost:8080;
  }
}

Lines 9-13 specifically address the challenge of working with two cascading reverse proxies, namely NGINX and WordPress’s Apache.

The key line is Line 12 (highlighted). Remove it and nothing will work. WordPress and Apache are both inherently configured to serve content as HTTP; by setting the X-Forwarded-Proto header, we communicate to WordPress that we’re utilizing HTTPS, ensuring content is served accordingly.

The regex at line 15 is there to make sure every request URL has a trailing /. This will avoid redirects and 404 errors caused by Apache.

Once NGINX is up and running with this configuration (make sure to reload/restart it), all you need to do is visit https://your.web.site/ to install WordPress and configure the administration user. Everything should be working.

I have already installed WordPress from another URL, what do I do?

If you have already installed WordPress from another IP (for example, http://your.web.site:8080), then go to that address and go to the admin area through the bar at the top.

If there is no bar at the top where you can go to the admin area, just append /wp-admin to the URL and log in. Then, from the menu on the left, click on “Settings” and write the address on both “WordPress Address (URL)” and “Site Address (URL)." For example:

https://your.web.site/

Then, scroll down and click on “Save changes”. Now, you should be redirected on the correct address.

I don’t remember my WordPress admin password. What do I do?
report

Caution

If you proceed here, you will lose all the data of the website. I assume that’s fine because you just installed it; otherwise, do not proceed without a backup.

If you used the named volume (the default), just do

docker compose down # to stop running containers
docker volume rm wordpress_db # to remove the db
docker compose up -d # to restart wp and recreate the db

Note that the volume name can vary depending on (1) the name of the folder in which the compose file resides, (2) whether you changed the name of the db volume. In general, the name of the volume created by Docker Compose is foldername_volumename.

Now that we have WordPress running via HTTPS, it’s advisable to restrict access to port 8080 within the local network. To achieve this, simply change Line 8 of the compose file to:

7
8
    ports:
      - 127.0.0.1:8080:80

This modification ensures that port 8080 is only accessible from localhost and not from outside. However, content proxying will continue to function as intended.

3. Change the URL directory of WordPress

The website is currently running smoothly at the URL root (/). But what if you prefer it at a different location, like /blog? After all the previous configurations, this process is really simple and involves just three steps:

3.1 Change the site URL from the admin area

Navigate to the WordPress admin area. On the left menu, go to “Settings” and modify both the “WordPress Address (URL)” and “Site Address (URL)” to your desired address. For instance:

https://edotmvps.mooo.com/blog

Ensure you use HTTPS; this is crucial. Scroll down and click on “Save Changes”. The website might redirect you, possibly resulting in a 404 error. This is normal, as we haven’t configured NGINX to handle this change yet.

3.2 Change the NGINX configuration

Access the NGINX configuration file and modify the location from / to /blog or the directory you’ve chosen.

lightbulb

Tip

Do not alter the proxy_pass directive; it should remain set to / (not /blog).

Also, change the regex at line 15 to the following:

15
rewrite ^/blog((?:/[^/\.?]+)*)$ /blog$1/ break;
warning

Warning

Be really careful with this regex, as a typo could lead to the unavailability of certain parts of the website. If you’ve opted for a location other than /blog, double-check if the modification you’ve made is actually correct.

3.3 Change the compose file

Go to the WordPress directory -- the one that contains the docker-compose.yml file. Shut down the service, by doing

docker compose down

and edit the compose file, modifying line 15 to reflect the URL directory:

14
15
    volumes:
      - wordpress:/var/www/html/blog

Of course, the directory must align with the URL we want (if we want WordPress at /some/dir, the volume should map to /var/www/html/some/dir). Now, just restart WordPress and everything should be working smoothly!

docker compose up -d
Tags: Docker Docker Hosting NGINX Nginx Setup Linux Linux WordPress WordPress