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:

image

Upon successful logon, you will be presented with this awesome interface:

image

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:

image

Accessing Portainer on https://portainer.meikel.rbkr.xyz:

image

After setting up our admin user:

image

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

Say Thanks!