DEV Community

Andrea Mignone
Andrea Mignone

Posted on • Originally published at imandrea.me on

Customizing error pages in Traefik v.2

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.

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"
Enter fullscreen mode Exit fullscreen mode

As stated in the official documentation:

  • status is the HTTP status that will trigger the error page (in this example, every code between 400 and 599)
  • serviceErroris the service that will serve the new requested error page
  • query is the URL of the error page (hosted by the service), where {status} in the query will be replaced by the received status code.

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
Enter fullscreen mode Exit fullscreen mode

URLs such as:

http://what-the-heck-is-this.example.com
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!

^..^

Top comments (0)