Introduction

In this tutorial, you will learn how to deploy a WordPress installation in a containerized environment by utilizing Docker. We will create virtualized isolated environments called Containers, which they will be communicating with each other through well defined internal networks. The configuration of installation is typically harder than a normal “legacy” installation, but the benefits using containers can not be neglected. We will install WordPress with PHP-FPM, Nginx WebServer, MySQL, LetEncrypt, and Redis for caching. In other words, a fully-fledged production Server. Let’s start learning how to install WordPress with Docker containers!

Prerequisites

  1. Root access to your server, or a non-root user with Sudo privileges.
  2. Docker and Docker Compose already installed in the host machine.

Nginx, WordPress-PHP, MySQL,Redis and LetsEncrypt Installation

In Docker, the installation of software takes place with images. Images are essentially snapshots of shrinked operating systems such as Alpine Linux, Ubuntu, Centos etc, containing the software we are requesting. Generally speaking, each container should only run one service at a time.

The modern way creating services with Docker, is through a file called Docker Compose file. This is basically just a file written in yaml syntax, that contains all the instructions and commands our application needs in order to run. It will automatically download all the images we specify, and run whichever command on them we instruct it to.

Step 1 – Local Folder Creation On Host

For starters, a working directory should be created. This will be the main directory in which all of our files will reside:

sudo mkdir myapp && cd myapp

The folders which we will create, will be used to create container data persistence amd service configuration without the need to ssh to containers in the traditional way. Let’s create now the folders needed to configure Nginx, PHP, Redis, and our environment variables:

sudo mkdir nginx-conf && sudo mkdir php-conf && sudo mkdir redis && sudo mkdir variables

These folder will be the base of our application configuration.

Step 2 – Nginx configuration

In order to have full control over Nginx configuration,the nginx-conf folder on our host willl be mounted to the location /etc/nginx on the container. In regards of that fact, we need to create the nessesary files inside the nginx-conf folder. Let’s do that:
nginx.conf file:

sudo echo "user  nginx;
worker_processes  1;
error_log  /var/log/nginx/error.log error;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    map_hash_max_size 256;
    map_hash_bucket_size 256;
    types_hash_bucket_size 256;
    server_names_hash_bucket_size 256;
    sendfile    on;
    tcp_nodelay on;
    tcp_nopush  off;
    autoindex off;
    server_tokens off;
    keepalive_timeout  15;    
    client_max_body_size 100m;
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    #=====================Basic Compression=====================
    gzip on;
    gzip_static on;
    gzip_disable "msie6";
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_types text/css text/xml text/plain application/javascript image/jpeg image/png image/gif image/x-icon image/svg+xml image/webp application/font-woff application/json application/vnd.ms-fontobject application/vnd.ms-powerpoint;
    #===Virtual Host Configs======    
    include /etc/nginx/sites-enabled/example-http.conf 
    #include /etc/nginx/sites-enabled/example-https.conf;    
}" > nginx-conf/nginx.conf

mime.types file:

sudo echo "types {
    text/html                                        html htm shtml;
    text/css                                         css;
    text/xml                                         xml;
    image/gif                                        gif;
    image/jpeg                                       jpeg jpg;
    application/javascript                           js;
    application/atom+xml                             atom;
    application/rss+xml                              rss;

    text/mathml                                      mml;
    text/plain                                       txt;
    text/vnd.sun.j2me.app-descriptor                 jad;
    text/vnd.wap.wml                                 wml;
    text/x-component                                 htc;

    image/png                                        png;
    image/svg+xml                                    svg svgz;
    image/tiff                                       tif tiff;
    image/vnd.wap.wbmp                               wbmp;
    image/webp                                       webp;
    image/x-icon                                     ico;
    image/x-jng                                      jng;
    image/x-ms-bmp                                   bmp;

    font/woff                                        woff;
    font/woff2                                       woff2;

    application/java-archive                         jar war ear;
    application/json                                 json;
    application/mac-binhex40                         hqx;
    application/msword                               doc;
    application/pdf                                  pdf;
    application/postscript                           ps eps ai;
    application/rtf                                  rtf;
    application/vnd.apple.mpegurl                    m3u8;
    application/vnd.google-earth.kml+xml             kml;
    application/vnd.google-earth.kmz                 kmz;
    application/vnd.ms-excel                         xls;
    application/vnd.ms-fontobject                    eot;
    application/vnd.ms-powerpoint                    ppt;
    application/vnd.oasis.opendocument.graphics      odg;
    application/vnd.oasis.opendocument.presentation  odp;
    application/vnd.oasis.opendocument.spreadsheet   ods;
    application/vnd.oasis.opendocument.text          odt;
    application/vnd.openxmlformats-officedocument.presentationml.presentation
                                                     pptx;
    application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
                                                     xlsx;
    application/vnd.openxmlformats-officedocument.wordprocessingml.document
                                                     docx;
    application/vnd.wap.wmlc                         wmlc;
    application/x-7z-compressed                      7z;
    application/x-cocoa                              cco;
    application/x-java-archive-diff                  jardiff;
    application/x-java-jnlp-file                     jnlp;
    application/x-makeself                           run;
    application/x-perl                               pl pm;
    application/x-pilot                              prc pdb;
    application/x-rar-compressed                     rar;
    application/x-redhat-package-manager             rpm;
    application/x-sea                                sea;
    application/x-shockwave-flash                    swf;
    application/x-stuffit                            sit;
    application/x-tcl                                tcl tk;
    application/x-x509-ca-cert                       der pem crt;
    application/x-xpinstall                          xpi;
    application/xhtml+xml                            xhtml;
    application/xspf+xml                             xspf;
    application/zip                                  zip;

    application/octet-stream                         bin exe dll;
    application/octet-stream                         deb;
    application/octet-stream                         dmg;
    application/octet-stream                         iso img;
    application/octet-stream                         msi msp msm;

    audio/midi                                       mid midi kar;
    audio/mpeg                                       mp3;
    audio/ogg                                        ogg;
    audio/x-m4a                                      m4a;
    audio/x-realaudio                                ra;

    video/3gpp                                       3gpp 3gp;
    video/mp2t                                       ts;
    video/mp4                                        mp4;
    video/mpeg                                       mpeg mpg;
    video/quicktime                                  mov;
    video/webm                                       webm;
    video/x-flv                                      flv;
    video/x-m4v                                      m4v;
    video/x-mng                                      mng;
    video/x-ms-asf                                   asx asf;
    video/x-ms-wmv                                   wmv;
    video/x-msvideo                                  avi;
}" > nginx-conf/mime.types

fastcgi_params file:

sudo echo "fastcgi_param  QUERY_STRING       $query_string;
fastcgi_param  REQUEST_METHOD     $request_method;
fastcgi_param  CONTENT_TYPE       $content_type;
fastcgi_param  CONTENT_LENGTH     $content_length;

fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
fastcgi_param  REQUEST_URI        $request_uri;
fastcgi_param  DOCUMENT_URI       $document_uri;
fastcgi_param  DOCUMENT_ROOT      $document_root;
fastcgi_param  SERVER_PROTOCOL    $server_protocol;
fastcgi_param  REQUEST_SCHEME     $scheme;
fastcgi_param  HTTPS              $https if_not_empty;

fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;

fastcgi_param  REMOTE_ADDR        $remote_addr;
fastcgi_param  REMOTE_PORT        $remote_port;
fastcgi_param  SERVER_ADDR        $server_addr;
fastcgi_param  SERVER_PORT        $server_port;
fastcgi_param  SERVER_NAME        $server_name;

# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param  REDIRECT_STATUS    200;" > nginx-conf/fastcgi_params

Now we need to create the folder sites-enabled, and our actually server configuration. (Which will be non-ssl for the time being):

sudo mkdir nginx-conf/sites-enabled

and lastly, issue the command:

sudo echo "server {
    server_name example.com;
    listen      80;
    listen      [::]:80;
    root        /var/www/html;
    index       index.php;
    charset     UTF-8;

location ~* \.(jpg|jpe?g|gif|png|ico|cur|gz|svgz|mp4|ogg|ogv|webm|htc|css|js|otf|eot|svg|ttf|woff|woff2)(\?ver=[0-9.]+)?$ {
    expires 1M;
   #expires modified 1M;
    add_header Access-Control-Allow-Origin '*';
    add_header Pragma public;
    add_header Cache-Control "public, must-revalidate, proxy-revalidate";
    access_log off;
    }
    location ~ /.well-known {
        allow all;
    }
location / {
    try_files $uri $uri/ /index.php?$args;
    }
    error_page  404  /404.php;
