Customizing error pages in Traefik v.2
Tags: traefik, reverseproxy, web, errorpage
Published: 2023-12-11 15:02:54

---
title: Customizing error pages in Traefik v.2
published: true
date: 2023-04-15 16:30:00 UTC
tags: Traefik, ReverseProxy, Web, errorpage
canonical_url: https://imandrea.me/blog/traefik-custom-404/
---

> 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!](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8gm6d452jihwphkl29sl.gif)

I described, by and large, **Traefik v.2** for **Docker** containers in this [post](intro-to-traefik), 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](#further-reading). 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**](https://docs.traefik.io/middlewares/errorpages/). Its duty is returning a custom page in lieu of the default, according to configured ranges of HTTP status codes. Here is an example:

```yaml
# 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:

- **status** is the HTTP status that will trigger the error page (in this example, every code between 400 and 599)
- `serviceError`is 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vx9pfozmnh9z5at50a7g.png)

## 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:

```bash
*.example.com
```

URLs such as:

```bash
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/32q7dop0qyqv7578hk44.png)

**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:

```yaml
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](https://www.imandrea.me/blog/intro-to-traefik/). 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`:

```yaml
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**](https://docs.traefik.io/routing/routers/#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`:

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

that is the actual Traefik's ErrorPage middleware:

```yaml
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:

```yaml
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:

```bash
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:

```yaml
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](https://this-does-not-exist-at-all.imandrea.me), Traefik will serve **the page**. But if you request: [imandrea.me/bad/path](https://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

- [The ErrorPage middleware](https://docs.traefik.io/middlewares/errorpages/)
- An interesting [thread about this topic](https://community.containo.us/t/global-404-handler/2144)
  on the Containous Community Forum
- [Static server error pages in a Docker image](https://github.com/tarampampam/error-pages)
  (also ready for Traefik)

Cheers!

^..^