host several sites in a single box with docker and traefik v2, https
03 Dec 2020
Last time I wrote about how simple is to host several sites with docker + traefik on a single node, on this article I’ll complement such information with https and automatic ssl certification renewal.
If you continue reading this you’ll need to get familiar with the previous post, since I’m building upon it. OK, ready?, let’s recapitulate:
Diagram and Folder Structure
Traefik will receive all requests and will send them to different containers depending the domain/subdomains, in the process it’ll provide ssl termination for our users and dockerized applications, those certifications will be auto-renewed every 2/3 months and won’t require any manual step, cool!
┬
├── multisite (traefik)
│ ├── docker-compose.yml
│ ├── docker-compose.ssl.yml => NEW FILE
│ ├── acme.json => NEW FILE
├── site1.com
│ ├── ...
│ ├── docker-compose.site1.yml
│ ├── docker-compose.site1.ssl.yml => NEW FILE
├── site2.com
├── ...
├── docker-compose.site2.yml
├── docker-compose.site1.ssl.yml => NEW FILE
As you noticed, new files were added, the idea is that we maintain the flexibility to either provision a http only or a http + https site.
pre-requisite, dns configuration
When working with http only there is no need to mv our code from our local environment, it’s easy to add some entries in /etc/hosts and call it a day, this time however is different, we need to upload our files into a box with a public ip address and verify that the dns routing is working as expected, that is, if we are going to hosts these sites at: 185.199.109.153, we need to make sure site1.com / site2.com resolve to 185.199.109.153
I won’t cover how to do that because it depends on your DNS Registrar, for reference I’m using RackNerd and DNSPod as my Linux / DNS servers.
Why do we need to prepare our setup like this before starting?, it has to do with Let’s Encrypt, the Certification Authority we’re going to depend on, this CA generates challenges to verify that we are the owners of the referenced domain/subdomain, fortunately that happens automatically so we don’t need to do anything besides making sure that Let’s Encrypt can communicate with our domains.
multisite
Remember that starting here all changes are located in a remote public machine. I’ll start by creating a copy of docker-compose.yml, that would make easier for us to track the ssl changes:
$ cp docker-compose.yml docker-compose.ssl.yml
multisite/docker-compose.ssl.yml.patch:
--- docker-compose.ssl.yml 2020-12-03 10:02:48.186590271 -0600 +++ docker-compose.ssl.changes.yml 2020-12-03 10:03:30.940486004 -0600 @@ -9,11 +9,21 @@ - "--providers.docker.exposedbydefault=false" - "--providers.docker.network=traefik_global" #- "--log.level=DEBUG" + - "--entrypoints.http.address=:80" + - "--entrypoints.https.address=:443" + - "--certificatesresolvers.myresolver.acme.email=your-personal@email.tld" + - "--certificatesresolvers.myresolver.acme.storage=/acme.json" + - "--certificatesresolvers.myresolver.acme.tlschallenge=true" ports: - "80:80" #reverse proxy => input to all containerized services + - "443:443" #reverse ssl proxy - "8080:8080" #traefik dashboard/api volumes: - /var/run/docker.sock:/var/run/docker.sock + # Run this command in the host machine before launching traefik: + # $ touch acme.json && chmod 600 acme.json + - ${PWD}/acme.json:/acme.json networks: - traefik
Now our original file is more verbose, nevertheless every option is there for a reason.
By default traefik only opens the http port (80), so if we want to allow both, http/https, we need to be more specific:
+ - "--entrypoints.http.address=:80" + - "--entrypoints.https.address=:443"
We also need to select which certification resolver we’re going to use, on this case, Let’s encrypt, we specify that by filling the acme fields.
acme.email can be any personal/business email. acme.storage is where our ssl certificates will be saved, it does need to exists but can be empty, if that is the case, traefik will override it with valid certs. acme.tlschallenge is the challenge type, there are other types, but I think this is the easiest.
+ - "--certificatesresolvers.myresolver.acme.email=your-personal@email.tld" + - "--certificatesresolvers.myresolver.acme.storage=/acme.json" + - "--certificatesresolvers.myresolver.acme.tlschallenge=true" ports: - "80:80" #reverse proxy => input to all containerized services + - "443:443" #reverse ssl proxy
Finally we’ll share the acme.json file between our host/container to avoid requesting new certificates each time we launch our traefik container.
+ # Run this command in the host machine before launching traefik: + # $ touch acme.json && chmod 600 acme.json + - ${PWD}/acme.json:/acme.json
As the comments suggest, this file needs to be created with specific permissions before running traefik.
$ touch acme.json && chmod 600 acme.json
Ok, that’s all on traefik side, let’s apply the patch:
$ patch -p0 < docker-compose.ssl.yml.patch
patching file docker-compose.ssl.yml
site1.com
Let’s copy and analyze what makes a new site https compatible:
$ cp docker-compose.site1.yml docker-compose.site1.ssl.yml
site1.com/docker-compose.site1.ssl.yml.patch:
--- docker-compose.site1.ssl.yml 2020-12-03 10:02:48.186590271 -0600 +++ docker-compose.site1.ssl.changes.yml 2020-12-03 10:03:30.940486004 -0600 @@ -28,7 +28,15 @@ - frontend labels: - "traefik.enable=true" - - "traefik.http.routers.site1_com.rule=Host(`site1.com`)" + + - "traefik.http.routers.http_site1_com.rule=Host(`site1.com`)" + - "traefik.http.routers.http_site1_com.entrypoints=http" + + - "traefik.http.routers.https_site1_com.rule=Host(`site1.com`)" + - "traefik.http.routers.https_site1_com.entrypoints=https" + - "traefik.http.routers.https_site1_com.tls=true" + - "traefik.http.routers.https_site1_com.tls.certresolver=myresolver" + - "traefik.http.services.site1_com.loadbalancer.server.port=80" app:
I don’t know about you, but for me the syntax is confusing, happily this only needs to be setup once and then can be reused in other domains/subdomains by changing only some words. Also, the ssl endpoint is transparent, our application doesn’t need to be aware of it, that’s great and IMO overrides the verbose configuration.
As you noticed, the site1_com rules were split in two, http_site1_com and https_site1_com, this is because each route needs to define a Host and an entrypoint (port), repetitive right?, in the https route we enable tls and point to our custom resolver myresolver which if we recall uses let’s encrypt. There is also one more detail:
- "traefik.http.services.site1_com.loadbalancer.server.port=80"
The service element redirects traefik routes to our app’s 80 port, as each route defines a domain and entrypoint, that means it affects the domain as a whole and therefore it’s kept as site1_com , @.@!
This configuration leaves an important use case that each year is more common, forcing users to use https over http. Since I personally do not agree with such IMO abusive behavior, I skipped it on purpose, however if you’re interested you can use a middleware to configure that.
$ patch -p0 < docker-compose.site1.yml.patch
patching file docker-compose.site1.yml
site2.com
The second site should be easier to review:
$ cp docker-compose.site2.yml docker-compose.site2.ssl.yml
site2.com/docker-compose.site2.ssl.yml.patch:
--- docker-compose.site2.ssl.yml 2020-12-03 10:02:48.186590271 -0600 +++ docker-compose.site2.ssl.changes.yml 2020-12-03 10:03:30.940486004 -0600 @@ -26,7 +26,15 @@ - frontend labels: - "traefik.enable=true" - - "traefik.http.routers.site2_com.rule=Host(`site2.com`)" + + - "traefik.http.routers.http_site2_com.rule=Host(`site2.com`)" + - "traefik.http.routers.http_site2_com.entrypoints=http" + + - "traefik.http.routers.https_site2_com.rule=Host(`site2.com`)" + - "traefik.http.routers.https_site2_com.entrypoints=https" + - "traefik.http.routers.https_site2_com.tls=true" + - "traefik.http.routers.https_site2_com.tls.certresolver=myresolver" + - "traefik.http.services.site2_com.loadbalancer.server.port=80"
Everything is the same, the only difference is that site1 was replaced with site2
$ patch -p0 < docker-compose.site2.yml.patch
patching file docker-compose.site2.yml
docker-compose up
If you’ve followed everything up until this point, congratulations!, technology is great but also tends to be harder to grasp as more elements are incorporated, let’s end this tutorial once for all so we can continue with our lives:
$ cd multisite/ && docker-compose -f docker-compose.ssl.yml up -d
$ cd site1.com/ && docker-compose -f docker-compose.site1.ssl.yml up -d
$ cd site2.com/ && docker-compose -f docker-compose.site2.ssl.yml up -d
That’s it!, a kind of simple setup with ssl certs that is only limited by the amount of RAM/CPU in your machine:
$ curl https://site1.com
hello world from 7b7d6302-e162-3806-9595-17f854dd5b98
$ curl https://site2.com; echo
{"Greetings": "Hello World!"}
Happy hacking!