#pass the PHP scripts to FastCGI server listening on php-fpm unix socket
location ~ \.php$ {
    try_files       $uri =404;
    fastcgi_index   index.php;
    fastcgi_pass    wordpress:9000;
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_param   SCRIPT_FILENAME  $document_root$fastcgi_script_name;
    fastcgi_intercept_errors on;
    fastcgi_ignore_client_abort off;
    fastcgi_connect_timeout 60;
    fastcgi_send_timeout 180;
    fastcgi_read_timeout 180;
    fastcgi_buffer_size 128k;
    fastcgi_buffers 4 256k;
    fastcgi_busy_buffers_size 256k;
    fastcgi_temp_file_write_size 256k;
    include fastcgi_params;
}

location = /robots.txt {
    access_log off;
    log_not_found off;
    }
location ~ /\. {
    deny  all;
    access_log off;
    log_not_found off;
    }
}" > nginx-conf/sites-enabled/example-http.conf

Nginx configuration is done for the moment and we can move on to the next step.

Step 3 – Environment Files

Enviroment files, are files containing the values of our enviroment files. In our case, we will need MySQL and WordPress enviroment values for our installation to work correcly. Go ahead and do that like so:

sudo echo "MYSQL_ROOT_PASSWORD=your-mysql-root-password
MYSQL_DATABASE=wordpress-database-name
MYSQL_USER=wordpress-mysql-user
MYSQL_PASSWORD=wordpress-mysql-user-password" > variables/mysql.env

And same thing goes for WordPress:

sudo echo"WORDPRESS_DB_HOST=db
WORDPRESS_DB_USER=wordpress-mysql-user
WORDPRESS_DB_PASSWORD=wordpress-mysql-user-password
WORDPRESS_DB_NAME=wordpress-database-name
WORDPRESS_DB_COLLATE=utf8_general_ci" > /variables/wordpress.env

Keep in mind that from the two env files:

  1. MYSQL_DATABASE=<code class="language-bash">WORDPRESS_DB_NAME
  2. MYSQL_USER= WORDPRESS_DB_USER
  3. MYSQL_PASSWORD= WORDPRESS_DB_PASSWORD

Replace all the above values with your own.

Step 4- PHP Configuration

This is essential only if you want custom values in php.ini, which i assume you do. For this situation, we need to create a php.ini file inside our php-conf folder. Execute the command below:

cd php-conf && sudo wget https://raw.githubusercontent.com/php/php-src/master/php.ini-production && sudo mv php.ini-production php.ini && cd ..

Great, now you have a fresh php.ini file inside your php-conf folder.

That was all the configuration needed for php. All the rest of the files and folders considering data persistance, will be created automatically in runtime while our containers are being created.

Step 5 – The Docker Compose File

Enviroment files, are files containing environment values, specific to each service. In our case, we will need MySQL and WordPress enviroment values for our installation to work properly. Go ahead and do that like so:

vi docker-compose.yaml

And paste the contents below:

version: '3.3'

services:
  db:
    image: mariadb:latest
    container_name: db
    hostname: db
    restart: unless-stopped
    env_file: variables/mysql.env
    volumes:
      - ./db-data:/var/lib/mysql
    command: mysqld --max_allowed_packet=128M --character-set-server=utf8 --collation-server=utf8_unicode_ci --init-connect='SET NAMES UTF8;' --innodb-flush-log-at-trx-commit=0
    networks:
      - app-net

  wordpress:
    depends_on:
        - db
    image: admintuts/wordpress:php7.3-fpm-redis-alpine
    container_name: wordpress
    hostname: wordpress
    restart: unless-stopped
    env_file: variables/wordpress.env
    volumes:
      - ./wordpress-data:/var/www/html
      - ./php-conf/php.ini:/usr/local/etc/php/php.ini
    networks:
      - app-net

  webserver:
    depends_on:
      - wordpress
    image: nginx:1.17.5-alpine
    container_name: webserver
    hostname: webserver
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./certbot/etc/:/etc/letsencrypt
      - ./certbot/var/:/var/lib/letsencrypt
      - ./wordpress-data:/var/www/html
      - ./nginx-conf:/etc/nginx
    networks:
      - app-net
  redis:
    depends_on:
      - webserver
    image: redis:5.0.6-alpine
    container_name: redis
    hostname: redis
    restart: unless-stopped
    volumes:
      - ./redis/cache/:/data
    command: redis-server --bind redis --requirepass some_long_password --maxmemory 256mb --maxmemory-policy allkeys-lru --appendonly yes
    ports:
      - "6379:6379"
    networks:
      - app-net

volumes:
  db-data:
  wordpress-data:
  nginx-conf:
  php-conf:
  redis:
  certbot:

