Manage a multiple websites server with Docker, Traefik and auto SSL certificates

Steeve - May 12 - - Dev Community

Since 2020, I manage a server to host all my fun side projects on it, but it turns out, it became a server to host multiple Wordpress for customers. At the beginning, I used Docker coupled with Nginx as reverse proxy. However, I migrated to Traefik as Reverse proxy because:

  • Easy setup and config
  • Automaticly generate and renew SSL certificates
  • Support Docker / Swarm / Kubernetes
  • Support HTTP/3
  • Active and Strong open-source community

Traefik has many roles, but the main ones:

  • Routing: The incoming traffic is redirected to the corresponding service based on Rules (a website, API or any service)
  • Load Balancing: The incoming traffic is distributed across multiple instances (if you use Swarm/Kubernetes)
  • SSL Decryption: The traffic is decrypted before it reaches the corresponding service.
  • Dynamic Config: New services can be discovered, and traefik configure itself.

For instance, you start a new Wordpress container, Traefik will automatically: detect it, create certificates, create a route, and voilà, your website is accessible from internet!

Diagram of Traefik usage

Setup the Traefik Reverse Proxy

Before going any further, I assume you have a VPS or server accessible from a public IP

To start the Traefik service, create a docker-compose.yml, and write the following configuration:

version: "3.7"

services:
  reverse-proxy:
    # The official v3 Traefik docker image
    image: traefik:latest
    container_name: reverse-proxy
    # Enables the web UI and tells Traefik to listen to docker
    command:
      - --entrypoints.web.address=:80
      - --entrypoints.web.http.redirections.entrypoint.to=websecure
      - --entrypoints.web.http.redirections.entrypoint.scheme=https
      - --entrypoints.websecure.address=:443
      - --entrypoints.websecure.http3
      - --log.level=INFO

      - --providers.docker=true
      - --providers.docker.network=proxy
      - --providers.docker.exposedbydefault=false # Do not expose containers unless explicitly told so

      - --api=true # Enable dashboard
      - --api.debug=true

      - --certificatesresolvers.le.acme.tlschallenge=true
      - --certificatesresolvers.le.acme.email=YOUR_EMAIL #  cert resolvers
      - --certificatesResolvers.le.acme.storage=/ssl/acme.json
    restart: always
    ports:
      - 80:80/tcp
      - 80:80/udp
      - 443:443/tcp
      - 443:443/udp
    volumes:
      # So that Traefik can listen to the Docker events
      - /var/run/docker.sock:/var/run/docker.sock
      - ${PWD}/ssl:/ssl
    labels:
      traefik.enable: true

      # Dashboard
      traefik.http.routers.reverse-proxy.rule: Host(`YOUR_CUSTOM_DOMAIN`)
      traefik.http.routers.reverse-proxy.entrypoints: websecure
      traefik.http.routers.reverse-proxy.service: api@internal
      traefik.http.routers.reverse-proxy.tls: true
      traefik.http.routers.reverse-proxy.tls.certresolver: le
      traefik.http.routers.reverse-proxy.middlewares: dashboard
      traefik.http.middlewares.dashboard.basicauth.users: YOUR_CUSTOM_ADMIN_HTTP_PASSWORD


      # SECURITY MIDDLEWARE named "hstsx"
      traefik.http.middlewares.hstsx.headers.stsincludesubdomains: true
      traefik.http.middlewares.hstsx.headers.stspreload: true
      traefik.http.middlewares.hstsx.headers.stsseconds: 31536000
      traefik.http.middlewares.hstsx.headers.forcestsheader: true
      traefik.http.middlewares.hstsx.headers.customframeoptionsvalue: sameorigin
      traefik.http.middlewares.hstsx.headers.browserxssfilter: true
      traefik.http.middlewares.hstsx.headers.sslredirect: true
      traefik.http.middlewares.hstsx.headers.contenttypenosniff: true
    networks:
      - default

networks:
  default:
    external:
      name: proxy
Enter fullscreen mode Exit fullscreen mode

