WordPress Deployment with Jenkins and Docker

August 2020 | John Binzak
WordPress, Github, Docker, Jenkins, and Watchtower. We will need to use the combination of these 5 in order to achieve our goal of containerizing and automating our WordPress deployment.

WordPress Pipeline

WordPress, Github, Docker, Jenkins, and Watchtower. We will need to use the combination of these 5 in order to achieve our goal of containerizing and automating our WordPress deployment. WordPress will be our website platform, Github our source control, Docker will containerize our website, Jenkins will build the container, and Watchtower will deploy the container.

Why do we want to do this? Reliability and scalability. When you create a website, you need to focus on building and growing that website. The last thing you want to deal with is a server issue. Whether it is a plugin conflicting with an outdated PHP version, a memory leak causing crash, or anything in between, this means downtime for your website. Downtime is bad for business. Sometimes fatality bad.

Source control will allow us to track changes to the website. If there is a bug in the code, we can easily revert to a safe build. Containerizing the website will allow us to maintain the environment where the website lives, mainly the PHP version, along with other server config. We can test updates to PHP before we push it live. Jenkins will then help run tests and build our container, while Watchtower deploys the new container to our server or servers. All of these together helps us with our reliability and scalability.

New WordPress Website

If you don't already have a website, you can download a fresh WordPress website installation from WordPress.org. Unzip the download you will then have all the source files for a WordPress website.

Source Control

Next you need a git repository. We like to use Github, however you can easily use Gitlab, Bitbucket, or any other solution. Create a repository for your WordPress website. This will be our source control and Jenkins will eventually pull from this repo before it builds the Docker container.

Now you will need to commit your website code to the git repository. Moving forward try to make you commits isolated in terms of changes. For example, don't edit 5 plugins and commit all the changes at once. Make one commit for each of the set of changes made to a given plugin. This will be helpful incase you need to rollback changes. Need a refresher on git commands? See the git documentation.

Docker

If you don't already have a Docker account, go ahead and sign up for one. You can view their pricing here. Currently you can have one private repo with a free account. Once you have an account, create a new repository for your WordPress website container. This is similar to what you did with Github.

Jenkins

To install Jenkins on your operating system, visit the Jenkins website and review the installation which is suitable for your operating system or cloud service. We could give you directions, but it is best to follow the installation instructions on their website.

Once you have Jenkins installed, create a new pipeline.

Next you can adjust the settings to your liking but we typically use the following:

  • Discard old builds, Max # of builds to keep = 2
  • Github project, you will need to enter the url of the git repo you created
  • Pipeline script from SCM, again you will need to enter the url of the git repo you created
  • Additional Behaviors = Clean after checkout, Clean before checkout, Wipe out repository & force clone

WordPress Security Config

One of the tricky items you face as a developer when creating a development pipeline is what to do with credentials. We don't want to store database credentials or other secret keys directly in our git repo for security reasons. So we will use Jenkins to inject them into our Docker building process.

Before we do that let's go back to our website source code. We will be dynamically creating a so let's copy in the same directory but name the new copy . In this new file let's remove all the definitions for database constants and security salts/keys. In replace of those, we will add a require statement for a new file that we will dynamically create within our Docker build process. Your should look something like this:

<?php
/**
 * The base configuration for WordPress
 *
 * The wp-config.php creation script uses this file during the
 * installation. You don't have to use the web site, you can
 * copy this file to "wp-config.php" and fill in the values.
 *
 * This file contains the following configurations:
 *
 * * MySQL settings
 * * Secret keys
 * * Database table prefix
 * * ABSPATH
 *
 * @link https://wordpress.org/support/article/editing-wp-config-php/
 *
 * @package WordPress
 */

/** load env vars set via pipeline */
require_once ABSPATH . 'wp-config-env.php';

/**#@-*/

/**
 * WordPress Database Table prefix.
 *
 * You can have multiple installations in one database if you give each
 * a unique prefix. Only numbers, letters, and underscores please!
 */
$table_prefix = 'wp_';

/**
 * For developers: WordPress debugging mode.
 *
 * Change this to true to enable the display of notices during development.
 * It is strongly recommended that plugin and theme developers use WP_DEBUG
 * in their development environments.
 *
 * For information on other constants that can be used for debugging,
 * visit the documentation.
 *
 * @link https://wordpress.org/support/article/debugging-in-wordpress/
 */
define( 'WP_DEBUG', false );

/* That's all, stop editing! Happy publishing. */

/** Absolute path to the WordPress directory. */
if ( ! defined( 'ABSPATH' ) ) {
	define( 'ABSPATH', __DIR__ . '/' );
}

/** Sets up WordPress vars and included files. */
require_once ABSPATH . 'wp-settings.php';

