edit - discovered caddy, seems simpler, here is its guide.
requirements
- have docker running somewhere
- have a domain
example.com
- use cloudflare to manage DNS of the domain
- have 80/443 ports open
chapters
- traefik routing to docker containers
- traefik routing to a local IP addresses
- middlewares
- let's encrypt certificate HTTP challenge
- let's encrypt certificate DNS challenge
- redirect HTTP traffic to HTTPS
-
create a new docker network
docker network create traefik_net
.
Traefik and the containers need to be on the same network. Compose creates one automatically, but that fact is hidden and there is potential for a fuck up later on. Better to just create own network and set it as default in every compose file.extra info: use
docker network inspect traefik_net
to see containers connected to that network -
create traefik.yml
This file contains so called static traefik configuration.
In this basic example there are just few self-explanatory settings.
Since exposedbydefault is set to false, a label"traefik.enable=true"
will be needed for containers that should be routed by traefik.
This file will be passed to a docker container using bind mount, this will be done when we get to docker-compose.yml for traefik.traefik.yml
## STATIC CONFIGURATION log: level: INFO api: insecure: true dashboard: true entryPoints: web: address: ":80" providers: docker: endpoint: "unix:///var/run/docker.sock" exposedByDefault: false
later on when traefik container is running, use command
docker logs traefik
and check if there is a notice stating:"Configuration loaded from file: /traefik.yml"
. You don't want to be the moron who makes changes to traefik.yml and it does nothing because the file is not actually being used. -
create
.env
file that will contain environmental variables.
Domain names, api keys, ip addresses, passwords,... whatever is specific for one case and different for another, all of that ideally goes here. These variables will be available for docker-compose when running thedocker-compose up
command.
This allows compose files to be moved from system to system more freely and changes are done to the .env file, so there's a smaller possibility for a fuckup of forgetting to change domain name in some host rule in a big ass compose file or some such..env
MY_DOMAIN=example.com DEFAULT_NETWORK=traefik_net
extra info:
Commanddocker-compose config
shows how the compose will look with the variables filled in.These variables are only filled in during the compose initial building of container. If an env variable should be available also inside the running container, it needs to be declared in the
environment
section of the compose file. -
create traefik-docker-compose.yml file.
It's a simple typical compose file.
Port 80 is mapped since we want traefik to be in charge of what comes on it - using it as an entrypoint. Port 8080 is for dashboard where traefik shows info. Mount of docker.sock is needed, so it can actually do its job interacting with docker. Mount oftraefik.yml
is what gives the static traefik configuration. The default network is set to the one created in the first step, as it will be set in all other compose files.traefik-docker-compose.yml
version: "3.7" services: traefik: image: "traefik:v2.1" container_name: "traefik" hostname: "traefik" ports: - "80:80" - "8080:8080" volumes: - "/var/run/docker.sock:/var/run/docker.sock:ro" - "./traefik.yml:/traefik.yml:ro" networks: default: external: name: $DEFAULT_NETWORK
-
run traefik-docker-compose.yml
docker-compose -f traefik-docker-compose.yml up -d
will start the traefik container.traefik is running, you can check it at the ip:8080 where you get the dashboard.
Can also check out logs withdocker logs traefik
.extra info:
Typically you see guides having just a single compose file calleddocker-compose.yml
with several services/containers in it. Then it's justdocker-compose up -d
to start it all. You don't even need to bother defining networks when it is all one compose. But this time I prefer small and separate steps when learning new shit. So that's why going with custom named docker-compose files as it allows easier separation.extra info2:
What you can also see in tutorials is no mention of traefik.yml and stuff is just passed from docker-compose using traefik's commands or labels.
like this:command: --api.insecure=true --providers.docker
But that way compose files look bit more messy and you still can't do everything from there, you still sometimes need that damn traefik.yml.
So... for now, going with nicely structured readable traefik.yml -
add labels to containers that traefik should route.
Here are examples of whoami, nginx, apache, portainer.- "traefik.enable=true"
enables traefik
- "traefik.http.routers.whoami.entrypoints=web"
defines router named
whoami
that listens on entrypoint web(port 80)- "traefik.http.routers.whoami.rule=Host(
whoami.$MY_DOMAIN
)"defines a rule for this
whoami
router, specifically that when url equalswhoami.example.com
(the domain name comes from the.env
file), that it means for router to do its job and route it to a service.Nothing else is needed, traefik knows the rest from the fact that these labels are coming from context of a docker container.
whoami-docker-compose.yml
version: "3.7" services: whoami: image: "containous/whoami" container_name: "whoami" hostname: "whoami" labels: - "traefik.enable=true" - "traefik.http.routers.whoami.entrypoints=web" - "traefik.http.routers.whoami.rule=Host(`whoami.$MY_DOMAIN`)" networks: default: external: name: $DEFAULT_NETWORK
nginx-docker-compose.yml
version: "3.7" services: nginx: image: nginx:latest container_name: nginx hostname: nginx labels: - "traefik.enable=true" - "traefik.http.routers.nginx.entrypoints=web" - "traefik.http.routers.nginx.rule=Host(`nginx.$MY_DOMAIN`)" networks: default: external: name: $DEFAULT_NETWORK
apache-docker-compose.yml
version: "3.7" services: apache: image: httpd:latest container_name: apache hostname: apache labels: - "traefik.enable=true" - "traefik.http.routers.apache.entrypoints=web" - "traefik.http.routers.apache.rule=Host(`apache.$MY_DOMAIN`)" networks: default: external: name: $DEFAULT_NETWORK
portainer-docker-compose.yml
version: "3.7" services: portainer: image: portainer/portainer container_name: portainer hostname: portainer volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - portainer_data:/data labels: - "traefik.enable=true" - "traefik.http.routers.portainer.entrypoints=web" - "traefik.http.routers.portainer.rule=Host(`portainer.$MY_DOMAIN`)" networks: default: external: name: $DEFAULT_NETWORK volumes: portainer_data:
-
run the damn containers
ignore some orphans talk, it's cuz these compose files are in the same directory and compose uses parent directory name for name of compose projectdocker-compose -f whoami-docker-compose.yml up -d
docker-compose -f nginx-docker-compose.yml up -d
docker-compose -f apache-docker-compose.yml up -d
docker-compose -f portainer-docker-compose.yml up -d
extra info:
to stop all containers running:docker stop $(docker ps -q)
When url should aim at something other than a docker container.
-
define a file provider, add required routing and service
What is needed is a router that catches some url and route it to some IP.
Previous examples shown how to catch whatever url, on port 80, but no one told it what to do when something fits the rule. Traefik just knew since it was all done using labels in the context of a container and thanks to docker being set as a provider intraefik.yml
.
For this "sending traffic at some IP" a traefik service is needed, and to define traefik service a new provider is required, a file provider - just a fucking stupid file that tells traefik what to do.
Somewhat common is to settraefik.yml
itself as a file provider so thats what will be done.
Under providers theres a newfile
section andtraefik.yml
itself is set.
Then the dynamic configuration stuff is added.
A router namedroute-to-local-ip
with a simple subdomain hostname rule. What fits that rule, in this case exact urltest.example.com
, is send to the loadbalancer service which just routes it to a specific IP and specific port.traefik.yml
## STATIC CONFIGURATION log: level: INFO api: insecure: true dashboard: true entryPoints: web: address: ":80" providers: docker: endpoint: "unix:///var/run/docker.sock" exposedByDefault: false file: filename: "traefik.yml" ## DYNAMIC CONFIGURATION http: routers: route-to-local-ip: rule: "Host(`test.example.com`)" service: route-to-local-ip-service priority: 1000 entryPoints: - web services: route-to-local-ip-service: loadBalancer: servers: - url: "http://10.0.19.5:80"
Priority of the router is set to 1000, a very high value, beating any possible other routers.
extra info:
Unfortunately the.env
variables are not working here, otherwise domain name in host rule and that IP would come from variables. So heads up that you will definitely forget to change these. -
run traefik-docker-compose and test if it works
docker-compose -f traefik-docker-compose.yml up -d
Example of an authentication middleware for any container.
-
create a new file -
users_credentials
containing username:passwords pairs, htpasswd style
Bellow example has passwordkrakatoa
set to all 3 accountsusers_credentials
me:$apr1$L0RIz/oA$Fr7c.2.6R1JXIhCiUI1JF0 admin:$apr1$ELgBQZx3$BFx7a9RIxh1Z0kiJG0juE/ bastard:$apr1$gvhkVK.x$5rxoW.wkw1inm9ZIfB0zs1
-
mount users_credentials in traefik-docker-compose.yml
traefik-docker-compose.yml
version: "3.7" services: traefik: image: "traefik:v2.1" container_name: "traefik" hostname: "traefik" ports: - "80:80" - "8080:8080" volumes: - "/var/run/docker.sock:/var/run/docker.sock:ro" - "./traefik.yml:/traefik.yml:ro" - "./users_credentials:/users_credentials:ro" networks: default: external: name: $DEFAULT_NETWORK
-
add two labels to any container that should have authentication
-
The first label attaches new middleware called
auth-middleware
to an already existingwhoami
router. -
The second label gives this middleware type basicauth, and tells it where is the file it should use to authenticate users.
No need to mount the
users_credentials
here, it's traefik that needs that file and these labels are a way to pass info to traefik, what it should do in context of containers.
whoami-docker-compose.yml
version: "3.7" services: whoami: image: "containous/whoami" container_name: "whoami" hostname: "whoami" labels: - "traefik.enable=true" - "traefik.http.routers.whoami.entrypoints=web" - "traefik.http.routers.whoami.rule=Host(`whoami.$MY_DOMAIN`)" - "traefik.http.routers.whoami.middlewares=auth-middleware" - "traefik.http.middlewares.auth-middleware.basicauth.usersfile=/users_credentials" networks: default: external: name: $DEFAULT_NETWORK
nginx-docker-compose.yml
version: "3.7" services: nginx: image: nginx:latest container_name: nginx hostname: nginx labels: - "traefik.enable=true" - "traefik.http.routers.nginx.entrypoints=web" - "traefik.http.routers.nginx.rule=Host(`nginx.$MY_DOMAIN`)" - "traefik.http.routers.nginx.middlewares=auth-middleware" - "traefik.http.middlewares.auth-middleware.basicauth.usersfile=/users_credentials" networks: default: external: name: $DEFAULT_NETWORK
-
-
run the damn containers and now there is login and password needed
docker-compose -f traefik-docker-compose.yml up -d
docker-compose -f whoami-docker-compose.yml up -d
docker-compose -f nginx-docker-compose.yml up -d
My understanding of the process, simplified.
LE
- Let's Encrypt. A service that gives out free certificates
Certificate
- a cryptographic key stored in a file on the server,
allows encrypted communication and confirms the identity
ACME
- a protocol(precisely agreed way of communication) to negotiate certificates
from LE. It is part of traefik.
DNS
- servers on the internet, translate domain names in to ip address
Traefik uses ACME to ask LE for a certificate for a specific domain, like example.com
.
LE answers with some random generated text that traefik puts at a specific place on the server.
LE then asks DNS internet servers for example.com
and that points to some IP address.
LE looks at that IP address through ports 80/443 for the file containing that random text.
If it's there then this proves that whoever asked for the certificate controls both the server and the domain, since it showed control over DNS records. Certificate is given and is valid for 3 months, traefik will automatically try to renew when less than 30 days is remaining.
Now how to actually get it done.
-
create an empty acme.json file with 600 permissions
This file will store the certificates and all the info about them.
touch acme.json && chmod 600 acme.json
-
add 443 entrypoint and certificate resolver to traefik.yml
In entrypoint section new entrypoint is added called websecure, port 443
certificatesResolvers is a configuration section that tells traefik how to use acme resolver to get certificates.
certificatesResolvers: lets-encr: acme: #caServer: https://acme-staging-v02.api.letsencrypt.org/directory storage: acme.json email: whatever@gmail.com httpChallenge: entryPoint: web
- the name of the resolver is
lets-encr
and uses acme - commented out staging caServer makes LE issue a staging certificate, it is an invalid certificate and wont give green lock but has no limitations, so it's good for testing. If it's working it will say issued by let's encrypt.
- Storage tells where to store given certificates -
acme.json
- The email is where LE sends notification about certificates expiring
- httpChallenge is given an entrypoint, so acme does http challenge over port 80
That is all that is needed for acme
traefik.yml
## STATIC CONFIGURATION log: level: INFO api: insecure: true dashboard: true entryPoints: web: address: ":80" websecure: address: ":443" providers: docker: endpoint: "unix:///var/run/docker.sock" exposedByDefault: false certificatesResolvers: lets-encr: acme: #caServer: https://acme-staging-v02.api.letsencrypt.org/directory storage: acme.json email: whatever@gmail.com httpChallenge: entryPoint: web
- the name of the resolver is
-
expose/map port 443 and mount acme.json in traefik-docker-compose.yml
Notice that acme.json is not :ro - read only
traefik-docker-compose.yml
version: "3.7" services: traefik: image: "traefik:v2.1" container_name: "traefik" hostname: "traefik" env_file: - .env ports: - "80:80" - "443:443" - "8080:8080" volumes: - "/var/run/docker.sock:/var/run/docker.sock:ro" - "./traefik.yml:/traefik.yml:ro" - "./acme.json:/acme.json" networks: default: external: name: $DEFAULT_NETWORK
-
add required labels to containers
compared to just plain http from first chapter, it is just changing router's entryPoint fromweb
towebsecure
and assigning certificate resolver namedlets-encr
to the existing routerwhoami-docker-compose.yml
version: "3.7" services: whoami: image: "containous/whoami" container_name: "whoami" hostname: "whoami" labels: - "traefik.enable=true" - "traefik.http.routers.whoami.entrypoints=websecure" - "traefik.http.routers.whoami.rule=Host(`whoami.$MY_DOMAIN`)" - "traefik.http.routers.whoami.tls.certresolver=lets-encr" networks: default: external: name: $DEFAULT_NETWORK
nginx-docker-compose.yml
version: "3.7" services: nginx: image: nginx:latest container_name: nginx hostname: nginx labels: - "traefik.enable=true" - "traefik.http.routers.nginx.entrypoints=websecure" - "traefik.http.routers.nginx.rule=Host(`nginx.$MY_DOMAIN`)" - "traefik.http.routers.nginx.tls.certresolver=lets-encr" networks: default: external: name: $DEFAULT_NETWORK
-
run the damn containers
give it a minute
containers will now work only over https and have the greenlockextra info:
check content of acme.json
delete acme.json if you want fresh start
My understanding of the process, simplified.
LE
- Let's Encrypt. A service that gives out free certificates
Certificate
- a cryptographic key stored in a file on the server,
allows encrypted communication and confirms the identity
ACME
- a protocol(precisely agreed way of communication) to negotiate certificates
from LE. It is part of traefik.
DNS
- servers on the internet, translate domain names in to ip address
Traefik uses ACME to ask LE for a certificate for a specific domain, like example.com
.
LE answers with some random generated text that traefik puts as a new DNS TXT record.
LE then checks example.com
DNS records to see if the text is there.
If it's there then this proves that whoever asked for the certificate controls the domain. Certificate is given and is valid for 3 months. Traefik will automatically try to renew when less than 30 days is remaining.
Benefit over httpChallenge is ability to have wild card certificates.
These are certificates that validate all subdomains *.example.com
Also no ports are needed to be open.
But traefik needs to be able to make these automated changes to DNS records, so there needs to be support for this from whoever manages sites DNS. Thats why going with cloudflare.
Now how to actually get it done.
-
add type A DNS records for all planned subdomains
[whoami, nginx, *] are used example subdomains, each one should have A-record pointing at traefik IP
-
create an empty acme.json file with 600 permissions
touch acme.json && chmod 600 acme.json
-
add 443 entrypoint and certificate resolver to traefik.yml
In entrypoint section new entrypoint is added called websecure, port 443
certificatesResolvers is a configuration section that tells traefik how to use acme resolver to get certificates.
certificatesResolvers: lets-encr: acme: #caServer: https://acme-staging-v02.api.letsencrypt.org/directory email: whatever@gmail.com storage: acme.json dnsChallenge: provider: cloudflare resolvers: - "1.1.1.1:53" - "8.8.8.8:53"
- the name of the resolver is
lets-encr
and uses acme - commented out staging caServer makes LE issue a staging certificate, it is an invalid certificate and wont give green lock but has no limitations, if it's working it will say issued by let's encrypt.
- Storage tells where to store given certificates -
acme.json
- The email is where LE sends notification about certificates expiring
- dnsChallenge is specified with a provider, in this case cloudflare. Each provider needs differently named environment variable in the .env file, but thats later, here it just needs the name of the provider
- resolvers are IP of well known DNS servers to use during challenge
traefik.yml
## STATIC CONFIGURATION log: level: INFO api: insecure: true dashboard: true entryPoints: web: address: ":80" websecure: address: ":443" providers: docker: endpoint: "unix:///var/run/docker.sock" exposedByDefault: false certificatesResolvers: lets-encr: acme: #caServer: https://acme-staging-v02.api.letsencrypt.org/directory email: whatever@gmail.com storage: acme.json dnsChallenge: provider: cloudflare resolvers: - "1.1.1.1:53" - "8.8.8.8:53"
- the name of the resolver is
-
to the
.env
file add required variables
We know what variables to add based on the list of supported providers.
For cloudflare variables areCF_API_EMAIL
- cloudflare loginCF_API_KEY
- global api key
.env
MY_DOMAIN=example.com DEFAULT_NETWORK=traefik_net CF_API_EMAIL=whateverbastard@gmail.com CF_API_KEY=8d08c87dadb0f8f0e63efe84fb115b62e1abc
-
expose/map port 443 and mount acme.json in traefik-docker-compose.yml
Notice that acme.json is not :ro - read only
traefik-docker-compose.yml
version: "3.7" services: traefik: image: "traefik:v2.1" container_name: "traefik" hostname: "traefik" env_file: - .env ports: - "80:80" - "443:443" - "8080:8080" volumes: - "/var/run/docker.sock:/var/run/docker.sock:ro" - "./traefik.yml:/traefik.yml:ro" - "./acme.json:/acme.json" networks: default: external: name: $DEFAULT_NETWORK
-
add required labels to containers
compared to just plain http from the first chapter- router's entryPoint is switched from
web
towebsecure
- certificate resolver named
lets-encr
assigned to the router - a label defining main domain that will get the certificate,
in this it is whoami.example.com, domain name pulled from
.env
file
whoami-docker-compose.yml
version: "3.7" services: whoami: image: "containous/whoami" container_name: "whoami" hostname: "whoami" labels: - "traefik.enable=true" - "traefik.http.routers.whoami.entrypoints=websecure" - "traefik.http.routers.whoami.rule=Host(`whoami.$MY_DOMAIN`)" - "traefik.http.routers.whoami.tls.certresolver=lets-encr" - "traefik.http.routers.whoami.tls.domains[0].main=whoami.$MY_DOMAIN" networks: default: external: name: $DEFAULT_NETWORK
nginx-docker-compose.yml
version: "3.7" services: nginx: image: nginx:latest container_name: nginx hostname: nginx labels: - "traefik.enable=true" - "traefik.http.routers.nginx.entrypoints=websecure" - "traefik.http.routers.nginx.rule=Host(`nginx.$MY_DOMAIN`)" - "traefik.http.routers.nginx.tls.certresolver=lets-encr" - "traefik.http.routers.nginx.tls.domains[0].main=nginx.$MY_DOMAIN" networks: default: external: name: $DEFAULT_NETWORK
- router's entryPoint is switched from
-
run the damn containers
docker-compose -f traefik-docker-compose.yml up -d
docker-compose -f whoami-docker-compose.yml up -d
docker-compose -f nginx-docker-compose.yml up -d
-
Fuck that, the whole point of DNS challenge is to get wildcards!
fair enough
so for wildcard these labels go in to traefik compose.- same
lets-encr
certificateresolver is used as before, the one defined in traefik.yml - the wildcard for subdomains(*.example.com) is set as the main domain to get certificate for
- the naked domain(just plain example.com) is set as sans(Subject Alternative Name)
again, you do need
*.example.com
andexample.com
set in your DNS control panel as A-record pointing to IP of traefiktraefik-docker-compose.yml
version: "3.7" services: traefik: image: "traefik:v2.1" container_name: "traefik" hostname: "traefik" env_file: - .env ports: - "80:80" - "443:443" - "8080:8080" volumes: - "/var/run/docker.sock:/var/run/docker.sock:ro" - "./traefik.yml:/traefik.yml:ro" - "./acme.json:/acme.json" labels: - "traefik.enable=true" - "traefik.http.routers.traefik.tls.certresolver=lets-encr" - "traefik.http.routers.traefik.tls.domains[0].main=*.$MY_DOMAIN" - "traefik.http.routers.traefik.tls.domains[0].sans=$MY_DOMAIN" networks: default: external: name: $DEFAULT_NETWORK
Now if a container wants to be accessible as a subdomain, it just needs a regular router that has rule for the url, be on 443 port entrypoint, and use the same
lets-encr
certificate resolverwhoami-docker-compose.yml
version: "3.7" services: whoami: image: "containous/whoami" container_name: "whoami" hostname: "whoami" labels: - "traefik.enable=true" - "traefik.http.routers.whoami.entrypoints=websecure" - "traefik.http.routers.whoami.rule=Host(`whoami.$MY_DOMAIN`)" - "traefik.http.routers.whoami.tls.certresolver=lets-encr" networks: default: external: name: $DEFAULT_NETWORK
nginx-docker-compose.yml
version: "3.7" services: nginx: image: nginx:latest container_name: nginx hostname: nginx labels: - "traefik.enable=true" - "traefik.http.routers.nginx.entrypoints=websecure" - "traefik.http.routers.nginx.rule=Host(`nginx.$MY_DOMAIN`)" - "traefik.http.routers.nginx.tls.certresolver=lets-encr" networks: default: external: name: $DEFAULT_NETWORK
Here is apache but this time run on the naked domain
example.com
apache-docker-compose.yml
version: "3.7" services: apache: image: httpd:latest container_name: apache hostname: apache labels: - "traefik.enable=true" - "traefik.http.routers.apache.entrypoints=websecure" - "traefik.http.routers.apache.rule=Host(`$MY_DOMAIN`)" - "traefik.http.routers.apache.tls.certresolver=lets-encr" networks: default: external: name: $DEFAULT_NETWORK
- same
http stops working with https setup, better to redirect http(80) to https(443).
Traefik has special type of middleware for this purpose - redirectscheme.
There are several places where this redirect can be declared,
in traefik.yml
, in the dynamic section when traefik.yml
itself is set as a file provider.
Or using labels in any running container, this example does it in traefik compose.
-
add new router and a redirect scheme using labels in traefik compose
- "traefik.enable=true"
enables traefik on this traefik container, not that there is need of the typical routing to a service here, but other labels would not work without this
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
creates new middleware called
redirect-to-https
, type "redirectscheme" and assigns it schemehttps
.- "traefik.http.routers.redirect-https.rule=hostregexp(`{host:.+}`)"
creates new router called
redirect-https
, with a regex rule that catches any and every incoming request- "traefik.http.routers.redirect-https.entrypoints=web"
declares on which entrypoint this router listens - web(port 80)
- "traefik.http.routers.redirect-https.middlewares=redirect-to-https"
assigns the freshly created redirectscheme middleware to this freshly created router.
So to sum it up, when a request comes at port 80, router that listens at that entrypoint looks at it. If it fits the rule, and it does because everything fits that rule, it goes to the next step. Ultimately it should get to a service, but if there is middleware declared, that middleware goes first, and since middleware is there, and it is some redirect scheme, it never reaches any service, it gets redirected using https scheme, which I guess is stating - go for port 443.
Here is the full traefik compose, with dns challenge labels from previous chapter included:
traefik-docker-compose.yml
version: "3.7" services: traefik: image: "traefik:v2.1" container_name: "traefik" hostname: "traefik" env_file: - .env ports: - "80:80" - "443:443" - "8080:8080" volumes: - "/var/run/docker.sock:/var/run/docker.sock:ro" - "./traefik.yml:/traefik.yml:ro" - "./acme.json:/acme.json" labels: - "traefik.enable=true" ## DNS CHALLENGE - "traefik.http.routers.traefik.tls.certresolver=lets-encr" - "traefik.http.routers.traefik.tls.domains[0].main=*.$MY_DOMAIN" - "traefik.http.routers.traefik.tls.domains[0].sans=$MY_DOMAIN" ## HTTP REDIRECT - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" - "traefik.http.routers.redirect-https.rule=hostregexp(`{host:.+}`)" - "traefik.http.routers.redirect-https.entrypoints=web" - "traefik.http.routers.redirect-https.middlewares=redirect-to-https" networks: default: external: name: $DEFAULT_NETWORK
-
run the damn containers and now
http://whoami.example.com
is immediately changed tohttps://whoami.example.com