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:
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:
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.
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.
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.
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:
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