Final Year Project: Part 2 - Setting up Traefik v2 with dockerized NodeJS API

Now we have a working setup with Node.js and Docker, lets setup the production server.

I wanted it to be easy to use for deployments, and the most autonomous and secured as possible with automatic SSL certs generation and load balancing.

The API uses MongoDB, everything runs in docker containers. I choose Traefik as reverse proxy because it provide native support for reverse-proxying docker containers, it works well with letsencrypt to request certificates automatically, and it support load balancing on docker containers. And also because I wanted to try an NGINX alternative for curiosity.

However, when there is PLENTY of information online about the version 1.7 of Traefik, there is not so much for version 2, and this v2 has many big breaking changes. And as of now, the official documentation is not easy to understand and many functionnalities are not documented.

Setting up Traefik in Docker

The final setup will look like that:

All docker containers.

All docker containers.

Quick explanation of how Traefik works

Traefik handles requests like this:

Traefik chain.

Traefik chain.

Here is the router used in this chain (note the rule part), with its TLS configuration (letsencrypt http method) and the middleware used (basicauth).

Traefik dashboard router.

Traefik dashboard router.

It means: “For a request with host=traefik.domain.com coming through port 443, we go through router traefik-secure@docker (because the host matched the router’s rule), then through any middleware used in this router (here a basicauth) and finally, to the service (here api@internal refers to traefik’s internal api, which is the web dashboard)

File structure

I like to use Docker-Compose to manage my persistent containers (containers I want to be able to restart easily).

To begin with a clear architecture, our traefik directory should look like this:

traefik/
├── acme.json
├── config
│   └── config.yml
├── docker-compose.yml
└── traefik.yml

Explanation:

  • acme.json will store all the SSL Certificates obtained by Traefik, we will mount it as a volume to keep them accross container restarts.
  • The config/ directory contains our dynamic configuration files (where we define manually some routes and rules “à la NGINX”). Traefik will watch this directory and automatically integrate any rules created in any files in it.
  • docker-compose.yml will be used to manage Traefik container.
  • traefik.yml is the static configuration file, read once at Traefik startup.

Traefik configuration

acme.json should be empty for the first start.

Starting with traefik.yml:

# This is the traefik dashboard, accessible through traefik.domain.com
api:
  dashboard: true
  debug: true

# Only using the usual :80 and :443 ports, but traefik v2 can also catch other ports like :8080 if you want !
entryPoints:
  http:
    address: ":80"
  https:
    address: ":443"

# Define where our services or middlewares can be found
providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
  file:
    directory: /config
    watch: true

log:
  level: "ERROR"
  filePath: "log/error.log"
  format: "json"

accessLog:
  filePath: "log/access.log"
  format: "json"

# Configuration for Letsencrypt cert resolver
certificatesResolvers:
  http:
    acme:
      email: yourmail@domain.com
      storage: acme.json
      httpChallenge:
        entryPoint: http

Now, our dynamic config file config/config.yml :

http:

  # Create our middlewares
  middlewares:
  	 # When used it redirects the client to https entryPoint
    https-redirect:
      redirectScheme:
        scheme: https
	 
	 # When used, applies some headers to the request
    default-headers:
      headers:
        frameDeny: true
        sslRedirect: true
        browserXssFilter: true
        contentTypeNosniff: true
        forceSTSHeader: true
        stsIncludeSubdomains: true
        stsPreload: true

    # When used, applies default-headers middleware
    secured:
      chain:
        middlewares:
        - default-headers

  # This is a catch-all http -> https redirect. With this, you can only define https router for your services, this will redirect users coming from http to your https router.
  # If you don't need it, delete everything under this line
  routers:
    https-redirouter:
      rule: HostRegexp(`{any:.*}`)
      middlewares: [https-redirect]
      service: dummy

  services:
    dummy:
      loadBalancer:
        servers:
          - url: localhost

And finally, the docker-compose.yml:

version: '3.5'

