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!
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:
- 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.
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:
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
- The ErrorPage middleware
- An interesting thread about this topic on the Containous Community Forum
- Static server error pages in a Docker image (also ready for Traefik)
Cheers!
^..^