A lot is happening here, let me detail the code:

  • First, you have to replace 3 configurations to make it work:
    • YOUR_EMAIL: Email used by let's encrypt to create certificates
    • YOUR_CUSTOM_DOMAIN: Traefik provide a web dashboard to manage your services, you can create a dedicated domain or sub-domain for accessing the panel. The domain must link to your server public IP.
    • YOUR_CUSTOM_ADMIN_HTTP_PASSWORD: To access the Traefik dashboard, you can restrict the access with credentials. Search online "htpasswd generator", generate credentials and write the value here. (Corresponding BasicAuth documentation).
  • At the beginning of the file, there is a rule used to redirect all non secured traffic (port 80) to HTTPS (port 443)
  • HTTP/3 is enabled by default: that's why the UDP and TCP connections are opened for both ports (80 and 443)
  • You can find a special middleware applied for all websites: it adds security headers used by web browsers to enable HSTS, Redirect to HTTPS, enable X-framing to avoid click-jacking attacks, by ensuring that their content is not embedded into other sites. Finally there is browserxssfilter, to stop pages from loading when they detected reflected cross-site scripting (XSS) attacks.
  • At the end of the file, the Traefik service is only available on the Docker external network named proxy.

Before starting the service, create the network proxy by running the command:

docker network create proxy
Enter fullscreen mode Exit fullscreen mode

Then start the Traefik container:

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

The Web Dashboard is now accessible from the domain specified at YOUR_CUSTOM_DOMAIN. The UI shows all the routes, providers and services handled by Traefik (screenshot taken from the Traefik documentation):

Traefik Web Dashboard

Your dashboard should print 0 Routes, 0 Services, and 0 provider

Voilà, we covered the most important with Traefik, to learn more about each configuration, take a look at the Traefik documentation.

Setup a Wordpress Container with Traefik

This part covers how to create a Wordpress container, and make it accessible from a custom domain thanks to traefik.

Create a new docker-compose.yml and write the following configuration:

version: '3.7'

services:
   wordpress-db:
     container_name: ${CONTAINER_DB_NAME}
     image: mariadb:latest
     restart: unless-stopped
     volumes:
       - ${DB_PATH}:/var/lib/mysql
     environment:
       MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
       MYSQL_DATABASE: ${MYSQL_DATABASE}
       MYSQL_USER: ${MYSQL_USER}
       MYSQL_PASSWORD: ${MYSQL_PASSWORD}
     expose:
       - ${PORTDB}
     healthcheck:
      test: mysql --user=${MYSQL_USER} --password=${MYSQL_PASSWORD} --database=${MYSQL_DATABASE} --silent --execute "SHOW DATABASES"
      interval: 2m
      timeout: 10s
      retries: 5
      start_period: 40s
   wordpress-server:
     container_name: ${CONTAINER_WP_NAME}
     image: wordpress:latest
     restart: unless-stopped
     volumes:
       - ${WP_CORE}:/var/www/html
       - ${WP_CONTENT}:/var/www/html/wp-content
     environment:
       WORDPRESS_DB_HOST: ${CONTAINER_DB_NAME}:${PORTDB}
       WORDPRESS_DB_NAME: ${MYSQL_DATABASE}
       WORDPRESS_DB_USER: ${MYSQL_USER}
       WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}
       WORDPRESS_TABLE_PREFIX: ${WORDPRESS_TABLE_PREFIX}
       VIRTUAL_HOST: ${DOMAINS}
     logging:
       options:
         max-size: ${LOGGING_OPTIONS_MAX_SIZE:-200k}
     expose:
       - ${PORTWEBSITE}
     depends_on:
       - wordpress-db
     healthcheck:
      test: ["CMD", "curl", "--fail", "--silent", "http://localhost"]
      interval: 1m30s
      timeout: 10s
      retries: 3
      start_period: 40s
     labels:
      traefik.enable: true
      traefik.http.routers.wordpress-server.rule: Host(`${DOMAINS}`)
      traefik.http.routers.wordpress-server.tls: true
      traefik.http.routers.wordpress-server.tls.certresolver: le
      traefik.http.routers.wordpress-server.entrypoints: websecure
     networks:
      - default

networks:
    default:
      external:
        name: proxy

Enter fullscreen mode Exit fullscreen mode

