Docker For Local Web Development, Part 5 - HTTPS All The Things
Docker For Local Web Development, Part 5 - HTTPS All The Things
In this post
In this series
In this post
Introduction
Generating the certificate
Installing the certificate
The Nginx server configurations
Automating the process
Container to container traffic
Conclusion
Introduction
Since its inception by Netscape Communications back in 1994,
Hypertext Transfer Protocol Secure (HTTPS) has been spreading
over the Internet at an ever increasing rate, and now accounts for
more than 80% of global traffic (as of February 2020). This growth
in coverage has been particularly strong in the past few years,
catalysed by entities like the Internet Security Research Group –
the one behind the free certificate authority Let's Encrypt – and
companies like Google, whose Chrome browser has flagged HTTP
websites as insecure since 2018.
If you prefer, you can also directly checkout the part-5 branch,
which is the final result of today's article.
1 *
2 !.gitignore
These two lines mean that all of the files contained in that directory
except for .gitignore will be ignored by Git (this is a nicer
version of the .keep file you may sometimes encounter, which
aims to version an empty folder in Git).
1 FROM nginx:1.21-alpine
2
3 # Install packages
4 RUN apk --update --no-cache add openssl
All the tools necessary to generate our certificate are now in place
– we just need to add the corresponding Bash command and
function.
First, let's update our application menu, at the bottom of the demo
file:
Command line interface for the Docker-based web development environment demo.
Usage:
demo [options] [arguments]
Available commands:
artisan ................................... Run an Artisan command
build [image] ............................. Build all of the images or the spe
cert ...................................... Certificate management commands
generate .............................. Generate a new certificate
install ............................... Install the certificate
composer .................................. Run a Composer command
destroy ................................... Remove the entire Docker environme
down [-v] ................................. Stop and destroy all containers
Options:
-v .................... Destro
init ...................................... Initialise the Docker environment
logs [container] .......................... Display and tail the logs of all c
restart [container] ....................... Restart all containers or the spec
start ..................................... Start the containers
stop ...................................... Stop the containers
update .................................... Update the Docker environment
yarn ...................................... Run a Yarn command
To save us a trip later, I've also added the menu for the certificate
installation, even if we won't implement it just yet.
1 cert)
2 case "$2" in
3 generate)
4 cert_generate
5 ;;
6 install)
7 cert_install
8 ;;
9 *)
10 cat << EOF
11
12 Certificate management commands.
13
14 Usage:
15 demo cert <command>
16
17 Available commands:
18 generate .................................. Generate a new certificate
19 install ................................... Install the certificate
20
21 EOF
22 ;;
23 esac
24 ;;
$ demo
$ demo cert
The first line of the function simply gets rid of previously generated
certificates and keys that may still be in the certs directory. The
second line is quite long and a bit complicated, but essentially it
brings up a new, single-use container based on Nginx's image
( docker compose run --rm nginx ) and runs a bunch of
commands on it (that's the portion between the double quotes, after
sh -c ).
I won't go into the details of these, but the gist is they create a
wildcard self-signed certificate for *.demo.test as well as the
corresponding key. A self-signed certificate is a certificate that is not
signed by a certificate authority; in practice, you wouldn't use such
a certificate in production, but it is fine for a local setup.
docker-tutorial/
├── .docker/
│ ├── backend/
│ ├── mysql/
│ └── nginx/
│ ├── certs/
│ │ ├── .gitignore
│ │ ├── demo.test.crt
│ │ └── demo.test.key
│ ├── conf.d/
│ └── Dockerfile
├── src/
├── .env
├── .env.example
├── .gitignore
├── demo
└── docker-compose.yml
Using WSL?
Even if you run your project through WSL and the certificate
seemingly installs properly on your Linux distribution, you're
most likely still accessing the URL via a browser from
Windows. For it to recognise and accept the certificate, you will
need to copy the corresponding file from Linux to Windows and
proceed with the manual installation as described in the
aforementioned tutorial.
If the current system is Linux, the function will create a symbolic link
between the certificate and the /usr/local/share/ca-
certificates folder, and run update-ca-certificates so it is
taken it into account. Note that this code will only work for Debian-
based distributions – if you use a different one, you will need to
adapt the if condition accordingly, or add extra conditions to
cover more distributions.
Let's try to install the certificate (you can also run this on Windows,
but you'll be invited to install the certificate manually as mentioned
earlier):
If all went well, it should now appear in the list of certificates, like in
macOS' Keychain Access:
1 server {
2 listen 443 ssl http2;
3 listen [::]:443 ssl http2;
4 server_name frontend.demo.test;
5
6 ssl_certificate /etc/nginx/certs/demo.test.crt;
7 ssl_certificate_key /etc/nginx/certs/demo.test.key;
8
9 location / {
10 proxy_pass http://frontend:8080;
11 proxy_http_version 1.1;
12 proxy_set_header Upgrade $http_upgrade;
13 proxy_set_header Connection 'upgrade';
14 proxy_cache_bypass $http_upgrade;
15 proxy_set_header Host $host;
16 }
17 }
18
19 server {
20 listen 80;
21 listen [::]:80;
22 server_name frontend.demo.test;
23 return 301 https://$server_name$request_uri;
24 }
...
server: {
host: true,
hmr: {port: 443},
port: 8080,
watch: {
usePolling: true
}
}
...
Looks like we're ready for a test! Restart the containers for the
changes to take effect:
$ demo restart
We're encrypted!
Not working in your browser?
Open the demo file again and update the init function:
if [ ! -f .docker/nginx/certs/demo.test.crt ]; then
cert_generate
fi
$ ping frontend
The ping should respond with the frontend container's private IP,
which is the expected behaviour. Try pinging the frontend again,
this time using the domain name:
$ ping frontend.demo.test
We also get a response, but from localhost, which is not quite right:
we should get the same private IP address instead.
Let's run a few more tests, still from the backend container, but
using cURL commands:
$ curl frontend
Response:
$ curl frontend:8080
This command correctly returns the frontend's HTML code. Let's try
again, but this time using the domain name:
$ curl frontend.demo.test
Response:
$ curl frontend.demo.test:8080
Response:
Things are probably getting a bit confusing, so let's bring back our
diagram from earlier:
We can now proceed with the same tests as earlier, starting with
the ping:
$ curl frontend.demo.test
$ curl https://frontend.demo.test
Response:
$ curl -k https://frontend.demo.test
$ demo restart
Access the backend container once again, and install the new
certificate (you can ignore the warning):
$ curl https://frontend.demo.test
After installing the certificate on the local machine, the function will
now also do the same on the backend container.
I must confess that the example above isn't the most relevant,
as in practice I can't really think of any reason why the
backend would need to interact with the frontend in such a
way. Container-to-container communication is not a rare feat,
however: typical use cases comprise applications querying an
authentication server (think OAuth), or microservices
communicating through HTTP. I simply didn't want to make this
tutorial any longer by introducing another container.
In the next part, we will see how to expose a local container to the
Internet, which comes in handy when testing the integration of a
third-party service. Subscribe to email alerts below so you don't
miss it, or follow me on Twitter where I will share my posts as soon
as they are published.