networks:
  app-net:
    driver: bridge

Save the file, and exit your editor. Now let me explain what this docker-compose file features:

  1. Version: This is the docker-compose version.
  2. Services: This is the start of the definition of our services, which are basicly are containers.
  3. db, wordPress, webserver, redis: Our services names.
  4. image: The docker imaged to be pulled (if doesn’t exist locally) from Docker Hub.
  5. container_name: The name of our containers.
  6. hostname: The hostname of our containers.
  7. restart: Restart one or more containers under certain conditions.
  8. env_file: A file containing environment variables to be used. In our case, MySQL and WordPress credentials.
  9. volumes: Docker’s way of creating persistent data. It mounts the host’an s file system location, to a container’s file system location.
  10. depends_on: Express dependency between services. In our case the start up order of containers (services), will be db->wordpress->webserver->redis.
  11. networks: The network that our containers belongs to. This is how our containers will communicate with each other creating a unified network.

Now is time to run the docker-compose file that we created. Issue the following command and watch our containers created:

docker-compose -f docker-compose.yaml up

You will be seeing a similar output in your console like this one:

Starting db ... 
Starting db ... done
Starting wordpress ... 
Starting wordpress ... done
Starting webserver ... 
Starting webserver ... done
Starting redis ... 
Starting redis ... done
Attaching to db, wordpress, webserver, redis

And if you done everything correctly, when all services interactions are finished, you will be seeing this:

redis        | 1:M 31 Oct 2019 21:54:06.497 * Ready to accept connections
db           | 2019-10-31 21:54:05 0 [Note] Plugin 'FEEDBACK' is disabled.
db           | 2019-10-31 21:54:05 0 [Note] InnoDB: Loading buffer pool(s) from /var/lib/mysql/ib_buffer_pool
db           | 2019-10-31 21:54:05 0 [Note] Server socket created on IP: '::'.
db           | 2019-10-31 21:54:05 0 [Warning] 'proxies_priv' entry '@% root@db' ignored in --skip-name-resolve mode.
db           | 2019-10-31 21:54:05 0 [Note] Reading of all Master_info entries succeeded
db           | 2019-10-31 21:54:05 0 [Note] Added new Master_info '' to hash table
db           | 2019-10-31 21:54:05 0 [Note] mysqld: ready for connections.
db           | Version: '10.4.8-MariaDB-1:10.4.8+maria~bionic'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  mariadb.org binary distribution
db           | 2019-10-31 21:54:05 0 [Note] InnoDB: Buffer pool(s) load completed at 191031 21:54:05
wordpress    | [31-Oct-2019 21:54:08] NOTICE: fpm is running, pid 1
wordpress    | [31-Oct-2019 21:54:08] NOTICE: ready to handle connections

As you probably noticed, redis, mysql, and wordpress are ready to handle incoming connections. Give your self a pat on your back! Visit your site to complete WordPress installation.

Step 6 – SSL Certificate Issuance

We will use the –staging flag while running Certbot to make sure everything is going as planned. While your website is up and running, issue the following command

docker run -it --rm -v /certbot/etc/:/etc/letsencrypt -v /certbot/var/:/var/lib/letsencrypt -v /wordpress-data:/var/www/html certbot/certbot:latest certonly --webroot --webroot-path=/var/www/html --agree-tos --no-eff-email --staging -d example.com -d www.example.com

Normally you will receice output that the certificate issuance was successful. Clean the local certbot directory with sudo rm -rf certbot, and execute the production certificate command:

docker run -it --rm -v /certbot/etc/:/etc/letsencrypt -v /wordpress-data:/var/www/html certbot/certbot:latest certonly --webroot --webroot-path=/var/www/html --email username@example.com --agree-tos -d example.com -d www.example.com

Now that your certificate is created, and is time to create a new nginx ssl enabled configuration. Let’s create it with one command:

sudo echo "server {
  server_name xxx.xxx.xxx.xxx; #Your current server ip address. It will redirect to the domain name.
  listen 80;
  listen 443 ssl http2;
  ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
  return 301 https://example.com$request_uri;
}
server {
  server_name  example.com;
  listen       *:80;
  return 301   https://example.com$request_uri;
}
server {
  server_name www.example.com;
  listen 80;
  listen 443 ssl http2;
  listen [::]:80;
  listen [::]:443 ssl http2;
  add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains; preload';
  ssl_prefer_server_ciphers on;
  ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
  ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
  ssl_session_cache   shared:SSL:10m;
  ssl_session_timeout 10m;
  keepalive_timeout   70;
  ssl_buffer_size 1400;
  ssl_dhparam ssl/dhparam.pem;
  ssl_stapling on;
  ssl_stapling_verify on;
  resolver 8.8.8.8 8.8.4.4 valid=86400;
  resolver_timeout 10;
  ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
  # Non-www redirect
  return 301 https://example.com$request_uri;
}
server {
  server_name example.com;
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  root /var/www/html;
  charset UTF-8;
  add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains; preload';
  add_header X-Frame-Options SAMEORIGIN;
  add_header X-Content-Type-Options nosniff;
  add_header X-XSS-Protection "1; mode=block";
  add_header Referrer-Policy no-referrer;
  ssl_prefer_server_ciphers on;
  ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
  ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
  ssl_session_cache   shared:SSL:10m;
  ssl_session_timeout 10m;
  keepalive_timeout   70;
  ssl_buffer_size 1400;
  ssl_dhparam ssl/dhparam.pem;
  ssl_stapling on;
  ssl_stapling_verify on;
  resolver 8.8.8.8 8.8.4.4 valid=86400;
  resolver_timeout 10;
  ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
  location ~* \.(jpg|jpe?g|gif|png|ico|cur|gz|svgz|mp4|ogg|ogv|webm|htc|css|js|otf|eot|svg|ttf|woff|woff2)(\?ver=[0-9.]+)?$ {
  expires modified 1M;
  add_header Access-Control-Allow-Origin '*';
  add_header Pragma public;
  add_header Cache-Control "public, must-revalidate, proxy-revalidate";
  access_log off;
  }
  #access_log  logs/host.access.log  main;
  location ~ /.well-known { #letsencrypt hidden directory for renewals
    allow all;
  }
  location / {
    index index.php;
    try_files $uri $uri/ /index.php?$args;
  }
  error_page  404    /404.php;
  location /wp-config.php {
    deny all;
  }
  #pass the PHP scripts to php-fpm server listening on wordpress:php-fpm port 9000
  location ~ \.php$ {
   try_files       $uri =404;
   fastcgi_index   index.php;
   fastcgi_pass    wordpress:9000;
   fastcgi_pass_request_headers on;
   fastcgi_split_path_info ^(.+\.php)(/.+)$;
   fastcgi_param   SCRIPT_FILENAME  $document_root$fastcgi_script_name;
   fastcgi_intercept_errors on;
   fastcgi_ignore_client_abort off;
   fastcgi_connect_timeout 60;
   fastcgi_send_timeout 180;
   fastcgi_read_timeout 180;
   fastcgi_request_buffering on;
   fastcgi_buffer_size 128k;
   fastcgi_buffers 4 256k;
   fastcgi_busy_buffers_size 256k;
   fastcgi_temp_file_write_size 256k;
   include fastcgi_params;
  }
  location = /robots.txt {
    access_log off;
    log_not_found off;
  }
  location ~ /\. {
    deny  all;
    access_log off;
    log_not_found off;
  }
}" > nginx-conf/sites-enabled/example-https.conf

And lastly, edit your nginx.cof and comment out the line include /etc/nginx/sites-enabled/example-http.confand uncomment the line include /etc/nginx/sites-enabled/example-https.conf.

Close the file, and execute the command:

docker container exec webserver nginx -s reopen

This will reload the new configuration on the container name “webserver”, that our Nginx instance is running on. If you visit your website now, you will see that is fully secured!

Step 7 – Automated LetsEncrypt Certificate Renewal

For the certificate renewal, we will create a bash script and we will use a crobjob to execute it periodically since LetsEncrypt certificates are only valid for 90 days. Let’s create a file called ssl-renewal.sh by sudo vi ssl-renew.sh, with the contents as described below:

#!/bin/bash
COMPOSE="/usr/bin/docker-compose --no-ansi"
cd /home/username/wordpress-data-location/
$COMPOSE run certbot renew && $COMPOSE kill -s SIGHUP webserver

This bash file can be placed anywhere you want, and it must be executable. This can be done by executing:

chmod +x ssl-renew.sh

For reference though, save it at /home/username. Let’s create now a crobjob as root, to handle it’s execution:

sudo crontab -e 

and paste the code below:

0 12 * * 5 /home/username/ssl_renew.sh >> /var/log/docker-ssl-cron.log 2>&1

This cronjob will be taking place at 12:00 every Friday, hence our own bash script.

So that was it! You now learned how to containerize your WordPress installation!

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *