Skip to content

Customizing error pages in Traefik v.2

Posted on:April 15, 2023 at 04:30 PM

Do you wanna serve your own error page instead of the default “404 page not found” message? Traefik v.2 is ready for that… Wow!

Wow!

I described, by and large, Traefik v.2 for Docker containers in this post, but here I wanna go through how to serve a global 404 page (and error pages in general) in Traefik v.2.

I haven’t found fully explained examples over the internet so far, thus I tried a few approaches putting together different hints. Here, I’m gonna summarize (for me too! 🙃) the solution I came up with.

Table of contents

Open Table of contents

The ErrorPage middleware

Traefik comes with an out-of-the-box error middleware. Its duty is returning a custom page in lieu of the default, according to configured ranges of HTTP status codes. Here is an example:

# Dynamic Custom Error Page for 4XX/5XX Status Code
labels:
  - "traefik.http.middlewares.test-errorpage.errors.status=400-599"
  - "traefik.http.middlewares.test-errorpage.errors.service=serviceError"
  - "traefik.http.middlewares.test-errorpage.errors.query=/{status}.html"

As stated in the official documentation:

It’s worth noting that error pages are not directly hosted on Traefik, but you need to serve them with your Web server. In the following picture, coming from Traefik docs, there is an example of this scenario.

The ErrorPage middleware

A working scenario

The ErrorPage middleware looks great. But how can we harness it to serve our own ** page** (and error pages in general)?

Imagine you wanna serve a global page for the URLs that point to your host but that are not bound to defined services. For example, ìf you have a DNS record that matches requests like:

*.example.com

URLs such as:

http://what-the-heck-is-this.example.com

will hit your Traefik but no router can handle them. In those cases, we want Traefik to return our cool page.

How can we set this up? Let’s start from this blueprint:

A working example

In a nutshell: we are gonna define a low-priority catchall router rule that kicks in only if other routers for defined services can’t handle the request. Then, such an unknown request is handled by the ErrorPage middleware that tells Nginx to serve the error page.

Diving into code

It’s time to get our hands dirty with code! Here is the complete docker-compose file:

Expand docker-compose.yml
version: "3.7"

services:
  # A cool reverse-proxy / load balancer
  traefik:
    # The official v2 Traefik docker image
    image: traefik:v2.2.7
    container_name: traefik
    security_opt:
      - no-new-privileges:true
    restart: always
    volumes:
      # So that Traefik can listen to the Docker events
      - /var/run/docker.sock:/var/run/docker.sock:ro
    ports:
      # http
      - 80:80
    command:
      ###########################################
      #   Static Configuration harnessing CLI   #
      ###########################################
      # Activate dashboard.
      - --api.dashboard=true

      # Enable Docker backend with default settings.
      - --providers.docker=true
      # Do not expose containers by default.
      - --providers.docker.exposedbydefault=false
      # Default Docker network used.
      - --providers.docker.network=proxy

      # --entrypoints.<name>.address for ports
      # 80 (i.e., name = webinsecure)
      - --entrypoints.webinsecure.address=:80

    networks:
      # This is the network over which Traefik communicates with other containers.
      - proxy

    labels:
      ################################################
      #   Dynamic configuration with Docker Labels   #
      ################################################
      # You can tell Traefik to consider (or not) this container by setting traefik.enable to true or false.
      # We need it for the dashboard
      traefik.enable: true

      # Dashboard
      traefik.http.routers.traefik.rule: Host(`traefik.localhost`)
      traefik.http.routers.traefik.service: api@internal
      traefik.http.routers.traefik.entrypoints: webinsecure

  # The error pages server
  nginxError:
    image: nginx:latest
    volumes:
      - ./error-pages:/usr/share/nginx/error-pages
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
    networks:
      # This is the network over which Traefik communicates with other containers.
      - proxy
    labels:
      traefik.enable: true

      traefik.http.routers.error-router.rule: HostRegexp(`{host:.+}`)
      traefik.http.routers.error-router.priority: 1
      traefik.http.routers.error-router.entrypoints: webinsecure
      traefik.http.routers.error-router.middlewares: error-pages-middleware

      traefik.http.middlewares.error-pages-middleware.errors.status: 400-599
      traefik.http.middlewares.error-pages-middleware.errors.service: error-pages-service
      traefik.http.middlewares.error-pages-middleware.errors.query: /{status}.html

      traefik.http.services.error-pages-service.loadbalancer.server.port: 80

  # A defined service
  my-test-app:
    image: containous/whoami
    networks:
      # This is the network over which Traefik communicates with other containers.
      - proxy
    labels:
      traefik.enable: true
      traefik.http.routers.my-test-app.rule: Host(`test.localhost`)
      traefik.http.routers.my-test-app.entrypoints: webinsecure
      traefik.http.services.my-test-app.loadbalancer.server.port: 80

networks:
  proxy:
    external: true

I largely covered static and dynamic configuration of this file in the previous post. Here, all we need to serve the page lies in the Docker service nginxError that manages an Nginx container devoted to error pages.

All starts from the error-router:

labels:
  traefik.http.routers.error-router.rule: HostRegexp(`{host:.+}`)
  traefik.http.routers.error-router.priority: 1
  traefik.http.routers.error-router.entrypoints: webinsecure

It has a priority set to 1, so it catches all the requests iif they are not handled before by the others (i.e., traefik.http.routers.traefik and traefik.http.routers.my-test-app).

Then, we attach to it the error-pages-middleware:

labels:
  traefik.http.routers.error-router.middlewares: error-pages-middleware

that is the actual Traefik’s ErrorPage middleware:

labels:
  traefik.http.middlewares.error-pages-middleware.errors.status: 400-599
  traefik.http.middlewares.error-pages-middleware.errors.service: error-pages-service
  traefik.http.middlewares.error-pages-middleware.errors.query: /{status}.html

  traefik.http.services.error-pages-service.loadbalancer.server.port: 80

Such a middleware will ask the error-pages-service to serve our custom error pages.

A couple of things about Nginx volumes. In this example, we bind mount (but we might copy files in the container as well) two fundamental volumes:

volumes:
  - ./error-pages:/usr/share/nginx/error-pages
  - ./nginx/default.conf:/etc/nginx/conf.d/default.conf

In the folder ./error-pages we store error page files such as our own 404.html. In addition, we customize the configuration of this Nginx instance in default.conf as follows:

server {
    listen       80;
    server_name  localhost;

    error_page  404    /404.html;
    # other error pages here:
    # error_page  403    /403.html;

    location / {
        root  /usr/share/nginx/error-pages;
        internal;
    }
}

Ok, we are ready!

The machinery in action

Now it’s time to turn the key of our containers and to take them to the road.

If you request: http://traefik.localhost/ or http://test.localhost/ you get the Traefik dashboard and the whoami output, respectively.

If you try to get: http://this-does-not-exist.localhost, Traefik returns exactly your friendly 404 error page (i.e., 404.html).

Note that, if you are interested in managing errors in the same way for defined services too, you can leverage the ErrorPage middleware. For example, you can attach the middleware to the Traefik’s dashboard router as follows:

labels:
  # Dashboard
  traefik.http.routers.traefik.rule: Host(`traefik.localhost`)
  traefik.http.routers.traefik.service: api@internal
  traefik.http.routers.traefik.entrypoints: webinsecure
  # Attach the error middleware also to this router
  traefik.http.routers.traefik.middlewares: error-pages-middleware

Now, if you request: http://traefik.localhost/does/not/exist, you get your customized 404 error page again.

Alternatively, you can exploit a specific per-service strategy for bad paths. This is what I do here (please, note that things may change in the future, since I’m moving my stuff to the cloud). If you try to get: this-does-not-exist-at-all.imandrea.me, Traefik will serve the page. But if you request: imandrea.me/bad/path, this time you get the blog’s 404 page. This happens since Traefik can route those URLs to the blog service that, in turn, has its own internal strategy for managing internal routes that do not exist.

Further reading

Cheers!

^..^