WordPress deploy with Docker

How to Deploy WordPress with Docker Containers on Ubuntu 20.04

Introduction

WordPress is one of the most popular Content Management Systems (CMSs) out there. Statistically, it powers over 39% of all websites you see around the world wide web. It’s a popular choice because of its extensibility through plugins and its flexible templating system. It allows you to change its appearance in seconds. Moreover, its administration can be done through the web interface without requiring a lot of technical know-how.

In addition, WordPress is free and open-source and is built on a MySQL database with PHP processing. You can deploy WordPress on a LAMP stack (Linux, Apache, MySQL, and PHP) or a LEMP stack (Linux, Nginx, MySQL, and PHP). However, it proves to be time-consuming to set up the stack every time you want to deploy.

Luckily, modern software delivery methods such as cloud computing, Docker, and Docker Compose have smoothened the overall developer experience. These tools simplify the process of setting any stack by avoiding the overhead of installing and configuring individual components every time you want to deploy an application. Instead, you write configuration files that will be used to pull and create images and run them in Docker containers, allowing you to deploy your application with just one command.

Containers are lightweight, virtualized, portable, software-defined standardized environments that allow the software to run in isolation from other software running on the physical host machine. Docker Compose allows you to manage multiple containers and ensure they communicate. For example, an application source code and a database must communicate.

In this tutorial, we will be building a multiple containerized WordPress application. A complete WordPress app requires three containers: MySQL database, Nginx server, and WordPress source code. Security being a priority in modern websites, we will obtain an SSL certificate from Let’s Encrypt to secure your installation. Then, we will set up a cron job to periodically check and renew certificates so that your website’s security is maintained continuously.

Prerequisites

Step 1: Define the Configurations for the Web Server

The web server holds your website files and lets users access your web application. Thus, it’s only appropriate that in the first step we define the configuration for the webserver. We will be defining an Nginx server configuration file which will include WordPress-specific location blocks. We will also include location blocks to direct Let’s Encrypt verification requests to the Certbot client for automated renewals of the certificate.

Let’s start by making a directory for the project. You can choose a directory name you prefer. We will use wordpress_docker for this tutorial. Enter the following command to create the directory and navigate into it:

Next, create a directory to hold the Nginx configuration files with the command:

Use nano to open the file with the following command:

In this file, we will define basic directives for an Nginx server block configuration. These include directives for the server name, document root, and location blocks to direct Certbot plugin requests for certificates, static files, and PHP processing. You may read our tutorial on How to Secure Nginx with Let’s Encrypt to learn more. Add the following code to the file, replacing example.com with your registered domain name:

Let’s define the sections you have added:

  • Directives:
    • listen: it tells Nginx to listen on port 80. This allows the use of Certbot’s webroot plugin to make certificate requests. Once we have obtained an SSL certificate, we will update this configuration to use port 443.
    • server_name: this defines the domain name for which this configuration should handle. The traffic to the domain name defined here will be directed to this particular server block, and hence to the document root.
    • root: it defines the root directory for requests to the domain name above. It’s usually the directory holding our actual website files. We have set the directory to this /var/www/html. It will be created as a Docker mount point during the container building time. We will define the instructions for this process inside the WordPress Dockerfile.
    • index: this defines files that will be used as indexes or the entry point to your web server when processing requests. We have moved index.php before index.html so that Nginx prioritizes index.php.
  • Location Blocks:
    • location ~ /.well-known/acme-challenge: handles requests to the well-known directory where Certbot adds a temporary file to validate that the DNS for the specified domain directs to the particular server we are requesting SSL certificates from. This is why you should add a valid domain for this step to work instead of the example.com we are using in this tutorial.
    • location /: picks URI requests and gives control to WordPress index.php to request arguments for processing.
    • location ~ \.php$: handles PHP processing and passes the request to the WordPress container (we will define a configuration file for this in a later step). We have defined configurations specific to FastCGI protocol here because the WordPress Docker image will be based on the php:fpm image. Nginx uses an independent PHP processor for PHP-specific requests. We will use the php-fpm processor that comes with the php:fpm Docker image.
    • location ~ /\.ht: handles .htaccess files that Nginx does not use. The deny all directive ensures these files are never served to website visitors.
    • location = /favicon.ico, location = /robots.txt: as seen in the definition, this prevents logging of requests to /favicon.ico and /robots.txt files.
    • location ~* \.(css|gif|ico|jpeg|jpg|js|png)$: turns off logging of requests to static files and ensures they are cached to lessen the load on the server.

You may now save and close the file by pressing CTRL+X, Y, then ENTER. That completes the first step.

Step 2: Define Environment Variables

Environment variables are necessary to facilitate communication between the WordPress application and the Database. They also ensure the application data is persisted. The environment variables include sensitive information such as database credentials and non-sensitive information such as database name and host.

For security purposes, it’s always a good idea not to add sensitive information to project repositories. Hence, instead of setting the sensitive values in the Docker Compose file, we will be defining the MySQL credentials inside the .env files that will not be committed to the project repository and risk public exposure. Inside the project root ~/wordpress_docker open the .env file:

Add the following MySQL credentials to the file, updating it with a strong password of your choice:
We first defined a password for the MySQL root administrative account, and credentials specific to our WordPress app. Once done, save and close the file.

The next thing you must do is to add the .env file to .gitignore and .dockerignore files to ensure that it does not get added to your repositories or Docker images respectively.

This is not necessary for this tutorial, but if you want to work with Git for version control, enter the following command to initialize the current directory as a git repository:

Open the .gitignore with nano:

Add the following line:

Save and close the file. Next, open the .dockerignore with nano:

Add the following line:

While at it, you may optionally add other files and directories associated with your application’s development:

Save and close the file when done. That is all for this step. Let’s move on to the Docker Compose definition.

Step 3: Configure Services with Docker Compose

Docker Compose uses a docker-compose.yml file to build images. This file contains service definitions for the complete setup of an application. Service definitions are basically instructions for how a container will run. A service is an actual running container.

Docker Compose makes it possible to define different services for multi-container applications by linking the various services together with shared networks and volumes. You will see this in action as we will be defining three containers for our application: web server, WordPress installation, and database. We will add a fourth container to run the Certbot client for certificate renewals.

Enter the following command to create the docker-compose.yml file:

The first line in a docker-compose.yml file is the version definition line. We have set 3 for ours. Then, you can start defining your services. Add the following code snippet in the file to define the db service:

Let’s discuss what we have in the db service definitions below:

  • image: determines the image the container will be based on. It’s always better to specify a specific version (mysql:8.0) than using the latest tag (mysql:latest) as future versions of MySQL images may conflict with our application if we happen to rebuild this image. You may find more information about Dockerfiles Best practices on the official Dockerfile Docs.
  • container_name: we specify the container name here.
  • restart: this directive determines the restart behavior of the container. The default is no but we have set it to always restart unless it is manually stopped.
  • env_file: this directive is used to specify the location of the file with the environment variables (.env) used by our application.
  • environment: used to specify additional environment variables. In this tutorial, we have specified the MYSQL_DATABASE variable to hold the database name for our application. The database name can be included in the docker-compose.yml.
  • volumes: used for specifying mount locations. In our example, we have mounted a named volume called dbdata to the /var/lib/mysql directory on the container, which is usually the standard data directory for MySQL.
  • command: this directive specifies a command that will override the default CMD instruction for the image. We have added an option to the Docker image’s standard mysqld command that starts the MySQL server inside the container. The option we have added is --default-authentication-plugin=mysql_native_password, which updates the default authentication plugin for MySQL to use password authentication (mysql_native_password). This is necessary for your PHP (WordPress application to work) because they use a username and password to access the database. In newer MySQL versions, the default authentication plugin has changed. However, most applications use password authentication. Thus, you must change this setting for the application to work.
  • networks: this directive is used to specify that the db service should join the app-network, which we will define as we go along with the tutorial.

Next, let’s define the service configuration for our WordPress application. We will call the service and container_name app. Add the following code snippet below the db service definition, keeping in mind proper indentation:

Just like we did with the db service, we have named our container and defined the restart policy. Some more options we added are defined below:

  • depends_on: this directive ensures that containers are started in order of dependency. In our case, the app container depends on the db container. Hence, it will start after the db container has started. This needs to happen in this order because the WordPress app depends on the availability of a MySQL database for it to function.
  • image: as seen in the code snippet, we will be using WordPress version 5.1.1 fpm alpine image. We had explained about the php-fpm processor that Nginx requires for PHP processing. This image takes care of that. The alpine image based on the Alpine Linux project helps keep the image size smaller. If you need more information about image variations, you may follow this link for Docker Hub Wordpress images.
  • env_file: specifies the location of the .env file that contains the database credentials.
  • environment: this directive defines additional environment variables. For our case, we are defining the variables that WordPress expects and assigning them with the values of the variables from our .env file. These are WORDPRESS_DB_USER, WORDPRESS_DB_PASSWORD, and WORDPRESS_DB_HOST which refers to the MySQL server running on the db container, accessible from MySQL’s default port 3306. Finally, you see the WORDPRESS_DB_NAME which we have set to WordPress. The same value is specified in the MySQL service definition in the db container: MYSQL_DATABASE=wordpress.
  • volumes: this directive mounts a volume called app to the /var/www/html mount point, created by the WordPress image. The naming of volumes allows sharing of application code with other containers.
  • networks: finally, we add the app container to the app-network to ensure it communicates with other containers on the network.

That will be all for the app service container for the WordPress image. Let’s now define the webserver service for the Nginx image. First, add the following code snippet below the app service definition in your docker-compose.yml file:

We have already explained the depends_on option. In the case of this webserver service, the container will start after the app container has started. The web server container is based on the alpine Nginx image. It has a similar restart policy as the previous service definitions. The other options in the webserver service definition include:

  • ports: binds the ports between the host machine and the container. In Step 1, we had defined port 80 in the nginx.conf file. This port is mapped to port 80 on the container.
  • volumes: we have a combination of bind mounts and named volumes under this option:
    • app:/var/www/html: this volume definition mounts the WordPress application to the /var/www/html directory that earlier on, we had set as root in the Nginx server block.
    • ./nginx-conf:/etc/nginx/conf.d: this definition bind mounts the Nginx configuration directory on the host machine to the Nginx configuration directory we defined for the container. Hence, any changes on the host machine are automatically reflected in the container.
    • certbot-etc:/etc/letsencrypt: this definition mounts the Let’s Encrypt certificates and keys for the domain to the appropriate directory on the container.
  • networks: like in the previous service definitions, the networks directive adds the webserver service to the app-networks.

Since we are done with the webserver definition, let’s add instructions for the Certbot service. This will handle getting your TLS/SSL certificates from Let’s Encrypt. If you would like to know more about securing an Nginx server, this tutorial on how to secure Nginx with Let’s Encrypt is a good source.

Next, add the following code snippet below the webserver service. Remember to set your correct domain name and email address:

The certbot image will only start after the webserver has started, because of the depends_on directive. Docker Compose will pull the Certbot image from Docker Hub as defined.

Under the volumes definition, the Certbot container will share the domain certificates and key in certbot-etc with the Nginx webserver container and the application code with the app container.

Under the command definition, we have specified a subcommand to run the container’s default Certbot certonly command with additional options as listed below:

    • --webroot: specifies the use of the webroot plugin which places files in the webroot folder for authentication.
    • --webroot-path: specifies the path of the webroot directory.
    • --agree-tos: specifies that you agree to ACME’s Terms of service.
    • --no-eff-email: specifies that you do not want to share your email with EFF. You can omit this if you want to share.
    • --staging: tells Certbot that you want to first get test certificates from the Let’s Encrypt staging environment for testing your configuration before obtaining the actual certificate. Let’s Encrypt has domain request rate limiting. Hence, first testing your configuration will help you avoid your domain getting limited.
    • -d: this option takes the domain names for the certificate request. In this tutorial, we have included example.com and www.example.com. Please specify your actual registered domain.

Our docker-compose.yml file is almost complete. However, you must also add the network and volume definitions below the Certbot service:

The volumes key defines the volumes to be shared with all the services (containers) defined in this compose file: certbot-etc, app, and dbdata. The contents of the volumes that Docker creates are stored in a directory managed by Docker on the host file system: /var/lib/docker/volumes/. The contents of each volume are then mounted to any container that uses the volume. This makes it possible to share data and code between containers.