As mentioned before, we will create the required file from our Docker build process and it will have all the normal WordPress constants that we removed. We now need to add the values of those constants to Jenkins. Jenkins will pass them on to our Docker build. For each of the constants we removed from the , we need to add a secret text in Jenkins.

Jenkinsfile

Now we need to create our Jenkinsfile. This will outline all of the steps that Jenkins needs to perform. For now we will just have it build our Docker container, push it to our Docker hub, and send us a build result email.

pipeline {
    environment {
        registry = "your-account/my-wordpress-website"
        registryCredential = 'your-docker-credentials-in-jenkins'
        prodImage = ''
        PROD_DB_HOST = credentials('PROD_DB_HOST')
        PROD_DB_DATABASE = credentials('PROD_DB_DATABASE')
        PROD_DB_USERNAME = credentials('PROD_DB_USERNAME')
        PROD_DB_PASSWORD = credentials('PROD_DB_PASSWORD')
        PROD_AUTH_KEY_VAL = credentials('PROD_AUTH_KEY_VAL')
        PROD_SECURE_AUTH_KEY = credentials('PROD_SECURE_AUTH_KEY')
        PROD_LOGGED_IN_KEY = credentials('PROD_LOGGED_IN_KEY')
        PROD_NONCE_KEY = credentials('PROD_NONCE_KEY')
        PROD_AUTH_SALT = credentials('PROD_AUTH_SALT')
        PROD_SECURE_AUTH_SALT = credentials('PROD_SECURE_AUTH_SALT')
        PROD_LOGGED_IN_SALT = credentials('PROD_LOGGED_IN_SALT')
        PROD_NONCE_SALT = credentials('PROD_NONCE_SALT')
    }
    agent none
    stages {
        stage('Build Prod') {
            agent {
                label 'master'
            }
            steps {
                echo 'Building Production....'


                script{
                    // build & push our docker image
                    prodImage = docker.build((registry+":${env.BUILD_NUMBER}"),
                        "-t "+registry+":${env.BUILD_NUMBER} " +
                        "-t "+registry+":latest " +
                        "--build-arg build_version=${env.BUILD_NUMBER} " +
                        "--build-arg db_host=$PROD_DB_HOST " +
                        "--build-arg db_database=$PROD_DB_DATABASE " +
                        "--build-arg db_username=$PROD_DB_USERNAME " +
                        "--build-arg db_password=$PROD_DB_PASSWORD " +
                        "--build-arg auth_key_val='$PROD_AUTH_KEY_VAL' " +
                        "--build-arg sercure_auth_key_val='$PROD_SECURE_AUTH_KEY' " +
                        "--build-arg logged_in_key_val='$PROD_LOGGED_IN_KEY' " +
                        "--build-arg nonce_key_val='$PROD_NONCE_KEY' " +
                        "--build-arg auth_salt_val='$PROD_AUTH_SALT' " +
                        "--build-arg secure_auth_salt_val='$PROD_SECURE_AUTH_SALT' " +
                        "--build-arg logged_in_salt_val='$PROD_LOGGED_IN_SALT' " +
                        "--build-arg nonce_salt_val='$PROD_NONCE_SALT' " +
                        "--file Dockerfile .")
                    docker.withRegistry( '', registryCredential) {
                        prodImage.push("latest")
                    }
                }
            }
        }
        stage('Clean Up') {
            agent {
                label 'master'
            }
            steps {
                echo 'Removing docker images....'
                sh 'docker system prune -af --volumes'
            }
        }
    }
    post {
        always {
            emailext body: "${currentBuild.currentResult}: Job ${env.JOB_NAME} build ${env.BUILD_NUMBER}\n More info at: ${env.BUILD_URL}",
                    recipientProviders: [[$class: 'DevelopersRecipientProvider'], [$class: 'RequesterRecipientProvider']],
                    subject: "Jenkins Build ${currentBuild.currentResult}: Job ${env.JOB_NAME}"

        }
    }
}

The big thing to notice is all the being called with the Docker build step. This is how we pass in the WordPress environment variables/constants we saved as Jenkins keys to our Dockerfile.

Dockerfile

Now let's build our container with the correct variables/constants and . If your deployment is simple, aka one container to one server, then you might want to use the official WordPress docker image. In the case you will have another proxy container, using port 80, you might want to use a PHP/Apache docker image.

# Final App
FROM php:7.4-apache

# copy src
COPY . /var/www/html

# env
ARG build_version
ARG db_host
ARG db_database
ARG db_username
ARG db_password
ARG auth_key_val
ARG sercure_auth_key_val
ARG logged_in_key_val
ARG nonce_key_val
ARG auth_salt_val
ARG secure_auth_salt_val
ARG logged_in_salt_val
ARG nonce_salt_val

# copy config
RUN touch /var/www/html/wp-config-env.php
COPY wp-config-short-sample.php /var/www/html/wp-config.php

# set db
RUN echo "<?php" >> /var/www/html/wp-config-env.php
RUN echo "define( 'DB_HOST', '$db_host' );" >> /var/www/html/wp-config-env.php
RUN echo "define( 'DB_NAME', '$db_database' );" >> /var/www/html/wp-config-env.php
RUN echo "define( 'DB_USER', '$db_username' );" >> /var/www/html/wp-config-env.php
RUN echo "define( 'DB_PASSWORD', '$db_password' );" >> /var/www/html/wp-config-env.php
RUN echo "define( 'DB_CHARSET', 'utf8mb4' );" >> /var/www/html/wp-config-env.php
RUN echo "define( 'DB_COLLATE', '' );" >> /var/www/html/wp-config-env.php

# set salt
RUN echo "define( 'AUTH_KEY', '$auth_key_val' );" >> /var/www/html/wp-config-env.php
RUN echo "define( 'SECURE_AUTH_KEY', '$sercure_auth_key_val' );" >> /var/www/html/wp-config-env.php
RUN echo "define( 'LOGGED_IN_KEY', '$logged_in_key_val' );" >> /var/www/html/wp-config-env.php
RUN echo "define( 'NONCE_KEY', '$nonce_key_val' );" >> /var/www/html/wp-config-env.php
RUN echo "define( 'AUTH_SALT', '$auth_salt_val' );" >> /var/www/html/wp-config-env.php
RUN echo "define( 'SECURE_AUTH_SALT', '$secure_auth_salt_val' );" >> /var/www/html/wp-config-env.php
RUN echo "define( 'LOGGED_IN_SALT', '$logged_in_salt_val' );" >> /var/www/html/wp-config-env.php
RUN echo "define( 'NONCE_SALT', '$nonce_salt_val' );" >> /var/www/html/wp-config-env.php

# set build
RUN echo "define( 'WP_BUILD_VERSION', '$build_version' );" >> /var/www/html/wp-config-env.php


# make sure web user owns dir
RUN chown -R www-data:www-data /var/www/html

# optional copy apache config
# COPY custom-apache.conf /etc/apache2/sites-available/000-default.conf

# enable
RUN a2enmod rewrite

# restart apache
RUN service apache2 restart

# install some additional php
RUN apt-get update
RUN apt-get install -y --no-install-recommends \
    		libfreetype6-dev \
    		libjpeg-dev \
    		libmagickwand-dev \
    		libpng-dev \
    		libzip-dev \
    	; \
    	\
    	docker-php-ext-configure gd --with-freetype --with-jpeg; \
    	docker-php-ext-install -j "$(nproc)" \
    		bcmath \
    		exif \
    		gd \
    		mysqli \
    		zip \
    	; \
    	pecl install imagick-3.4.4; \
    	docker-php-ext-enable imagick;

# optional set different port besides 80
ENV PORT 80
CMD sed -i "s/80/$PORT/g" /etc/apache2/sites-available/000-default.conf /etc/apache2/ports.conf && docker-php-entrypoint apache2-foreground

Although long, this is a very simple Dockerfile. The vast majority of this code is to accept the , create our and , and populate the secret constants into the newly created . Why do we do it this way? We like to explicitly create a Jenkins secret per WordPress constant in order to better debug any possible issues. This method also allows you to add new constants easily.

Build

If you haven't already, we need to add the Jenkinsfile & Dockerfile to the website git repo via commit. Once we have these two files in source control, we can go ahead and build our pipeline in Jenkins.

Automated Deploy

If you are new to containers then perhaps you might want to just manually pull your new container (post Jenkins build). However if you are a little bit more familiar with containers then perhaps you are ready for a tool like Watchtower. Essentially Watchtower is another container you deploy to your server that will your WordPress website container repo and automatically pull the latest version when it is found.

Summary

As mentioned in the beginning, when you create a website, you need to focus on building and growing that website. The last thing you want to deal with is a server issue. All of the configuration outlined above helps us with our reliability and scalability. Ultimately allowing us to focus on what is most important, the actual WordPress website. Here is a quick summary of all the steps:

  • Get a WordPress website & get it in git source control
  • Create a Jenkins pipeline linked to that git repo
  • Add all of the WordPress config constants as secrets
  • Modify the WordPress config files to be dynamically created/populated
  • Create a docker container within Jenkins pipeline, with the secrets
  • Use Jenkins to push to Docker hub, user Watchtower as an automated deployment tool