Install Lets Encrypt SSL Certificate using Docker, WordPress and DigitalOcean

In this article I am going to show you that how to install SSL Certificate on your wordpress website using Docker, Docker compose, Let’s Encrypt and Digital Ocean.

If you are interested in Video version of this article then you watch it from following youtube video:

I assume that you already have Docker and Docker compose installed and functional already.

To start you should create following folder structure:

In docker-compose.yaml file we will create three services:
1- WordPress
2- MySQL
3- Nginx ( letsencrypt image )

1- WordPress Service

.....
  wordpress:2
    image: wordpress:5.2.2-php7.3-apache
    container_name: wordpress
    restart: always
    depends_on:
      - mysql
    stdin_open: true
    tty: true
    volumes:
      - ./src:/var/www/html
    env_file:
      - .env
1..... 2 wordpress:2 3 image: wordpress:5.2.2-php7.3-apache 4 container_name: wordpress 5 restart: always 6 depends_on: 7 - mysql 8 stdin_open: true 9 tty: true 10 volumes: 11 - ./src:/var/www/html 12 env_file: 13 - .env

I added restart policy always. The benefit of this policy is that in case if your docker container stopped because of any error or crashed then it would automatically restart the container. That helps a lot because you don’t need to manually go to server and restart.

As this wordpress container depends on the mysql server because as you know that wordpress cannot work without a functional mysql server. So we should wait for the mysql service to be ready first before creating wordpress service. So to wait we added depends_on property.

stdin_open and tty are added to keep container alive forever.

In the volumes array I mounted our local src folder with the html container inside the container. So the benefit of this step is that if you remove the container then it would not delete the volumes that you have mounted. In this way you can persist your data without any loss.

You don’t need to expose any port for wordpress container because we are not going to access wordpress container directly. Its the nginx container that would forward our traffic to wordpress container’s port 80. I will show that later in this article.

in env_file array I added .env file path because wordpress image needs us to pass some environment variables as shown in the following screenshot:

image-1 (1).webp

Normally we use localhost as the database host name. But in this case mysql and wordpress will be installed in two different containers hence normally wordpress cannot access mysql on localhost host. But with the magic of docker-compose we can communicate any container from any container. We just need to use the name of server instead of localhost.

As we set the service name mysql for mysql server so that is why we would use mysql as host name through the environment variable WORDPRESS_DB_HOST

In other three variables, you need to add database username, password and database name. So make sure that in next steps you should use same names in mysql service.

2- MySQL service:

.....
  mysql:
    container_name: mysql
    volumes:
      - ./docker/mysql/data:/var/lib/mysql
      - ./docker/mysql/configs/my.cnf:/etc/mysql/my.cnf
    ports:
      - 3306:3306
    image: mysql:5.6
    restart: always
    env_file:
      - .env
1..... 2 mysql: 3 container_name: mysql 4 volumes: 5 - ./docker/mysql/data:/var/lib/mysql 6 - ./docker/mysql/configs/my.cnf:/etc/mysql/my.cnf 7 ports: 8 - 3306:3306 9 image: mysql:5.6 10 restart: always 11 env_file: 12 - .env

I mounted two volumes. One is data folder to store all data files of mysql and second is my.cnf file so that I can easily change mysql settings in future if needed without going inside container.

Next you need to expose 3306 port and map it with your local 3306 port.

Just copy paste following my.cnf file:

# Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 2 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA

#
# The MySQL  Server configuration file.
#
# For explanations see
# http://dev.mysql.com/doc/mysql/en/server-system-variables.html

[mysqld]
pid-file        = /var/run/mysqld/mysqld.pid
socket          = /var/run/mysqld/mysqld.sock
datadir         = /var/lib/mysql
secure-file-priv= NULL
# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links=0

# Custom config should go here
!includedir /etc/mysql/conf.d/
innodb_buffer_pool_size = 20M
1# Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. 2# 3# This program is free software; you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published by 5# the Free Software Foundation; version 2 of the License. 6# 7# This program is distributed in the hope that it will be useful, 8# but WITHOUT ANY WARRANTY; without even the implied warranty of 9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10# GNU General Public License for more details. 11# 12# You should have received a copy of the GNU General Public License 13# along with this program; if not, write to the Free Software 14# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 15 16# 17# The MySQL Server configuration file. 18# 19# For explanations see 20# http://dev.mysql.com/doc/mysql/en/server-system-variables.html 21 22[mysqld] 23pid-file = /var/run/mysqld/mysqld.pid 24socket = /var/run/mysqld/mysqld.sock 25datadir = /var/lib/mysql 26secure-file-priv= NULL 27# Disabling symbolic-links is recommended to prevent assorted security risks 28symbolic-links=0 29 30# Custom config should go here 31!includedir /etc/mysql/conf.d/ 32innodb_buffer_pool_size = 20M 33

3- nginx Service

  nginx:
    image: linuxserver/letsencrypt
    ports:
      - 80:80
      - 443:443
    volumes:
      - ./docker/nginx/config:/config
      - ./docker/nginx/nginx.conf:/config/nginx/site-confs/default
      - ./docker/nginx/ssl.conf:/config/nginx/ssl.conf
    container_name: nginx
    restart: unless-stopped
    environment: 
      - PUID=1000
      - PGID=1000
      - TZ=Europe/London
      - URL=yourdomainname.com
      - SUBDOMAINS=www,
      - VALIDATION=http
      - STAGING=false #optional
1 nginx: 2 image: linuxserver/letsencrypt 3 ports: 4 - 80:80 5 - 443:443 6 volumes: 7 - ./docker/nginx/config:/config 8 - ./docker/nginx/nginx.conf:/config/nginx/site-confs/default 9 - ./docker/nginx/ssl.conf:/config/nginx/ssl.conf 10 container_name: nginx 11 restart: unless-stopped 12 environment: 13 - PUID=1000 14 - PGID=1000 15 - TZ=Europe/London 16 - URL=yourdomainname.com 17 - SUBDOMAINS=www, 18 - VALIDATION=http 19 - STAGING=false #optional

Here I exposed port 80 and 443 and mapped it with the local port 80 and 443 respectively to receive traffic directly.

Then I mounted the volumes pass my configuration and persist the configurations and certificate related files.
In the property SUBDOMAINS you can add as many sub domains as you want by separating them with comma. You can check the official documentation of the linuxserver/letsencrypt image to see all available options. There are different validation methods. But the easiest method it by using http validation.

As the initial stage you should set the value of STAGING equal to true. Because for the first time you want to test if your all configurations are correct. Production certificate has rate limits. So you cannot create a lot of certificates within specific limit of time. So to avoid to reach the rate limit, you should first try staging that would then create fake certificate. After you see that fake certificate is live in your website then you are ready to change the value from staging=true to false to apply real production certificate.

Use following configuration file in your file structure:

nginx.conf

# redirect all traffic to https
server {
  listen 80;
  listen [::]:80;
  server_name _;
  return 301 https://$host$request_uri;
}

# main server block
server {
  listen 443 ssl http2 default_server;
  listen [::]:443 ssl http2 default_server;

  server_name _;

  # enable subfolder method reverse proxy confs
  include /config/nginx/proxy-confs/*.subfolder.conf;

  # all ssl related config moved to ssl.conf
  include /config/nginx/ssl.conf;

  client_max_body_size 0;

  location / {
    proxy_pass http://wordpress:80;
    proxy_redirect off;
    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-Host $host;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}
1# redirect all traffic to https 2server { 3 listen 80; 4 listen [::]:80; 5 server_name _; 6 return 301 https://$host$request_uri; 7} 8 9# main server block 10server { 11 listen 443 ssl http2 default_server; 12 listen [::]:443 ssl http2 default_server; 13 14 server_name _; 15 16 # enable subfolder method reverse proxy confs 17 include /config/nginx/proxy-confs/*.subfolder.conf; 18 19 # all ssl related config moved to ssl.conf 20 include /config/nginx/ssl.conf; 21 22 client_max_body_size 0; 23 24 location / { 25 proxy_pass http://wordpress:80; 26 proxy_redirect off; 27 proxy_set_header Host $host; 28 proxy_set_header X-Real-IP $remote_addr; 29 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 30 proxy_set_header X-Forwarded-Host $host; 31 proxy_set_header X-Forwarded-Server $host; 32 proxy_set_header X-Forwarded-Proto $scheme; 33 } 34}

There is only one important thing to note in above file that is proxy_pass value. I used proxy_pass http://wordpress:80;

In this value you need to provide the name of the service of wordpress that you mentioned in docker-compose.yaml

ssl.conf

## Version 2018/05/31 - Changelog: https://github.com/linuxserver/docker-etsencrypt/commits/master/root/defaults/ssl.conf

# session settings
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;

# Diffie-Hellman parameter for DHE cipher suites
ssl_dhparam /config/nginx/dhparams.pem;

# ssl certs
ssl_certificate /config/keys/letsencrypt/fullchain.pem;
ssl_certificate_key /config/keys/letsencrypt/privkey.pem;

# protocols
ssl_protocols TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';

# HSTS, remove # from the line below to enable HSTS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;

# Optional additional headers
#add_header Content-Security-Policy "upgrade-insecure-requests";
#add_header X-Frame-Options "SAMEORIGIN" always;
#add_header X-XSS-Protection "1; mode=block" always;
#add_header X-Content-Type-Options "nosniff" always;
#add_header X-UA-Compatible "IE=Edge" always;
#add_header Cache-Control "no-transform" always;
#add_header Referrer-Policy "same-origin" always;
1## Version 2018/05/31 - Changelog: https://github.com/linuxserver/docker-etsencrypt/commits/master/root/defaults/ssl.conf 2 3# session settings 4ssl_session_timeout 1d; 5ssl_session_cache shared:SSL:50m; 6ssl_session_tickets off; 7 8# Diffie-Hellman parameter for DHE cipher suites 9ssl_dhparam /config/nginx/dhparams.pem; 10 11# ssl certs 12ssl_certificate /config/keys/letsencrypt/fullchain.pem; 13ssl_certificate_key /config/keys/letsencrypt/privkey.pem; 14 15# protocols 16ssl_protocols TLSv1.1 TLSv1.2; 17ssl_prefer_server_ciphers on; 18ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; 19 20# HSTS, remove # from the line below to enable HSTS 21add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; 22 23# OCSP Stapling 24ssl_stapling on; 25ssl_stapling_verify on; 26 27# Optional additional headers 28#add_header Content-Security-Policy "upgrade-insecure-requests"; 29#add_header X-Frame-Options "SAMEORIGIN" always; 30#add_header X-XSS-Protection "1; mode=block" always; 31#add_header X-Content-Type-Options "nosniff" always; 32#add_header X-UA-Compatible "IE=Edge" always; 33#add_header Cache-Control "no-transform" always; 34#add_header Referrer-Policy "same-origin" always;

Now our files are ready to run.

Before that you should create a droplet on digital ocean. After your droplet is ready you will get an ip address like this:

image-4.webp

Copy the ip address and change the A record of your domain name to set this ip address.

I linked my domain name server with digital ocean so I can change A record right from the digitalOcean domains section. But if you are managing domain name from other domain company then still procedure is same. let me show you screenshot of A record in digital ocean.

image-5-1024x655.webp

After A record is added then you are ready to continue.

First create folder /root/project/ in the droplet and copy all your files to the droplet. I used this command form the root of my project folder:

scp -r ./* root@www.yourdomainame.com:/root/project
1scp -r ./* root@www.yourdomainame.com:/root/project

You have to ssh into your container. I used this command to enter into this container but you can use password if this does not work for you:

ssh root@www.yourdomainname.com
1ssh root@www.yourdomainname.com

Now run this command:

docker-compose up
1docker-compose up

now wait and watch the progress.

image-6-1024x575.webp

image-7-1024x564.webp

Now open your domain name in the browser and you would see a warning message saying your connection is not private. This means that your fake certificate is installed.

image-9-1024x599.webp

As now your configurations worked fine so now you are ready to install production real ssl certificate. Before this you need to remove all existing certificate configurations from the docker/nginx/configs folder.

So run this command:

rm -r /root/project/docker/nginx/configs/*
1rm -r /root/project/docker/nginx/configs/*

run this command to shutdown all services:

docker-compose down
1docker-compose down

In docker-compose.yaml file change the value from STAGING=true to false

and then run again

docker-compose up
1docker-compose up

Now watch the progress and if you see congratulations message like before then you can refresh your page to see if ssl certificate is installed. It should show lock icon with green message:

image-10.webp

As now everything working perfectly so now stop server by pressing command + c keys on your keyboard in terminal.

And then run services in detached mode by passing -d flag so that i can keep running forever in the background without any need to keep terminal open. you can then close terminal after running this command:

docker-compose up -d
1docker-compose up -d

Github source code: https://bit.ly/2MoaBWf

Youtube Video Link: https://www.youtube.com/watch?v=lbXc6mKh7U0&t=587s