Code break-down:

  • The file defines two services: MariaDB service for the Database, and the Wordpress service which depends on the Database.
  • Volumes are used to save locally the Database, and Wordpress files.
  • Two Healthchecks are made for both service to verify if every runs smoothly. If the Healthcheck fails, Traefik won't make the service accessible.
  • The Wordpress service has special Traefik labels:
    • traefik.enable: true: Notifies traefik to expose the container. If false, the container is ignored.
    • traefik.http.routers.wordpress-server: This label is used to define rules to link a custom Domain to the Wordpress container, then it enables SSL (HTTPS), finaly the traffic must only come from HTTPS (websecure Port 443).

Before starting the Wordpress container, make sure you have defined the following environment variables in the .env file:

PORTWEBSITE=80
PORTDB=5432
#
# Database Container configuration
#
CONTAINER_DB_NAME=
# Path to store your database (Volume)
DB_PATH=/data/database_name
# Root password for your database
MYSQL_ROOT_PASSWORD=
# Database name, user and password for your wordpress
MYSQL_DATABASE=
MYSQL_USER=
MYSQL_PASSWORD=
#
# Wordpress Container configuration
#
CONTAINER_WP_NAME=
# Path to store your wordpress files
WP_CORE=./public/wp-core
WP_CONTENT=./public/wp-content
# Table prefix
WORDPRESS_TABLE_PREFIX=wp_
# Your domain (or domains)
DOMAINS=domain.org,blog.domain2.com
# Your email for Let's Encrypt register
LETSENCRYPT_EMAIL=
Enter fullscreen mode Exit fullscreen mode

Now we are ready to start the Wordpress container, run the docker command:

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

-d for detached mode, it starts the container in the background and leaves it running.

Traefik may takes a couple of minutes to create an HTTPs certificate, and make the service available online from your public IP. Voilà, you created a fresh new Wordpress website 🎉.

Setup a Node Container with Traefik

This section covers how to create a Node Server container, and make it accessible from a custom domain thanks to traefik. It could be an API, a frontend, or the Email API server from my previous article.

The Docker image used is node:18-slim to make the container small, and we are going to install CURL for the Docker healthcheck. Create a file named Dockerfile, and write the following config:

FROM node:18-slim

RUN apt-get update && apt-get install curl -y

ENV APP_ROOT /src
WORKDIR ${APP_ROOT}
ADD . ${APP_ROOT}

RUN npm install
Enter fullscreen mode Exit fullscreen mode

The base image is updated to get the latest security patches, and the CURL command is installed. In a second step, the local directory is loaded into the /src container directory, and Node packages are installed.

The Node image is now ready, let's create a docker-compose.yml file, and write the following configuration:

version: "3.7"

services:
  node-project:
    build: .
    container_name: ${DOMAIN}
    restart: always
    expose:
      - "${PORT}"
    command: "npm run start"
    networks:
      - default
    healthcheck:
      test: ["CMD", "curl", "--fail", "--silent", "http://localhost:${PORT}"]
      interval: 1m30s
      timeout: 10s
      retries: 3
      start_period: 40s
    labels:
      traefik.enable: true
      traefik.http.routers.node-project.rule: Host(`${DOMAIN}`)
      traefik.http.routers.node-project.tls: true
      traefik.http.routers.node-project.tls.certresolver: le
      traefik.http.routers.node-project.entrypoints: websecure
networks:
  default:
    external:
      name: proxy

Enter fullscreen mode Exit fullscreen mode

Exactly like the previous Wordpress container, the Node container has the similar Traefik labels for: Defining a domain, enabling SSL, and it receives the traffic from HTTPS (Websecure).

The Docker-compose file requires only two environment variables on the .env file:

PORT=3000
DOMAIN=api.domain.org
Enter fullscreen mode Exit fullscreen mode

Finally, start the Docker container with the command:

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

Wait a couple of minutes, then your Node server is available from your public IP 🎉!

Conclusion

Note: Before writing this article, I migrated from Traefik v2 to Traefik v3, and I restarted the container with the same configuration: full backward compatibility, no errors, that's beautiful! 🤌🤌

After using Traefik as Reverse Proxy for 3 years for professional and personal use, it does a perfect job, with no maintenance, easy configuration, easy monitory and no downtime.

Feel free to write a comment if you need help or have questions with Traefik.

Have a great day, cheers 🍻

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .