How To Install WordPress With Docker On Ubuntu 18.04
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
- Root access to your server, or a non-root user with Sudo privileges.
- 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:
MYSQL_DATABASE=<code class="language-bash">WORDPRESS_DB_NAME
MYSQL_USER
=WORDPRESS_DB_USER
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:
- Version: This is the docker-compose version.
- Services: This is the start of the definition of our services, which are basicly are containers.
- db, wordPress, webserver, redis: Our services names.
- image: The docker imaged to be pulled (if doesn’t exist locally) from Docker Hub.
- container_name: The name of our containers.
- hostname: The hostname of our containers.
- restart: Restart one or more containers under certain conditions.
- env_file: A file containing environment variables to be used. In our case, MySQL and WordPress credentials.
- volumes: Docker’s way of creating persistent data. It mounts the host’an s file system location, to a container’s file system location.
- depends_on: Express dependency between services. In our case the start up order of containers (services), will be db->wordpress->webserver->redis.
- 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.conf
and 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!