services:
  traefik:
    image: traefik:v2.0
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - web
    ports:
      - 80:80
      - 443:443
    volumes:
    	# Time sync with host
      - /etc/localtime:/etc/localtime:ro
       # Access to docker socket to monitor other containers
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik.yml:/traefik.yml:ro
      - ./acme.json:/acme.json
      - ./config:/config:ro
      - ./log:/log
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.traefik.entrypoints=http"
      - "traefik.http.routers.traefik.rule=Host(`traefik.domain.com`)"
      - "traefik.http.middlewares.traefik-auth.basicauth.users=admin:$apr1$Z3U0xxxxxxxxxxxxxxxxxRz1"
      - "traefik.http.middlewares.traefik-https-redirect.redirectscheme.scheme=https"
      - "traefik.http.routers.traefik.middlewares=traefik-https-redirect"
      - "traefik.http.routers.traefik-secure.entrypoints=https"
      - "traefik.http.routers.traefik-secure.rule=Host(`traefik.domain.com`)"
      - "traefik.http.routers.traefik-secure.middlewares=traefik-auth"
      - "traefik.http.routers.traefik-secure.tls=true"
      - "traefik.http.routers.traefik-secure.tls.certresolver=http"
      - "traefik.http.routers.traefik-secure.service=api@internal"

# Put traefik in an external docker network. 
# Other containers that need to be accessible will be declared in the same network
networks:
  web:
    external: true

⚠️ Sharing the docker socket with a docker container can be dangerous, because it means that this container can have root access to the host. The use of a proxy on this socket is recommended.

Note the labels section in this docker-compose. Traefik will read the labels of every container in its network (web), and apply the instructions as dynamic configuration.

So here, we are defining 2 routers: traefik for http and traefik-secure for https.

  • traefik uses a middleware for redirecting to https (defined here as well, it is traefik-https-redirect).
  • traefik-secure uses a basic-auth middleware with a user:password combination.

This configuration is the one we looked at earlier !

Starting Traefik container

Once the configuration is finished, just do:

docker-compose up -d

Log data can be found in log/ directory.

Reverse-Proxying to other containers

Attach traefik configuration to each container

Now that Traefik is up and running, lets use it to serve other containers, like our API.

I am using the same docker-compose file I talked about in part 1 of this series, you can find it here..

We need to connect it to web network, and apply the correct traefik configuration inside labels for each containers. We don’t want MongoDB container to be accessed by traefik, so it will be prohibited.

api-ecobol/docker-compose.yml

version: '3'
services:
  api-ecobol:
    env_file:
      - .env
    restart: on-failure
    build: .
    links:
      - mongo
    depends_on:
      - mongo
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.api-ecobol.entrypoints=http"
      - "traefik.http.routers.api-ecobol.rule=Host(`api.example.com`)"
      - "traefik.http.routers.api-ecobol.middlewares=https-redirect@file"
      - "traefik.http.routers.api-ecobol-secure.middlewares=secured@file"
      - "traefik.http.routers.api-ecobol-secure.entrypoints=https"
      - "traefik.http.routers.api-ecobol-secure.rule=Host(`api.example.com`)"
      - "traefik.http.routers.api-ecobol-secure.tls=true"
      - "traefik.http.routers.api-ecobol-secure.tls.certresolver=http"
      - "traefik.http.routers.api-ecobol-secure.service=api-ecobol"
      - "traefik.http.services.api-ecobol.loadbalancer.server.port=3000"
      - "traefik.docker.network=web"
    ports:
      - '127.0.0.1:3001:3000'
    networks:
      - web
  mongo:
    image: 'mongo:4'
    volumes:
      - './data:/data/db'
    restart: on-failure
    labels:
      - "traefik.enable=false"
    ports:
      - '127.0.0.1:27018:27017'
    networks:
      - web
networks:
  web:
    external: true

Note: If you have kept the http -> global https redirection in config/config.yml, you can delete these lines (the http router) from the labels:

- "traefik.http.routers.api-ecobol.entrypoints=http"
- "traefik.http.routers.api-ecobol.rule=Host(`api.example.com`)"
- "traefik.http.routers.api-ecobol.middlewares=https-redirect@file"

There is two new things here: the loadbalancer.server.port line, indicating traefik on which port the app is running inside the container, and the line traefik.enable=false, to prevent traefik from exposing the container.

Following my last article this configuration can be specified only for production in docker-compose.prod.yml.

As usual:

docker-compose up -f docker-compose.yml -f docker-compose.prod.yml -d

Scaling containers: Load Balancing

Now, we can try load balancing. With Docker-Compose, the command for scaling a container is:

docker-compose scale api-ecobol=3

This will create a total of 3 containers, and Traefik will automatically detect them, and balance incoming traffic between all of them, while monitoring thier status. You can observe that in the dashboard at traefik.domain.com.