Skip to content

Sharing the Traefik Proxy ​

Use case ​

Brewblox uses a Traefik gateway to let multiple services be publicly accessible on the same host port.

This works fine, but if you decide you want a secondary Traefik container to do the same for other non-Brewblox services on your computer, you run into problems.

Traefik service discovery is an issue, and port sharing is (again) an issue.

Requirements ​

  • System includes an active and accessible Brewblox system.
  • System includes one or more non-Brewblox containers.
  • Both Brewblox and non-Brewblox containers are accessible on port 80/443.
  • Top-level routing is based on hostname.
  • Brewblox has a dedicated hostname.
  • Hostname routing logic is extensible: 2..N hostnames must be recognized as routing rules.

Brewblox routing ​

We'll start with the routing rules required for Brewblox, and then extend to include other hosts.

Referenced compose configuration should be implemented in docker-compose.yml. It will override the default configuration in docker-compose.shared.yml.

Traefik service configuration ​

If you check brewblox/docker-compose.shared.yml, you'll find the traefik service configuration:

yaml
  traefik:
    image: traefik:2.10
    restart: unless-stopped
    labels:
      - traefik.http.routers.api.rule=PathPrefix(`/api`) || PathPrefix(`/dashboard`)
      - traefik.http.routers.api.service=api@internal
      - traefik.http.middlewares.prefix-strip.stripprefixregex.regex=/[^/]+
      - traefik.http.middlewares.auth.forwardauth.address=http://auth:5000/auth/verify
      - traefik.http.middlewares.cors.headers.AccessControlAllowMethods=CONNECT,HEAD,GET,DELETE,OPTIONS,PATCH,POST,PUT,TRACE
      - traefik.http.middlewares.cors.headers.accessControlAllowOriginListRegex=.*
      - traefik.http.middlewares.cors.headers.AccessControlAllowCredentials=true
      - traefik.http.middlewares.cors.headers.AccessControlAllowHeaders=Origin,X-Requested-With,Content-Type,Accept
    volumes:
      - type: bind
        source: ./traefik
        target: /config
        read_only: true
      - type: bind
        source: /var/run/docker.sock
        target: /var/run/docker.sock
      - type: bind
        source: /etc/localtime
        target: /etc/localtime
        read_only: true
    ports:
      - "${BREWBLOX_PORT_HTTP}:${BREWBLOX_PORT_HTTP}"
      - "${BREWBLOX_PORT_HTTPS}:${BREWBLOX_PORT_HTTPS}"
      - "${BREWBLOX_PORT_MQTT}:${BREWBLOX_PORT_MQTT}"
      - "${BREWBLOX_PORT_MQTTS}:${BREWBLOX_PORT_MQTTS}"
      - "127.0.0.1:${BREWBLOX_PORT_ADMIN}:${BREWBLOX_PORT_ADMIN}"
    environment:
      - TRAEFIK_API_DASHBOARD=true
      - TRAEFIK_PROVIDERS_DOCKER=true
      - TRAEFIK_PROVIDERS_DOCKER_CONSTRAINTS=LabelRegex(`com.docker.compose.project`, `${COMPOSE_PROJECT_NAME}`)
      - TRAEFIK_PROVIDERS_DOCKER_DEFAULTRULE=PathPrefix(`/{{ index .Labels "com.docker.compose.service" }}`)
      - TRAEFIK_PROVIDERS_FILE_DIRECTORY=/config
      - TRAEFIK_ENTRYPOINTS_WEBSECURE_ADDRESS=:${BREWBLOX_PORT_HTTPS}
      - TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_TLS=true
      - TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_MIDDLEWARES=cors,auth
      - TRAEFIK_ENTRYPOINTS_WEB_ADDRESS=:${BREWBLOX_PORT_HTTP}
      - TRAEFIK_ENTRYPOINTS_WEB_HTTP_REDIRECTIONS_ENTRYPOINT_TO=websecure
      - TRAEFIK_ENTRYPOINTS_ADMIN_ADDRESS=:${BREWBLOX_PORT_ADMIN}
      - TRAEFIK_ENTRYPOINTS_ADMIN_HTTP_MIDDLEWARES=cors
      - TRAEFIK_ENTRYPOINTS_MQTT_ADDRESS=:${BREWBLOX_PORT_MQTT}/tcp
      - TRAEFIK_ENTRYPOINTS_MQTTS_ADDRESS=:${BREWBLOX_PORT_MQTTS}/tcp

This is a big blob of configuration at once, so we'll go through it section by section.

yaml
    labels:
      - traefik.http.routers.api.rule=PathPrefix(`/api`) || PathPrefix(`/dashboard`)
      - traefik.http.routers.api.service=api@internal
      - traefik.http.middlewares.prefix-strip.stripprefixregex.regex=/[^/]+
      - traefik.http.middlewares.auth.forwardauth.address=http://auth:5000/auth/verify
      - traefik.http.middlewares.cors.headers.AccessControlAllowMethods=CONNECT,HEAD,GET,DELETE,OPTIONS,PATCH,POST,PUT,TRACE
      - traefik.http.middlewares.cors.headers.AccessControlAllowOriginListRegex=.*
      - traefik.http.middlewares.cors.headers.AccessControlAllowCredentials=true
      - traefik.http.middlewares.cors.headers.AccessControlAllowHeaders=Origin,X-Requested-With,Content-Type,Accept

The two labels starting with traefik.http.routers.api are for the Traefik dashboard, hosted at <ADDRESS>/dashboard/.

traefik.http.middlewares.prefix-strip.stripprefixregex.regex=/[^/]+ is a reusable middleware for routing a public path with a prefix to a private path without a prefix.

traefik.http.middlewares.cors.(...) headers contain access control and Cross-Origin Resource Sharing (CORS) configuration. For Brewblox this is set very permissive, as the origin (server address) is different for every installation.

yaml
    volumes:
      - type: bind
        source: ./traefik
        target: /config
        read_only: true
      - type: bind
        source: /var/run/docker.sock
        target: /var/run/docker.sock
      - type: bind
        source: /etc/localtime
        target: /etc/localtime
        read_only: true

The used SSL certs are placed in ./traefik, along with a traefik-cert.yaml configuration file.

/var/run/docker.sock:/var/run/docker.sock allows access to the Docker socket. This is required in order to autodetect active Docker containers.

/etc/localtime is mounted to make sure the container uses the same time and timezone settings as the host.

yaml
    ports:
      - "${BREWBLOX_PORT_HTTP}:${BREWBLOX_PORT_HTTP}"
      - "${BREWBLOX_PORT_HTTPS}:${BREWBLOX_PORT_HTTPS}"
      - "${BREWBLOX_PORT_MQTT}:${BREWBLOX_PORT_MQTT}"
      - "${BREWBLOX_PORT_MQTTS}:${BREWBLOX_PORT_MQTTS}"
      - "127.0.0.1:${BREWBLOX_PORT_ADMIN}:${BREWBLOX_PORT_ADMIN}"

The BREWBLOX_PORT_XXXX variables are defined in the brewblox/.env file. The default values are set during installation.

The admin port is special: it is a non-authenticated HTTP port that is only accessible from the server itself. This port is used by brewblox-ctl during installation and updates.

There are quite a few arguments in the environment: section. We'll look at it a few lines at a time.

yaml
      - TRAEFIK_API_DASHBOARD=true

This enables the Traefik dashboard at /dashboard/ (the trailing / is required).

yaml
      - TRAEFIK_PROVIDERS_DOCKER=true
      - TRAEFIK_PROVIDERS_DOCKER_CONSTRAINTS=LabelRegex(`com.docker.compose.project`, `${COMPOSE_PROJECT_NAME}`)
      - TRAEFIK_PROVIDERS_DOCKER_DEFAULTRULE=PathPrefix(`/{{ index .Labels "com.docker.compose.service" }}`)

TRAEFIK_PROVIDERS_DOCKER enables Traefik scanning the Docker socket for active containers.

To avoid trying to route to any and all containers on the host, we add constraints. Docker-compose sets the com.docker.compose.project label on managed containers. The value equals that of the COMPOSE_PROJECT_NAME that is set in the brewblox/.env file.

The default routing rule for Brewblox services is to use the service name as prefix. eg. <ADDRESS>/spark-one/blocks should be routed to the spark-one service. We can get service name from another container label set by docker-compose: com.docker.compose.service.

yaml
      - TRAEFIK_PROVIDERS_FILE_DIRECTORY=/config

/config is a mounted volume that leads to brewblox/traefik. There's a configuration file and SSL certificates in there.

yaml
      - TRAEFIK_ENTRYPOINTS_WEBSECURE_ADDRESS=:${BREWBLOX_PORT_HTTPS}
      - TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_TLS=true
      - TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_MIDDLEWARES=cors,auth
      - TRAEFIK_ENTRYPOINTS_WEB_ADDRESS=:${BREWBLOX_PORT_HTTP}
      - TRAEFIK_ENTRYPOINTS_WEB_HTTP_REDIRECTIONS_ENTRYPOINT_TO=websecure
      - TRAEFIK_ENTRYPOINTS_ADMIN_ADDRESS=:${BREWBLOX_PORT_ADMIN}
      - TRAEFIK_ENTRYPOINTS_ADMIN_HTTP_MIDDLEWARES=cors
      - TRAEFIK_ENTRYPOINTS_MQTT_ADDRESS=:${BREWBLOX_PORT_MQTT}/tcp
      - TRAEFIK_ENTRYPOINTS_MQTTS_ADDRESS=:${BREWBLOX_PORT_MQTTS}/tcp

There are five entrypoints:

  • websecure
  • web
  • admin
  • mqtt
  • mqtts

websecure is the primary user-facing entrypoint. HTTPS and authentication are both enabled.

web is a HTTP entrypoint that immediately redirects requests to websecure. Without it, you would get an error when visiting http://{address}.

admin is a HTTP entrypoint used by brewblox-ctl. To keep it safe, it is only accessible from the server itself (ie. bound to 127.0.0.1 in ports:).

mqtt and mqtts are directly forwarded to the MQTT eventbus.

Traefik dashboard ​

You can check the Traefik dashboard by navigating to <ADDRESS>/dashboard/. The trailing / is required.

Here you'll see all Routers, Services, and Middlewares that are currently active.

For a detailed explanation of how these interact, you can check this Traefik documentation page.

Other Brewblox services ​

Traefik allows for a number of shortcuts:

  • A Traefik Router is automatically added if you declare a Traefik Service.
  • You only need to specify a target port on your service if multiple ports are exposed.

The traefik service includes a default setting for the routing rule.

If you check docker-compose.shared.yml, you'll find:

yaml
  eventbus:
    ...
    labels:
      # MQTT
      - traefik.tcp.routers.mqtt.entrypoints=mqtt
      - traefik.tcp.routers.mqtt.rule=HostSNI(`*`)
      - traefik.tcp.routers.mqtt.tls=false
      - traefik.tcp.routers.mqtt.service=mqtt
      - traefik.tcp.services.mqtt.loadBalancer.server.port=1883
      # MQTTS with TLS termination by traefik
      - traefik.tcp.routers.mqtts.entrypoints=mqtts
      - traefik.tcp.routers.mqtts.rule=HostSNI(`*`)
      - traefik.tcp.routers.mqtts.tls=true
      - traefik.tcp.routers.mqtts.service=mqtts
      - traefik.tcp.services.mqtts.loadBalancer.server.port=1884
      # MQTT over websockets
      - traefik.http.services.eventbus.loadbalancer.server.port=15675

  victoria:
    ...
    labels:
      - traefik.http.services.victoria.loadbalancer.server.port=8428

  redis:
    ...
    labels:
      - traefik.enable=false

  ui:
    ...
    labels:
      - traefik.http.routers.ui.rule=PathPrefix(`/ui`) || PathPrefix(`/static`) || Path(`/`)

Notes:

  • eventbus and victoria have multiple published ports, so we need to be specific.
  • redis is not accessible through the gateway.
  • ui has a custom routing rule: if you go to <Address>:<Port>, you will be routed to the UI service.

So much for the default settings. Time for change!

Adding DNS hostnames ​

Let's add another service: webby. We want to be routed to webby if we navigate to webby.local.

First, we want the webby.local address to point to our host. A permanent solution is to add an entry in the router DNS records, but for now we can use avahi-publish to temporarily add DNS-SD records.

Open two terminal windows, and run (one command in each):

sh
avahi-publish -a -R brewblox.local $(hostname -I | cut -d' ' -f1)
sh
avahi-publish -a -R webby.local $(hostname -I | cut -d' ' -f1)

Leave the terminal windows open. The records disappear when you close the running process.

Now, if we navigate to either https://webby.local or https://brewblox.local, we end up in the Brewblox UI.

Host-based routing ​

Now, add the actual webby service to docker-compose.yml. We'll keep it simple, and use the default Nginx image.

We want webby.local to go to webby, and brewblox.local to go to the Brewblox UI.

yaml
  webby:
    image: nginx
    labels:
      - traefik.http.routers.webby.rule=Host(`webby.local`)
      - traefik.http.routers.webby.priority=9001

Now, if you go to brewblox.local, you'll end up at https://brewblox.local/ui/dashboard/dashboard-home, and if you go to webby.local, you get the Nginx welcome message.

Multi-project routing ​

If you don't want webby to be defined in brewblox/docker-compose.yml, you will need to set up a shared Docker network so the Traefik router can reach webby.

See the docker-compose documentation for how to set up custom networks.

You will also need to change or remove the provider constraint for the traefik service.

Currently, it is:

yaml
  traefik:
    ...
    environment:
      ...
      - TRAEFIK_PROVIDERS_DOCKER_CONSTRAINTS=LabelRegex(`com.docker.compose.project`, `${COMPOSE_PROJECT_NAME}`)
      ...

To add the webby project, you can modify the value to:

go
LabelRegex(`com.docker.compose.project`, `(${COMPOSE_PROJECT_NAME}|webby)`)

If you want all containers on the host to be managed by traefik, you can completely remove the TRAEFIK_PROVIDERS_DOCKER_CONSTRAINTS argument.

Note that you can't partially override the traefik service environment. If you want to change one argument, you'll have to copy and include all other arguments.

Run docker-compose config afterwards to check the merged result.