The networks key defines the bridge network that allows communication between containers. Containers on the same bridge network such as webserver and db can communicate securely through ports without exposing the traffic to the outside network. We only expose port 80 to allow access to the front-end website pages.

The complete docker-compose.yml file will look like this:

You can save and close the file. In the next step, we will be starting and testing the container and certificate requests.

Step 4: Running the Containers and Obtaining SSL Certificates

The greatest advantage of Docker Compose is that, once you have defined all your services in the docker-compose.yml file, you can start all the containers with just one command: docker-compose up. The command runs every instruction specified. If the domain requests are successful, you should be able to see the correct exit status in your terminal. Enter the following command to create the containers. The -d flag is for running the containers in the background:

If you see the output like in the screenshot below, then the services were created successfully:

docker-compose up

To confirm the status of the services, run the docker-compose ps command:

The output of the command is as shown below if everything was successful. The app, db, and webserver containers’ state should be up, and the certbot container should have Exit0 status:

Exit0

If you see anything other than Up in the state column for app, db or webserver, or an Exit status that is not 0 for the certbot container, then something went wrong. You can check the logs of each container using the command docker-compose logs, and specify the service_name:

For example, you can check the logs of the certbot container by entering the following command:

To check if the certificates were mounted to the webserver container, use the docker-compose exec command:

If you used an actual registered domain name other than the example.com and the certificate requests were successful, you should see an output similar to this:

registered domain

Once you have confirmed that the certificate request was successful, you can edit the docker-compose.yml file and remove the --staging flag. Open the file with nano:

Scroll down to the Certbot service definition section, in the command option and replace the --staging flag with --force-renewal flag. This tells Certbot that you are requesting a certificate renewal for a certificate of the same domain. Your Certbot service definition should now look like this:

Save the file when you are done editing.

Enter the following command to recreate the certbot container. The included --no-deps flag tells Compose to skip restarting the web server service since it’s already running:

The command outputs the following screenshot, showing that the certificate request was successful:

docker-compose up

That is all for this step. In the next step, you will modify the Nginx configuration file to include the SSL certificate.

Step 5: Enabling SSL in Nginx Configuration and Service Definition

To make Nginx serve traffic over secure SSL, you will first modify the Nginx configuration file to add an HTTP redirection to HTTPS. Then, you need to specify the certificate and key locations, and finally add security parameters and headers.

Before modifying the configuration file, you should get the recommended Nginx security parameters from Certbot’s GitHub repository using curl with the following command:

https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf

The command runs and saves the parameters it pulls into a file called options-ssl-nginx.conf, inside the nginx-conf directory. Remove the Nginx configuration file so that we can create a new one with the following commands:

In the now empty nginx.conf file, add the following code that includes a redirection from HTTP to HTTPS, SSL credential protocols, and security headers. As you have done earlier, replace the example.com domain with your own registered domain:

In the first server block that handles unsecured requests using port 80, we specify the webroot for Certbot renewal requests. We also include a redirect directive that redirects HTTP requests to HTTPS.

The second server block handles secure HTTPS traffic coming on port 443. As you can see, we also enable SSL and HTTP2. HTTP/2 improves the performance of your server. You can read more about it from the official Nginx docs on HTTP/2.

In this block, we have also specified that Nginx includes the SSL certificate and key locations, as well as the recommended Certbot security parameters that curl saved to nginx-conf/options-ssl-nginx.conf directory.

The additional security headers serve to improve the ratings of your website on security test sites such as Security Headers and SSL Labs. You can follow the links on these headers to learn more: X-Frame-Options, Referrer Policy, X-Content-Type-Options, X-XSS-Protection, Content-Security-Policy. We have commented out the HTTP Strict Transport Security (HSTS) header. You are free to read about its preload functionality and decide if you want to enable it.

The rest of the directives such as root, index, WordPress-specific location blocks remain as discussed in Step 1. You may now save and close the file when you have finished editing.

Now that we have enabled HTTPS traffic that uses port 443, we must also enable the port on the service definition of the web server. Enter the following command to open the docker-compose.yml file with nano:

In the web server section under the ports option, add a mapping for port 443 as highlighted below:

The complete docker-compose.yml file should now look like this:

Once you have confirmed that everything looks correct, save and close the file. After that, run the following command to recreate the webserver service:

Confirm that your services are running with the following command:
You should see something like the screenshot below confirming that your services are running well:

confirming

Now that all your containers are running, it’s possible to proceed with WordPress configuration from the web interface.

Step 6: Complete Your WordPress Configuration from the Web Interface

Navigate to your server’s domain name to continue the installation. You should see the WordPress setup homepage. It welcomes you to choose your language before continuing:

WordPress with Docker 6

Choose your language and click Continue to move to the next page:

WordPress with Docker 5

 

On this page, fill in your website title, choose a memorable username and a strong password. It’s recommended not to use Admin as your username for security reasons. Enter your email and click the Install WordPress button to begin installing WordPress.

Once the installation completes, you will be taken to the login screen where you will provide the username and the password you had set. When you enter the valid credentials, you should be able to see your WordPress dashboard:

WordPress with Docker 4

You have now successfully installed WordPress! Next, you need to take the steps to ensure that the SSL certificates will auto-renew.

Step 7: Configuring Automatic SSL Certificate Renewal

Let’s Encrypt TLS/SSL certificates are valid for 90 days only. It’s up to you to create an auto-renewal configuration to ensure they do not expire. You can achieve this by creating a script and scheduling it with the cron job utility. In this step, we will show you how to create a script that will renew the certificates. We will then schedule it with cron job utility to periodically run it and renew the certificates if they are approaching the expiry date.

Inside the wordpress_docker project directory, open a script called ssl_renewer.sh with nano:

Add the following code to the script to handle auto-renewal and Nginx configuration reloading. Remember to replace the highlighted username with your non-root username:

In this script, we assign the docker-compose binary to a variable called COMPOSE. We also include the –ansi never option which tells the script to run docker-compose commands without ANSI control characters. We further assign the Docker binary to a variable called DOCKER.

The script then moves into our project directory wordpress_docker and executes the following commands:

  • docker-compose run: it starts the certbot container and overrides the command we had provided in the certbot service definition. Instead of running the certonly subcommand, it runs the renew subcommand which will renew the SSL/TLS certificates from Let’s Encrypt if they are about to expire.
  • docker-compose kill: sends a SIGHUP signal to the webserver container to reload the Nginx configurations. You may want to check out this tutorial from Docker on how to use the Official Nginx Docker image.
  • docker system prune: this command removes all unused containers and images.

Save and close the file when you finish editing. Then, run the following command to make it executable:

Once you have made it executable, open your root crontab file to run the script periodically at the intervals we will specify:

The crontab asks you to choose your preferred editor if it’s your first time using it:

WordPress with Docker 2

Choose your preferred editor and press Enter to open the file. At the bottom of the file, add the following line:

This sets the interval for five minutes to allow us to test whether our renewal script will work or not. We have also specified a log file that will hold the output from the job: cron_docker.log.

Wait for five minutes and check the cron.log to see if the script was successful with the renewal request:

You should see something similar to the screenshot below if the requests were successful:

WordPress with Docker 1

Now that we have tested and confirmed it’s working, you can modify the crontab file to specify a daily renewal. For example, you may want to specify that the script runs every day at 6. PM. To do that, modify the last line of the crontab to look like this:

In addition, you need to remove the –dry-run flag from the ssl_renewer.sh script to ensure the actual renewal happens when it runs. It should look like this:

Next, save and close the file. Having done that, the cron job will keep your scripts valid, by renewing them before the 90 days’ end.

Conclusion

If you have reached this far in the tutorial, you may consider yourself a step closer to being a DevOps Engineer. You were able to create an Nginx configuration script, created a docker-compose.yml file, and defined several services necessary to run a WordPress application with Docker and Docker Compose. You obtained SSL/TLS certificates from Let’s Encrypt to ensure that your web server is secure. Finally,  you created a cron job to ensure that the certificates do not expire. Good job!

If you are trying to get deeper into DevOps, take a look at more resources on containers from our blog:

Happy Computing!