Using #traefik error pages to handle unavailable services

When a request is unknown, traefik displays a very basic 404 page. Wouldn’t it be nice to provide more guidance to your users? Fortunately, you can customize error pages for services behind traefik and use that feature to catch unknown services. Let’s take a closer look at this!

traefik and the service

For the purpose of this post, I have created the following set of services - a nginx-based web server behind traefik. The web server is available at www.127.0.0.1.nip.io and the traefik dashboard at traefik.127.0.0.1.nip.io.

services:

  traefik:
    image: traefik:v2.4
    command:
    - --log=true
    - --log.level=DEBUG
    - --api.dashboard=true
    - --entrypoints.http.address=:80
    - --providers.docker=true
    - --providers.docker.exposedByDefault=false
    ports:
    - 127.0.0.1:80:80
    volumes:
    - /var/run/docker.sock:/var/run/docker.sock:ro
    - /etc/localtime:/etc/localtime:ro
    restart: always
    labels:
      traefik.enable: "true"
      traefik.http.routers.traefik.entrypoints: http
      traefik.http.routers.traefik.service: api@internal
      traefik.http.routers.traefik.rule: HostRegexp(`traefik.127.0.0.1.nip.io`)

  www:
    image: nginx:stable
    volumes:
    - ./www/pages:/usr/share/nginx/html
    - ./www/default.conf:/etc/nginx/conf.d/default.conf
    - /etc/localtime:/etc/localtime:ro
    labels:
      traefik.enable: "true"
      traefik.http.services.www.loadbalancer.server.port: 80
      traefik.http.routers.www.entrypoints: http
      traefik.http.routers.www.rule: HostRegexp(`www.127.0.0.1.nip.io`)

To follow the examples provided in this post, please refer to my GitHub repository containing all required files. The example above can be deploying using docker-compose:

docker-compose \
    --file docker-compose.yaml \
    up -d

You can either test the services in a browser by visiting the traefik dashboard and the web server. Or you can test the web service by using curl:

curl -sv http://www.127.0.0.1.nip.io

Catching unavailable services

Let’s first introduce a new service to catch all requests to unknown services. You need to add a new service (see catch-all below) which matches all hosts (HostRegexp(\"{host.+}\")) and has a lower priority (lower number) than existing services. I have decided to place regular services at priority 100.

The service catch-all receives the traefik middleware ErrorPages called catch-all to define a custom error page for the HTTP response code 404. This response code occurs because the service catch-all does not offer any pages except for a custom 404.html. Therefore any request caught by this service is answered by the custom page defined by the middleware.

services:

  traefik:
    image: traefik:v2.4
    command:
    - --log=true
    - --log.level=DEBUG
    - --api.dashboard=true
    - --entrypoints.http.address=:80
    - --providers.docker=true
    - --providers.docker.exposedByDefault=false
    ports:
    - 127.0.0.1:80:80
    volumes:
    - /var/run/docker.sock:/var/run/docker.sock:ro
    - /etc/localtime:/etc/localtime:ro
    restart: always
    labels:
      traefik.enable: "true"
      traefik.http.routers.traefik.entrypoints: http
      traefik.http.routers.traefik.service: api@internal
      traefik.http.routers.traefik.rule: HostRegexp(`traefik.127.0.0.1.nip.io`)

  catch-all:
    image: nginx:stable
    volumes:
    - ./catch-all/pages:/usr/share/nginx/error-pages
    - ./catch-all/default.conf:/etc/nginx/conf.d/default.conf
    - /etc/localtime:/etc/localtime:ro
    labels:
      traefik.enable: "true"
      traefik.http.services.catch-all.loadbalancer.server.port: 80
      traefik.http.routers.catch-all.entrypoints: http
      traefik.http.routers.catch-all.rule: HostRegexp(`{host:.+}`)
      traefik.http.routers.catch-all.priority: 1
      traefik.http.middlewares.catch-all.errors.status: 404
      traefik.http.middlewares.catch-all.errors.service: catch-all
      traefik.http.middlewares.catch-all.errors.query: /404.html

  www:
    image: nginx:stable
    volumes:
    - ./www/pages:/usr/share/nginx/html
    - ./www/default.conf:/etc/nginx/conf.d/default.conf
    - /etc/localtime:/etc/localtime:ro
    labels:
      traefik.enable: "true"
      traefik.http.services.www.loadbalancer.server.port: 80
      traefik.http.routers.www.entrypoints: http
      traefik.http.routers.www.rule: HostRegexp(`www.127.0.0.1.nip.io`)
      traefik.http.routers.www.priority: 100

This example is also contained in my GitHub repository and can be deploying using docker-compose:

docker-compose \
    --file docker-compose.yaml \
    --file docker-compose.catch-all.yaml \
    up -d

You can either test the catch-all service in a browser by visiting a non existent server. Or you can test the catch-all service by trying to access a non-existent service producing the custom error page:

curl -sv http://missing.127.0.0.1.nip.io

Returning custom error pages

Custom error pages can also be configured for other errors like a misbehaving service throwing a HTTP response code of 500-599.

services:

  traefik:
    image: traefik:v2.4
    command:
    - --log=true
    - --log.level=DEBUG
    - --api.dashboard=true
    - --entrypoints.http.address=:80
    - --providers.docker=true
    - --providers.docker.exposedByDefault=false
    ports:
    - 127.0.0.1:80:80
    volumes:
    - /var/run/docker.sock:/var/run/docker.sock:ro
    - /etc/localtime:/etc/localtime:ro
    restart: always
    labels:
      traefik.enable: "true"
      traefik.http.routers.traefik.entrypoints: http
      traefik.http.routers.traefik.service: api@internal
      traefik.http.routers.traefik.rule: HostRegexp(`traefik.127.0.0.1.nip.io`)

  catch-all:
    image: nginx:latest
    volumes:
    - ./catch-all/pages:/usr/share/nginx/error-pages
    - ./catch-all/default.conf:/etc/nginx/conf.d/default.conf
    - /etc/localtime:/etc/localtime:ro
    labels:
      traefik.enable: "true"
      traefik.http.services.catch-all.loadbalancer.server.port: 80
      traefik.http.routers.catch-all.entrypoints: http
      traefik.http.routers.catch-all.rule: HostRegexp(`{host:.+}`)
      traefik.http.routers.catch-all.priority: 1

  error-pages:
    image: nginx:stable
    volumes:
    - ./error-pages/pages:/usr/share/nginx/html
    - ./error-pages/default.conf:/etc/nginx/conf.d/default.conf
    - /etc/localtime:/etc/localtime:ro
    labels:
      traefik.enable: "true"
      traefik.http.services.error-pages.loadbalancer.server.port: 80
      traefik.http.routers.error-pages.entrypoints: http
      traefik.http.routers.error-pages.rule: HostRegexp(`error-pages.127.0.0.1.nip.io`)
      traefik.http.routers.error-pages.priority: 2
      traefik.http.middlewares.error-pages.errors.status: 500-599
      traefik.http.middlewares.error-pages.errors.service: error-pages
      traefik.http.middlewares.error-pages.errors.query: /5xx.html

  www:
    image: nginx:stable
    volumes:
    - ./www/pages:/usr/share/nginx/html
    - ./www/default.conf:/etc/nginx/conf.d/default.conf
    - /etc/localtime:/etc/localtime:ro
    labels:
      traefik.enable: "true"
      traefik.http.services.www.loadbalancer.server.port: 80
      traefik.http.routers.www.entrypoints: http
      traefik.http.routers.www.rule: HostRegexp(`www.127.0.0.1.nip.io`)
      traefik.http.routers.www.priority: 100
      traefik.http.routers.www.middlewares: error-pages

  httpbin:
    image: kennethreitz/httpbin
    volumes:
    - /etc/localtime:/etc/localtime:ro
    labels:
      traefik.enable: "true"
      traefik.http.services.bin.loadbalancer.server.port: 80
      traefik.http.routers.bin.entrypoints: http
      traefik.http.routers.bin.rule: HostRegexp(`bin.127.0.0.1.nip.io`)
      traefik.http.routers.bin.priority: 100
      traefik.http.routers.bin.middlewares: error-pages

This example is also contained in my GitHub repository and can be deploying using docker-compose:

docker-compose \
    --file docker-compose.yaml \
    --file docker-compose.catch-all.yaml \
    --file docker-compose.internal-error.yaml \
    up -d

You can either test this in a browser by forcing an 5xx response code using httpbin. Or you can achieve the same using curl:

curl -sv http://bin.127.0.0.1.nip.io/status/500

See also my other post about using cookies with traefik to display an informational page during maintenance as well as a message of the day (MOTD).

Feedback is always welcome! If you'd like to get in touch with me concerning the contents of this article, please use Twitter.