In this tutorial we will deploy a 2 Node Docker Swarm and Deploy Traefik with SSL for our Reverse Proxy and Portainer for our Docker Management User Interface.
At the end of this tutorial you will see how easy it is to deploy Traefik and get all your web services on HTTPS with the help of Letsencrypt.
Install Docker:
Install Docker on both Nodes, as instructed from the official documentation.
Note: Always install Docker the way it's intended to by following their documentation, and not from untrusted / unofficial user scripts as those scripts can get outdated or even modified
Following their documentation, I ran the following to install docker on both nodes (Ubuntu in my case)
$ sudo apt update && sudo apt-get upgrade -y
$ sudo apt remove docker docker-engine -y
$ sudo apt install linux-image-extra-virtual -y
$ sudo apt install apt-transport-https ca-certificates curl software-properties-common -y
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
$ sudo apt update
$ sudo apt install docker-ce -y
Initialize the Swarm
Initialize Swarm on Manager (node-1):
$ docker swarm init --advertise-addr ens3
Swarm initialized: current node (jhs46c7mv0vl86v488joqazpd) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-3kgazh7s0aebjgov5tw0s85d0oz1wu4whefibiszaiuij7f7ub-3ocy5sathgputnxzpjacfypip 10.163.68.18:2377
Join Worker Node to the Swarm (node-2):
$ docker swarm join --token SWMTKN-1-3kgazh7s0aebjgov5tw0s85d0oz1wu4whefibiszaiuij7f7ub-3ocy5sathgputnxzpjacfypip 10.163.68.18:2377
This node joined a swarm as a worker.
List nodes from the Manager (node-1):
$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
jhs46c7mv0vl86v488joqazpd * docker1 Ready Active Leader 18.09.7
3bzwcuokvfi7w3gitfturzw93 docker2 Ready Active 18.09.7
DNS
Setup a A Record to the Manager IP, in my case:
meikel.rbkr.xyz
->185.136.234.52
Setup a Wildcard Record with the value of CNAME to the previous record:
*.meikel.rbkr.xyz
->meikel.rbkr.xyz
Testing:
$ dig A meikel.rbkr.xyz +short
185.136.234.52
$ dig CNAME test.meikel.rbkr.xyz +short
meikel.rbkr.xyz.
Provision Traefik:
Edit: Traefik image tag has been updated to 1.7.13, super thanks to Damien from Traefik:
"You should not use traefik:latest, but traefik:1.7 instead (of even traefik:1.7.13), to avoid breaking your setup when traefik v2.0 will be released"
Create the compose file for traefik as docker-compose.traefik.yml
, it's important to note that we will be storing our htpasswd
file as a config, and map the config to the path where we will be instructing traefik where to find the basic auth configuration:
version: '3.7'
services:
traefik:
image: traefik:1.7.13
ports:
- target: 80
published: 80
mode: host
- target: 443
published: 443
mode: host
command: >
--api
--acme
--acme.storage=/certs/acme.json
--acme.entryPoint=https
--acme.httpChallenge.entryPoint=http
--acme.onHostRule=true
--acme.onDemand=false
--acme.acmelogging=true
--acme.email=${EMAIL:-root@localhost}
--docker
--docker.swarmMode
--docker.domain=${DOMAIN:-localhost}
--docker.watch
--defaultentrypoints=http,https
--entrypoints='Name:http Address::80'
--entrypoints='Name:https Address::443 TLS'
--logLevel=INFO
--accessLog
--metrics
--metrics.prometheus
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- traefik_certs:/certs
configs:
- source: traefik_htpasswd
target: /etc/htpasswd
networks:
- public
deploy:
mode: replicated
replicas: 1
placement:
constraints:
- node.role == manager
update_config:
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
labels:
- "traefik.docker.network=public"
- "traefik.port=8080"
- "traefik.backend=traefik"
- "traefik.enable=true"
- "traefik.frontend.rule=Host:traefik.${DOMAIN:-localhost}"
- "traefik.frontend.auth.basic.usersFile=/etc/htpasswd"
- "traefik.frontend.headers.SSLRedirect=true"
- "traefik.frontend.entryPoints=http,https"
configs:
traefik_htpasswd:
file: ./htpasswd
networks:
public:
driver: overlay
name: public
volumes:
traefik_certs: {}
Install dependency to create the basic auth file that we will use to secure our traefik dashboard:
$ sudo apt install apache2-utils -y
Create the credentials file and save the file as htpasswd
which we are referencing in our docker-compose.traefik.yml
.
I will be using the username admin and password also admin:
$ htpasswd -c htpasswd admin
New password:
Re-type new password:
Adding password for user admin
Set the domain and reachable email as environment variable:
$ export DOMAIN=meikel.rbkr.xyz
$ export [email protected]
Deploy the Traefik stack:
$ docker stack deploy -c docker-compose.traefik.yml proxy
Creating network public
Creating config proxy_traefik_htpasswd
Creating service proxy_traefik
List the service:
$ docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
c4cm18zspces proxy_traefik replicated 1/1 traefik:latest
When accessing the Traefik UI on https://traefik.meikel.rbkr.xyz
you will be faced with a password challenge as we have secured our Traefik Dashboard with Basic Auth:
Upon successful logon, you will be presented with this awesome interface:
Just a double check that we are not exposing our API to the world:
$ curl https://traefik.meikel.rbkr.xyz/api
401 Unauthorized
And testing auth againts Traefik's API:
$ curl -su admin:admin https://traefik.meikel.rbkr.xyz/api | jq .docker.frontends
{
"frontend-Host-traefik-meikel-rbkr-xyz-0": {
"entryPoints": [
"http",
"https"
],
"backend": "backend-traefik",
"routes": {
"route-frontend-Host-traefik-meikel-rbkr-xyz-0": {
"rule": "Host:traefik.meikel.rbkr.xyz"
}
},
"passHostHeader": true,
"priority": 0,
"basicAuth": null,
"headers": {
"sslRedirect": true
},
"auth": {
"basic": {
"usersFile": "/etc/htpasswd"
}
}
}
}
Portainer
Create the compose for Portainer: docker-compose.portainer.yml
version: '3.7'
services:
agent:
image: portainer/agent
environment:
AGENT_CLUSTER_ADDR: tasks.agent
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/lib/docker/volumes:/var/lib/docker/volumes
networks:
- private
deploy:
mode: global
placement:
constraints:
- node.platform.os == linux
portainer:
image: portainer/portainer
command: -H tcp://tasks.agent:9001 --tlsskipverify
volumes:
- portainer-data:/data
networks:
- private
- public
deploy:
placement:
constraints:
- node.role == manager
labels:
- traefik.frontend.rule=Host:portainer.${DOMAIN}
- traefik.enable=true
- traefik.port=9000
- traefik.tags=public
- traefik.docker.network=public
- traefik.redirectorservice.frontend.entryPoints=http
- traefik.redirectorservice.frontend.redirect.entryPoint=https
- traefik.webservice.frontend.entryPoints=https
networks:
private:
driver: overlay
name: private
public:
external: true
volumes:
portainer-data: {}
Make sure the domain environment variable is still set:
$ env | grep DOMAIN
DOMAIN=meikel.rbkr.xyz
Deploy the Portainer stack:
$ docker stack deploy -c docker-compose.portainer.yml portainer
Creating network private
Creating service portainer_agent
Creating service portainer_portainer
Check if all the containers has checked in for the respective services:
$ docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
wwu7alr6ysw0 portainer_agent global 2/2 portainer/agent:latest
09flw7vt80r7 portainer_portainer replicated 1/1 portainer/portainer:latest
c4cm18zspces proxy_traefik replicated 1/1 traefik:latest
Portainer should show up on the Traefik UI as a Frontend and Backend:
Accessing Portainer on https://portainer.meikel.rbkr.xyz
:
After setting up our admin user:
And having a look at our docker stacks:
Thank You
Thank you for checking out my post to see how easy it is to Deploy Traefik and Portainer on Docker Swarm.
If you would like to check out more of my content, check out my website at ruan.dev or follow me on Twitter @ruanbekker
Comments