How to deploy a WordPress website as a docker compose stack with NGINX reverse proxy


To configure and run a WordPress website using Docker containers and NGINX reverse proxy, on an AWS EC2 instance.


1a. Start an EC2 instance with at least 2 GB RAM.

The instance type I used was t3.small (2 vCPU, 2 GB RAM)

For a production server, stay away from t2 and t3 instances as they will exhibit low performance (burstable CPU). Go for m5 or c5 instances.

Always use the latest generation of instances and get the best performance. For example, when there are c4 and c5 instance types, go for c5.

If you are confused on which instance type to use, go to ec2instances.info and select your region. It will show you a list of instance types, their specs and price per hour.

1b. Add a data volume of 50 GB size while creating the instance.

This will serve as the data directory for Docker and will persist even after the instance is terminated.

2a. Login to the instance using OpenSSH / PuTTY.

2b. Update the OS packages

sudo apt update
sudo apt upgrade

2c. Format and mount the data volume as /data.

I need not elaborate on that, right?

sudo lsblk
sudo mkfs.ext4 /dev/vdb
sudo blkid /dev/vdb
sudo nano /etc/fstab
UUID /data defaults 0 0
sudo mount -a

3a. Install Docker and docker-compose

Docker is used to run applications in isolated namespaces called containers. It is similar to VM’s, but all containers have a shared Linux kernel. Containers are isolated from each other and from the Host OS.

This eliminates scenarios like, “My code runs fine on the test server, but not on production!”. With Docker, the deployment is guaranteed to work on any PC or server that can run containers; let it be your dev PC, staging server or production server. Advantages are ease of deployment, security, scalability, clean host OS etc.

docker-compose is used to deploy a stack of containers from a compose script. This makes it easier to manage apps as stacks of containers.

Go to get.docker.com and get the install script from there. Also add your user to the ‘docker’ group.

curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker username

For docker-compose,
Go to https://docs.docker.com/compose/install/#install-compose

sudo curl -L "https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version

3b. Change the data directory of Docker to the data volume mounted on /data

sudo mkdir /data/docker
sudo nano /etc/docker/daemon.json
	"data-root": "/data/docker"
sudo systemctl restart docker
docker info

4a. Create the docker-compose.yml script in a new folder.

sudo mkdir wp && cd wp
sudo nano docker-compose.yml
version: '3.3'

    image: mysql:8
    command: --default-authentication-plugin=mysql_native_password
      - db_data:/var/lib/mysql
    restart: unless-stopped
      MYSQL_DATABASE: wpondocker
      MYSQL_USER: wpuser
      MYSQL_PASSWORD: gC8mN5jX7cU5oL2z

      - db
    image: wordpress:latest
      - "8000:80"
      - www_data:/var/www/html
    restart: unless-stopped
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wpuser
      WORDPRESS_DB_NAME: wpondocker

      - db
    image: adminer
    restart: unless-stopped
      - ""

  db_data: {}
  www_data: {}

Here’s a short explanation of this file:

docker-compose scripts have 3 sections:

1. services

2. volumes

3. networks

Services are the containers that will be run as a stack
Volumes are persistent data storage locations
Networks are not used here, but you can specify what networks are to be created and what containers to run on each network.

There are 3 services:

db – run the MySQL database container, as WordPress requires a DB.

It uses the Docker image = mysql:5.7
DB data is stored in a named volume called db_data. This will ensure that the DB data is not deleted when the container is terminated.
Restart policy is to restart the container on boot unless it is manually stopped.
Environment variables are used to assign a root password and create a database automatically for WordPress.

wp – run the WordPress container. It listens on port 8000.

It starts after the db container.
It uses the Docker image = wordpress:latest
Port mapping is done to map port 8000 on the Host to port 80 of the container.
It uses a named volume called www_data to store files.
Environment variables are used to connect to the DB.

adminer – this is a DB management interface used to edit, import and export the DB. It listens on localhost at port 10000

Port mapping is done to map the localhost port 10000 to port 8080 of the container. It listens on localhost only and cannot be accessed over the Internet for security reasons. SSH Local port forwarding needs to be done to access the adminer interface on your PC.
An environment variable is used to set the default DB server to db, the hostname of the db container.

4b. Start the docker-compose stack

docker-compose up -d

This will pull the images from Docker Hub, start and configure the containers so that you get a working WordPress site. But this site is only accessible on port 8000. We need to configure NGINX in reverse proxy mode: NGINX receives the incoming client cconnections, adds custom header and passes it to the WordPress container on port 8000.

5a. Install NGINX

sudo apt install nginx

5b. Configure NGINX to serve as a reverse proxy by adding a config file for each site in sites-available. My config file is given below with the full explanation.

cd /etc/nginx/sites-available
sudo nano wpdocker.conf
server {

listen 80;
server_name wpdocker.mvcloud.xyz;
index index.php;
access_log /var/log/nginx/wpdocker-access.log;
error_log /var/log/nginx/wpdocker-error.log;

location / {
proxy_http_version 1.1;
proxy_cache_bypass $http_upgrade;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
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 X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;

# Proxy timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;


A short Explanation

server {
listen 80;
- listens on port 80 for HTTP connections

server_name wpdocker.mvcloud.xyz;
- URL of the website index

- Load index.php first

access_log /var/log/nginx/wpdocker-access.log;
- Access Log logs incoming requests

error_log /var/log/nginx/wpdocker-error.log;
- Error Log

location / {
- For all requests, forward to localhost port 8000

proxy_http_version 1.1;
- HTTP version for communicating with the WordPress container

proxy_cache_bypass $http_upgrade;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
- These three statements are used to turn a connection between a client and server from HTTP/1.1 into WebSocket.

proxy_set_header Host $host;
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 X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
- Set custom headers for the backend servers to identify the client properly.

# Proxy timeouts
proxy_connect_timeout 60s;
- Defines a timeout for establishing a connection with a proxied server. It should be noted that this timeout cannot usually exceed 75 seconds.

proxy_send_timeout 60s;
- Sets a timeout for transmitting a request to the proxied server. The timeout is set only between two successive write operations, not for the transmission of the whole request. If the proxied server does not receive anything within this time, the connection is closed.

proxy_read_timeout 60s;
- Defines a timeout for reading a response from the proxied server. The timeout is set only between two successive read operations, not for the transmission of the whole response. If the proxied server does not transmit anything within this time, the connection is closed.

This is getting tough, right? Please let me know if you have any doubts – in the comments section below!

5c. Create a symlink in sites-enabled for your NGINX config

sudo ln -s /etc/nginx/sites-available/wpdocker.conf /etc/nginx/sites-enabled/wpdocker

5d. Test the configuration and reload NGINX

sudo nginx -t
sudo systemctl reload nginx

Now you should be able to see the HTTP version of your WordPress installer in your browser.

6a. Enable HTTPS and HTTP/2, using a free SSL certificate by Let’s Encrypt Certbot.

Let’s Encrypt is a free service that issues SSL certificates that are valid for 90 days, after checking whether your domain resolves to your server. So, before doing this step, your DNS settings must be correct, else validation wil fail.

Goto https://certbot.eff.org/instructions

sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
sudo certbot --nginx
Enter email address (used for urgent renewal and security notices)
(Enter 'c' to cancel): xxxxxxx@gmail.com
Which names would you like to activate HTTPS for?

1: wpdocker.mvcloud.xyz

Select the appropriate numbers separated by commas and/or spaces, or leave input
blank to select all options shown (Enter 'c' to cancel): 1
Requesting a certificate for wpdocker.mvcloud.xyz
Performing the following challenges:
http-01 challenge for wpdocker.mvcloud.xyz
Waiting for verification…
Cleaning up challenges
Deploying Certificate to VirtualHost /etc/nginx/sites-enabled/wpondocker
Redirecting all traffic on port 80 to ssl in /etc/nginx/sites-enabled/wpondocker

Congratulations! You have successfully enabled https://wpdocker.mvcloud.xyz

Subscribe to the EFF mailing list (email: ).
Congratulations! Your certificate and chain have been saved at:
Your key file has been saved at:
Your cert will expire on 2021-03-05. To obtain a new or tweaked
version of this certificate in the future, simply run certbot again
with the "certonly" option. To non-interactively renew all of
your certificates, run "certbot renew"

Certbot has tweaked your NGINX config file and

  • installed a free SSL certificate
  • redirected HTTP to HTTPS

Here are the directives added by Certbot to your NGINX config file:

listen 443 ssl http2;
- You need to add "http2" to the end to make connections use HTTP/2, which improves performance
ssl_certificate /etc/letsencrypt/live/wpdocker.mvcloud.xyz/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/wpdocker.mvcloud.xyz/privkey.pem; # managed by Certbot
- SSL certificate and private key, added by Certbot

Certbot added a new server block and shifted listen 80; to that block:

server {
if ($host = wpdocker.mvcloud.xyz) {
return 301 https://$host$request_uri;
} # managed by Certbot
- Redirect HTTP to HTTPS

listen 80;
server_name wpdocker.mvcloud.xyz;
return 404; # managed by Certbot

6b. Test configuration and reload NGINX

sudo nginx -t
sudo systemctl reload nginx

Now open your website in the browser and complete the WordPress installation.
Your WordPress website is now ready! It is running in a Docker container with NGINX reverse proxy at the frontend.

NGINX accepts incoming client requests on ports 80 and 443 and forwards them to your WordPress container at port 8000.

6c. Add a scheduled task for SSL certificate renewal

Do a dry run of the certificate renewal for testing purposes

certbot renew --dry-run
sudo crontab -e
@daily certbot renew

This completes our WordPress deployment. I encourage you to try this out on an AWS EC2 instance or any other cloud instance. You can also try this out on other cloud providers or even Virtualbox if you know how to edit your /etc/hosts file to resolve the website URL. DigitalOcean has cheap instances. Google Cloud has a $300 trial for 1 year; you may use that for your test deployments…

I will write about Traefik v2 reverse proxy in an upcoming article! Traefik is more advanced and well suited for Docker environments. Till then, have a good time!

2 thoughts on “How to deploy a WordPress website as a docker compose stack with NGINX reverse proxy”

Leave a Reply