Simplify your deployment with Traefik - A comprehensive walk-through

DevOps Jun 26, 2022

Deploying docker containers, managing routes, generating TLS certificates, and load balancing can be a hassle sometimes. It may become more difficult to manage all of this if you want to run multiple docker containers on the same host!

Solution? For running multiple services on the same host you can use a reverse proxy. A reverse proxy is a service running on your host machine which maps each public request to its corresponding backend service running on the same host. There are many reverse proxies out there. One of which is Traefik!

Traefik

Traefik 2 Docs

Traefik is an open-source router which takes care of reverse proxying requests, load balancing, TLS certificate generation (let's encrypt), etc. It supports HTTP, HTTP/2, TCP, UDP, Websockets, and gRPC protocols. It also has a clean and modern dashboard for monitoring. And adding to all these, it is way easier to use as compared to Nginx!

For more details: https://traefik.io/traefik/

In this post, I will guide you through setting up Traefik 2 and running a simple example Node.js app.

Prerequisites

For the sake of this post, I am going to use a VM instance from Oracle Cloud free tier (2 vCPU arm64, 8GB ram, Ubuntu 22.04). You are free to use whatever host you want.

Requirements:

  • A Linux server with Docker installed. Having at least 2 GB of RAM will be better for running multiple containers. Though you can run it on an even smaller server.
  • docker-compose should also be installed.
  • A domain name with a wildcard record pointing towards your server IP
    Here is a sample DNS record which you can use to set up for your domain.
Type Name Content TTL
A * server.ip Auto

This will make *.example.com available for Traefik. This means you can route your apps and services with names like blog.example.com, forum.example.com, dashboard.example.com, etc.

If you don't want to make a wildcard record for the main domain name but instead for a subdomain then you can change the value under Name to *.subdomain . This will result in names like xyz.subdomain.example.com, dashboard.subdomain.example.com, etc.

Step-1

Before going any further we need to update our system.

sudo apt-get update

Now, you can install docker and docker-compose.

sudo apt-get install \
    ca-certificates \
    curl \
    gnupg \
    lsb-release && \
    sudo mkdir -p /etc/apt/keyrings && \
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg && \
    echo \
    "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
    $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null && \
    sudo apt-get update && \
    sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin docker-compose && \
    sudo groupadd docker || sudo usermod -aG docker $USER && \
    newgrp docker && \
    sudo systemctl enable docker.service && sudo systemctl enable containerd.service

(I have munched up all the commands in one single just to save some time. It even includes some post-installation steps)

Verify your docker installation by running the famous hello-world image

docker run hello-world

Step-2

Let's start with Traefik. Create a data folder for storing Traefik configuration file traefik.yml, acme.json for storing certificates and a configurations folder for storing dynamic configurations file dynamic.yml.

mkdir -p data/configurations && \
  touch data/traefik.yml && \
  touch data/acme.json && \
  touch data/configurations/dynamic.yml && \
  chmod 600 data/acme.json

Now create a docker-compose.yml for Traefik.

nano docker-compose.yml

and add the following lines to docker-compose.yml

Replace traefik.yourdomain on line 25 with your dashboard URL.
Example: traefik.fossian.com or dash.fossian.com

version: '3.7'

services:
  traefik:
    image: traefik:v2.7
    container_name: traefik
    restart: always
    security_opt:
      - no-new-privileges:true
    ports:
      - 80:80
      - 443:443
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./data/traefik.yml:/traefik.yml:ro
      - ./data/acme.json:/acme.json
      - ./data/configurations:/configurations
    networks:
      - web
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=web"
      - "traefik.http.routers.traefik-secure.entrypoints=websecure"
      - "traefik.http.routers.traefik-secure.rule=Host(`traefik.yourdomain`)"
      - "traefik.http.routers.traefik-secure.middlewares=user-auth@file"
      - "traefik.http.routers.traefik-secure.service=api@internal"

networks:
  web:
    external: true

There's a lot of stuff going on here so let's understand some part of it.

We have passed no-new-privileges as true to avoid container processes from gaining additional privileges.

We have added docker.sock file in volumes so that Traefik can listen to all the changes happening to the containers. We have also added our configuration file traefik.yml and acme.json. We also added the path to our dynamic configuration file.

We have also set the network to web.

Then comes the labels. We have defined traefik.docker.network to web. We have defined entrypoint to websecure which listens on port 443. Then we have defined a rule for our Traefik dashboard.

Now let's edit Traefik configuration file traefik.yml.

nano ./data/traefik.yml

and add the following to it

Replace admin@yourdomain on lines 34 and 42 with your personal or website email. It will be used by Let's Encrypt to notify you regarding the expiration of TLS certificates. Nonetheless, you don't have to worry about regenerating certificates as Traefik will do it for you automatically.

api:
  dashboard: true

entryPoints:
  web:
    address: :80
    http:
      redirections:
        entryPoint:
          to: websecure

  websecure:
    address: :443
    http:
      middlewares:
        - secureHeaders@file
        - nofloc@file
      tls:
        certResolver: letsencrypt

pilot:
  dashboard: false

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
  file:
    filename: /configurations/dynamic.yml

certificatesResolvers:
  letsencrypt:
    acme:
      email: admin@yourdomain
      storage: acme.json
      keyType: EC384
      httpChallenge:
        entryPoint: web

  buypass:
    acme:
      email: admin@yourdomain
      storage: acme.json
      caServer: https://api.buypass.com/acme/directory
      keyType: EC256
      httpChallenge:
        entryPoint: web

Now edit dynamic.yml

nano ./data/configurations/dynamic.yml

and add the following to it

http:
  middlewares:
    nofloc:
      headers:
        customResponseHeaders:
          Permissions-Policy: "interest-cohort=()"
    secureHeaders:
      headers:
        sslRedirect: true
        forceSTSHeader: true
        stsIncludeSubdomains: true
        stsPreload: true
        stsSeconds: 31536000   
        
    # Generate yours using htpasswd
    # UserName : admin
    # Password : fossian    
    user-auth:
      basicAuth:
        users:
          # Generate using "htpasswd -nb admin secure_password"
          - "admin:$apr1$B5scLRiX$ivpndRS3XuqZ05oaDgHql0"

tls:
  options:
    default:
      cipherSuites:
        - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
        - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
      minVersion: VersionTLS12

To change the password install htpasswd which is a part of apache2-utils package.

sudo apt-get install apache2-utils

Then run this (make sure you replace secure_password with your password)

htpasswd -nb admin secure_password

This will return you something like this: admin:$apr1$L0VB4It1$Yx4tfFO26W68ikz/6VZOG1

You can now replace the old one (line 22) with your newly generated one.

Step-3

Now all the things are set. Just one more thing to do before running your shiny Traefik container, i.e., Creating the docker network web which we provided in the docker-compose.yml.

First, check if the network web is there or not.

docker network ls

If there is a network named web then you can skip the below command otherwise run it to make one.

docker network create web

When we will run other containers we can add them to this network so that Traefik can proxy them.

Now everything is set.

Run the Traefik container using docker-compose

docker-compose up -d

If everything goes well you can access your Traefik dashboard at traefik.yourdomain or the URL which you added in place of that.

Traefik Dashboard

There you have your own reverse proxy setup and running.

After setting Traefik, adding new docker containers is easy. You just have to define some labels and you are good to go.

Step-4: Hosting a node.js express webapp

We are going to deploy a simple node.js express web app.
Github: https://github.com/fossian-com/sample-webapp-nodejs

First, clone it to your server.

git clone https://github.com/fossian-com/sample-webapp-nodejs

Now, cd into the directory sample-webapp-nodejs and create a docker-compose.yml file.

cd sample-webapp-nodejs && nano docker-compose.yml

and add the following

Make sure you replace nodeapp.yourdomain at line 21 with the URL you want.
Example: nodeapp.fossian.com or express.fossian.com

version: '3'

services:
  nodejsapp:
    # image: image-name:tag
    build: .
    container_name: sample-webapp-nodejs
    environment:
      PORT: 8080
    networks:
      - web
    restart: always
    labels:
      # Allow Traefik to access the container
      - "traefik.enable=true"
      # Tells Traefik to look in web to find the internal IP of the container
      - "traefik.docker.network=web"
      # Entrypoints to websecure i.e., port 443
      - "traefik.http.routers.ghost-secure.entrypoints=websecure"
      # Rule for the url for webapp container
      - "traefik.http.routers.ghost-secure.rule=Host(`nodeapp.yourdomain`)"

networks:
  web:
    external: true

Now simply run

docker-compose up -d

and after successful deployment access your app at nodeapp.yourdomain or the URL which you provided.

Sample Nodejs Express WebApp

You can also see on your Traefik dashboard that a new route has been added.

sample-webapp-secure has been added

If you plan to run a full-fledge Nodejs app with database support then make sure you add a db service in docker-compose under the same network with the Nodejs app.

For example:

version: '3'

services:
  db:
    image: mariadb
    container_name: webapp-db
    volumes:
      - db-data:/var/lib/mysql
    networks:
      # For accepting communication from the webapp
      - default
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: db
      MYSQL_USER: dbuser
      MYSQL_PASSWORD: dbpassword

  nodejsapp:
    depends_on:
      - db
    # image: image-name:tag
    build: .
    container_name: sample-webapp-nodejs
    environment:
      PORT: 8080
      DB_HOST: db:3306
      DB_NAME: db
      DB_USER: dbuser
      DB_PASSWORD: dbpassword
    networks:
      # For communicating with the db
      - default
      - web
    restart: always
    labels:
      # Allow Traefik to access the container
      - "traefik.enable=true"
      # Tells Traefik to look in web to find the internal IP of the container
      - "traefik.docker.network=web"
      # Entrypoints to websecure i.e., port 443
      - "traefik.http.routers.ghost-secure.entrypoints=websecure"
      # Rule for the url for webapp container
      - "traefik.http.routers.ghost-secure.rule=Host(`nodeapp.yourdomain`)"

volumes:
  db-data:
    # Naming the database volume
    name: nodejs-webapp-db-data
networks:
  web:
    external: true

Conclusion

In this tutorial, we installed Traefik, ran a simple web app container and learnt how to reverse proxy it with Traefik using a few lines of code.

You could have done the same with Nginx but it wouldn't be this easy! Traefik handles everything on its own and you don't have to restart it every time you deploy or make any changes to any running container. With just a few lines in docker-compose.yml, we were able to reverse proxy the web app and even secure it with a TLS certificate.

You can learn more about Traefik here: Traefik Proxy Documentation.

Tags

Amresh Sinha

Developer👨‍💻 | Student🎓 | Traveller✈️