Final Year Project: Part 2 - Setting up Traefik v2 with dockerized NodeJS API
Table of contents
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:
Quick explanation of how Traefik works
Traefik handles requests like this:
Here is the router used in this chain (note the rule
part), with its TLS configuration (letsencrypt http method) and the middleware used (basicauth).
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 istraefik-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
.