:construction: Static server error pages in a docker image
One day, you might want to replace the standard error pages of your HTTP server or K8S cluster with something more
original and attractive. That’s why this repository was created :) It contains:
Content-Type
HTTP header (and X-Format
) value, responding with the corresponding formatjson
, xml
, and plaintext
)json
format/healthz
)nginx
in a few simple stepsDownload the latest binary file for your OS/architecture from the releases page or use our Docker image:
Registry | Image |
---|---|
GitHub Container Registry | ghcr.io/tarampampam/error-pages |
Docker Hub (mirror) | tarampampam/error-pages |
[!IMPORTANT]
Using thelatest
tag for the Docker image is highly discouraged due to potential backward-incompatible changes
during major upgrades. Please use tags in theX.Y.Z
format.
💣 Or you can also download the already rendered error pages pack as a zip or
tar.gz archive.
The following templates are built-in and available for use without any additional setup:
[!NOTE]
Thecats
template is the only one of those that fetches resources (the actual cat pictures) from external
servers - all other templates are self-contained.
Template | Preview |
---|---|
app-down | |
cats | |
connection | |
ghost | |
hacker-terminal | |
l7 | |
lost-in-space | |
noise | |
orient | |
shuffle |
[!NOTE]
The “used times” counter increments when someone start the server with the specified template. Stats service does
not collect any information about location, IP addresses, and so on. Moreover, the stats are open and available for
everyone at error-pages.goatcounter.com. This is simply a counter to display
how often a particular template is used, nothing more.
First, ensure you have a precompiled binary file on your machine or have Docker/Podman installed. Next, start the
server with the following command:
$ ./error-pages serve
# --- or ---
$ docker run --rm -p '8080:8080/tcp' tarampampam/error-pages serve
That’s it! The server will begin running and listen on address 0.0.0.0
and port 8080
. Access error pages using
URLs like http://127.0.0.1:8080/{page_code}.html
.
To retrieve different error page codes using a static URL, use the X-Code
HTTP header:
$ curl -H 'X-Code: 500' http://127.0.0.1:8080/
The server respects the Content-Type
HTTP header (and X-Format
), delivering responses in requested formats
such as HTML, XML, JSON, and PlainText. Customization of these formats is possible via CLI flags or environment
variables.
For integration with ingress-nginx or debugging purposes, start the server with --show-details
(or set the environment variable SHOW_DETAILS=true
) to enrich error pages (including JSON and XML responses)
with upstream proxy information.
Switch themes using the TEMPLATE_NAME
environment variable or the --template-name
flag; available templates
are detailed in the readme file below.
[!TIP]
Use the--rotation-mode
flag or theTEMPLATES_ROTATION_MODE
environment variable to automate theme
rotation. Available modes includerandom-on-startup
,random-on-each-request
,random-hourly
,
andrandom-daily
.
To proxy HTTP headers from requests to responses, utilize the --proxy-headers
flag or environment variable
(comma-separated list of headers).
my-super-theme.html
:html
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ code }}</title>
</head>
<body>
<h1>YEAH! {{ message }}: {{ description }}</h1>
</body>
</html>
bash
$ docker run --rm \
-v "$(pwd)/my-super-theme.html:/opt/my-template.html:ro" \
-p '8080:8080/tcp' ghcr.io/tarampampam/error-pages:3 serve \
--add-template /opt/my-template.html \
--template-name my-template
# --- or ---
$ ./error-pages serve \
--add-template /opt/my-template.html \
--template-name my-template
bash
$ curl -H "Accept: text/html" http://127.0.0.1:8080/503
<!DOCTYPE html>
<html lang="en">
<head>
<title>503</title>
</head>
<body>
<h1>YEAH! Service Unavailable: The server is temporarily overloading or down</h1>
</body>
</html>
html
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ code }}</title>
</head>
<body>
<h1>{{ message }}: {{ description }}</h1>
</body>
</html>
my-template.html
and use it as your custom template. Then, generate your error pages using the command:bash
$ mkdir -p /path/to/output
$ ./error-pages build --add-template /path/to/your/my-template.html --target-dir /path/to/output
bash
$ cd /path/to/output && tree .
├── my-template
│ ├── 400.html
│ ├── 401.html
│ ├── 403.html
│ ├── 404.html
│ ├── 405.html
│ ├── 407.html
│ ├── 408.html
│ ├── 409.html
│ ├── 410.html
│ ├── 411.html
│ ├── 412.html
│ ├── 413.html
│ ├── 416.html
│ ├── 418.html
│ ├── 429.html
│ ├── 500.html
│ ├── 502.html
│ ├── 503.html
│ ├── 504.html
│ └── 505.html
…
$ cat my-template/403.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>403</title>
</head>
<body>
<h1>Forbidden: Access is forbidden to the requested page</h1>
</body>
</html>
nginx
# File: nginx.conf
server {
listen 80;
server_name localhost;
error_page 401 /_error-pages/401.html;
error_page 403 /_error-pages/403.html;
error_page 404 /_error-pages/404.html;
error_page 500 /_error-pages/500.html;
error_page 502 /_error-pages/502.html;
error_page 503 /_error-pages/503.html;
location ^~ /_error-pages/ {
internal;
root /usr/share/nginx/errorpages;
}
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}
dockerfile
FROM docker.io/library/nginx:1.27-alpine
# override default Nginx configuration
COPY --chown=nginx ./nginx.conf /etc/nginx/conf.d/default.conf
# copy statically built error pages from the error-pages image
# (instead of `ghost` you may use any other template)
COPY --chown=nginx \
--from=ghcr.io/tarampampam/error-pages:3 \
/opt/html/ghost /usr/share/nginx/errorpages/_error-pages
bash
$ docker build --tag your-nginx:local -f ./Dockerfile .
bash
$ docker run --rm -p '8081:80/tcp' your-nginx:local
$ curl http://127.0.0.1:8081/foobar | head -n 15 # in another terminal
yaml
# file: compose.yml (or docker-compose.yml)
services:
traefik:
image: docker.io/library/traefik:v3.1
command:
#- --log.level=DEBUG
- --api.dashboard=true # activate dashboard
- --api.insecure=true # enable the API in insecure mode
- --providers.docker=true # enable Docker backend with default settings
- --providers.docker.exposedbydefault=false # do not expose containers by default
- --entrypoints.web.address=:80 # --entrypoints.<name>.address for ports, 80 (i.e., name = web)
ports:
- "80:80/tcp" # HTTP (web)
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
labels:
traefik.enable: true
# dashboard
traefik.http.routers.traefik.rule: Host(`traefik.localtest.me`)
traefik.http.routers.traefik.service: api@internal
traefik.http.routers.traefik.entrypoints: web
traefik.http.routers.traefik.middlewares: error-pages-middleware
depends_on:
error-pages: {condition: service_healthy}
error-pages:
image: ghcr.io/tarampampam/error-pages:3 # using the latest tag is highly discouraged
environment:
TEMPLATE_NAME: l7 # set the error pages template
labels:
traefik.enable: true
# use as "fallback" for any NON-registered services (with priority below normal)
traefik.http.routers.error-pages-router.rule: HostRegexp(`.+`)
traefik.http.routers.error-pages-router.priority: 10
# should say that all of your services work on https
traefik.http.routers.error-pages-router.entrypoints: web
traefik.http.routers.error-pages-router.middlewares: error-pages-middleware
# "errors" middleware settings
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
# define service properties
traefik.http.services.error-pages-service.loadbalancer.server.port: 8080
nginx-or-any-another-service:
image: docker.io/library/nginx:1.27-alpine
labels:
traefik.enable: true
traefik.http.routers.test-service.rule: Host(`test.localtest.me`)
traefik.http.routers.test-service.entrypoints: web
traefik.http.routers.test-service.middlewares: error-pages-middleware
docker compose up
in the same directory as the compose.yml
file, you can:traefik.localtest.me
test.localtest.me
custom-http-errors
config valueyaml
controller:
config:
custom-http-errors: >-
401,403,404,500,501,502,503
defaultBackend:
enabled: true
image:
repository: ghcr.io/tarampampam/error-pages
tag: '3' # using the latest tag is highly discouraged
extraEnvs:
- name: TEMPLATE_NAME # Optional: change the default theme
value: l7
- name: SHOW_DETAILS # Optional: enables the output of additional information on error pages
value: 'true'
Chart.yaml
file:yaml
> dependencies:
> - name: traefik
> version: 34.1.0 # change to the latest version
> repository: https://helm.traefik.io/traefik
>
values.yaml
file:yaml
errorPages:
enabled: true
appName: error-pages
namespace: error-pages
version: 3.3.1 # https://github.com/tarampampam/error-pages/releases
themeName: shuffle
yaml
# file: error-pages/namespace.yaml
{{ with .Values.errorPages }}
{{- if .enabled }}
apiVersion: v1
kind: Namespace
metadata: {name: "{{ .namespace }}"}
{{- end }}
{{- end }}
yaml
# file: error-pages/deployment.yaml
{{ with .Values.errorPages }}
{{- if .enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: "{{ .appName }}"
namespace: {{ .namespace }}
labels: {app: "{{ .appName }}"}
spec:
replicas: 1
selector: {matchLabels: {app: "{{ .appName }}"}}
template:
metadata: {labels: {app: "{{ .appName }}"}}
spec:
automountServiceAccountToken: false
containers:
- name: "{{ .appName }}"
image: "ghcr.io/tarampampam/error-pages:{{ .version | default "latest" }}"
env:
- {name: TEMPLATE_NAME, value: "{{ .themeName | default "app-down" }}"}
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
readOnlyRootFilesystem: true
ports:
- {name: http, containerPort: 8080, protocol: TCP}
livenessProbe:
httpGet: {port: http, path: /healthz}
periodSeconds: 10
readinessProbe:
httpGet: {port: http, path: /healthz}
periodSeconds: 10
resources:
limits: {memory: 64Mi, cpu: 200m} # change if needed
requests: {memory: 16Mi, cpu: 20m}
{{- end }}
{{- end }}
yaml
# file: error-pages/service.yaml
{{ with .Values.errorPages }}
{{- if .enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ .appName }}-service
namespace: {{ .namespace }}
labels: {app: "{{ .appName }}"}
spec:
type: ClusterIP
selector: {app: "{{ .appName }}"}
ports: [{name: http, protocol: TCP, port: 8080, targetPort: 8080}]
{{- end }}
{{- end }}
yaml
# file: error-pages/middleware.yaml
{{ with .Values.errorPages }}
{{- if .enabled }}
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: {{ .appName }}
namespace: {{ .namespace }}
spec: # https://doc.traefik.io/traefik/routing/providers/kubernetes-crd/#kind-middleware
errors:
status: ["401", "403", "404", "500-599"]
service: {name: "{{ .appName }}-service", port: 8080}
query: "/{status}.html"
{{- end }}
{{- end }}
diff
traefik:
# ...
- globalArguments: []
+ globalArguments: ["--providers.kubernetescrd.allowCrossNamespace=true"]
# ...
diff
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: some-app-http
namespace: some-app
spec: # https://doc.traefik.io/traefik/routing/providers/kubernetes-crd/#kind-ingressroute
entryPoints: [websecure]
routes:
- match: Host(`my.awesome-site.com`) && PathPrefix(`/`)
services: [{name: "some-app-service", namespace: some-app, port: 8080}]
+ {{- with $.Values.errorPages }}{{ if .enabled }}
+ middlewares: [{name: "{{ .appName }}", namespace: "{{ .namespace }}"}]
+ {{- end }}{{ end }}
Hardware used:
RPS: ~180k 🔥 requests served without any errors, with peak memory usage ~60 MiB under the default configuration
shell
$ ulimit -aH | grep file
core file size (blocks, -c) unlimited
file size (blocks, -f) unlimited
open files (-n) 1048576
file locks (-x) unlimited
$ go build ./cmd/error-pages/ && ./error-pages --log-level warn serve
$ ./error-pages perftest # in separate terminal
Starting the test to bomb ONE PAGE (code). Please, be patient...
Test completed successfully. Here is the output:
Running 15s test @ http://127.0.0.1:8080/
12 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 4.52ms 6.43ms 94.34ms 85.44%
Req/Sec 15.76k 2.83k 29.64k 69.20%
2839632 requests in 15.09s, 32.90GB read
Requests/sec: 188185.61
Transfer/sec: 2.18GB
Starting the test to bomb DIFFERENT PAGES (codes). Please, be patient...
Test completed successfully. Here is the output:
Running 15s test @ http://127.0.0.1:8080/
12 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 6.75ms 13.71ms 252.66ms 91.94%
Req/Sec 14.06k 3.25k 26.39k 71.98%
2534473 requests in 15.10s, 29.22GB read
Requests/sec: 167899.78
Transfer/sec: 1.94GB
Usage:
$ error-pages [GLOBAL FLAGS] [COMMAND] [COMMAND FLAGS] [ARGUMENTS...]
Global flags:
Name | Description | Default value | Environment variables |
---|---|---|---|
--log-level="…" |
Logging level (debug/info/warn/error) | info |
LOG_LEVEL |
--log-format="…" |
Logging format (console/json) | console |
LOG_FORMAT |
serve
command (aliases: s
, server
, http
)Please start the HTTP server to serve the error pages. You can configure various options - please RTFM :D.
Usage:
$ error-pages [GLOBAL FLAGS] serve [COMMAND FLAGS] [ARGUMENTS...]
The following flags are supported:
Name | Description | Default value | Environment variables |
---|---|---|---|
--listen="…" (-l ) |
The HTTP server will listen on this IP (v4 or v6) address (set 127.0.0.1/::1 for localhost, 0.0.0.0 to listen on all interfaces, or specify a custom IP) | 0.0.0.0 |
LISTEN_ADDR |
--port="…" (-p ) |
The TCP port number for the HTTP server to listen on (0-65535) | 8080 |
LISTEN_PORT |
--add-template="…" |
To add a new template, provide the path to the file using this flag (the filename without the extension will be used as the template name) | [] |
ADD_TEMPLATE |
--disable-template="…" |
Disable the specified template by its name (useful to disable the built-in templates and use only custom ones) | [] |
none |
--add-code="…" |
To add a new HTTP status code, provide the code and its message/description using this flag (the format should be ‘%code%=%message%/%description%’; the code may contain a wildcard ‘‘ to cover multiple codes at once, for example, ‘4*‘ will cover all 4xx codes unless a more specific code is described previously) | map[] |
none |
--json-format="…" |
Override the default error page response in JSON format (Go templates are supported; the error page will use this template if the client requests JSON content type) | RESPONSE_JSON_FORMAT |
|
--xml-format="…" |
Override the default error page response in XML format (Go templates are supported; the error page will use this template if the client requests XML content type) | RESPONSE_XML_FORMAT |
|
--plaintext-format="…" |
Override the default error page response in plain text format (Go templates are supported; the error page will use this template if the client requests plain text content type or does not specify any) | RESPONSE_PLAINTEXT_FORMAT |
|
--template-name="…" (-t ) |
Name of the template to use for rendering error pages (built-in templates: app-down, cats, connection, ghost, hacker-terminal, l7, lost-in-space, noise, orient, shuffle) | app-down |
TEMPLATE_NAME |
--disable-l10n |
Disable localization of error pages (if the template supports localization) | false |
DISABLE_L10N |
--default-error-page="…" |
The code of the default (index page, when a code is not specified) error page to render | 404 |
DEFAULT_ERROR_PAGE |
--send-same-http-code |
The HTTP response should have the same status code as the requested error page (by default, every response with an error page will have a status code of 200) | false |
SEND_SAME_HTTP_CODE |
--show-details |
Show request details in the error page response (if supported by the template) | false |
SHOW_DETAILS |
--proxy-headers="…" |
HTTP headers listed here will be proxied from the original request to the error page response (comma-separated list) | X-Request-Id,X-Trace-Id,X-Amzn-Trace-Id |
PROXY_HTTP_HEADERS |
--rotation-mode="…" |
Templates automatic rotation mode (disabled/random-on-startup/random-on-each-request/random-hourly/random-daily) | disabled |
TEMPLATES_ROTATION_MODE |
--read-buffer-size="…" |
Per-connection buffer size in bytes for reading requests, this also limits the maximum header size (increase this buffer if your clients send multi-KB Request URIs and/or multi-KB headers (e.g., large cookies), note that increasing this value will increase memory consumption) | 5120 |
READ_BUFFER_SIZE |
--disable-minification |
Disable the minification of HTML pages, including CSS, SVG, and JS (may be useful for debugging) | false |
DISABLE_MINIFICATION |
build
command (aliases: b
)Build the static error pages and put them into a specified directory.
Usage:
$ error-pages [GLOBAL FLAGS] build [COMMAND FLAGS] [ARGUMENTS...]
The following flags are supported:
Name | Description | Default value | Environment variables |
---|---|---|---|
--add-template="…" |
To add a new template, provide the path to the file using this flag (the filename without the extension will be used as the template name) | [] |
ADD_TEMPLATE |
--disable-template="…" |
Disable the specified template by its name (useful to disable the built-in templates and use only custom ones) | [] |
none |
--add-code="…" |
To add a new HTTP status code, provide the code and its message/description using this flag (the format should be ‘%code%=%message%/%description%’; the code may contain a wildcard ‘‘ to cover multiple codes at once, for example, ‘4*‘ will cover all 4xx codes unless a more specific code is described previously) | map[] |
none |
--disable-l10n |
Disable localization of error pages (if the template supports localization) | false |
DISABLE_L10N |
--index (-i ) |
Generate index.html file with links to all error pages | false |
none |
--target-dir="…" (--out , --dir , -o ) |
Directory to put the built error pages into | . |
none |
--disable-minification |
Disable the minification of HTML pages, including CSS, SVG, and JS (may be useful for debugging) | false |
DISABLE_MINIFICATION |
healthcheck
command (aliases: chk
, health
, check
)Health checker for the HTTP server. The use case - docker health check.
Usage:
$ error-pages [GLOBAL FLAGS] healthcheck [COMMAND FLAGS] [ARGUMENTS...]
The following flags are supported:
Name | Description | Default value | Environment variables |
---|---|---|---|
--port="…" (-p ) |
TCP port number with the HTTP server to check | 8080 |
LISTEN_PORT |
I want to say a big thank you to everyone who contributed to this project:
If you encounter any bugs in the project, please create an issue in this repository.
This is open-sourced software licensed under the MIT License.