Global 404 handler seems to be harder than expected

Hi,

having a global 404 handler that actually works and does https seemst to be harder than I originally expected. After reading the numerous threads about this topic, I am even more confused now than I appeared to be before.

I am using docker compose, and would like to have a prettier 404 error page than just a plain "404 not found” when the compose project serving that URL poiting to my traefik in DNS isn’t running.

Since I don’t know what might be important I am not trying to simplify things, but instead am pasting the whole thing:

docker-compose.yml:

services:
traefik:
image: traefik:latest
container_name: traefik
read_only: true
mem_limit: 2G
cpus: 0.75
env_file:
- .env
restart: unless-stopped
security_opt:
- no-new-privileges:true
depends_on:
- dockerproxy
volumes:
- "./traefik:/etc/traefik:ro"
- "${VOLUME_BASE_DIR}/${PROJECT_NAME}/socket:/socket:ro"
- "${VOLUME_BASE_DIR}/${PROJECT_NAME}/letsencrypt:/letsencrypt"
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"

networks: 
  - traefik_proxy 
ports: 
  - 80:80 
  - 10080:10080 
  - 443:443 
  - 10443:10443 

healthcheck: 
  test: \["CMD", "wget", "http://localhost:8082/ping", "--spider"\] 
  interval: 10s 
  timeout: 5s 
  retries: 3 
  start_period: 5s 
labels: 
  - "traefik.enable=true" 
  # A router to expose the Traefik dashboard 
  - "traefik.http.routers.dashboard.rule=Host(\`${TRAEFIK_HOSTNAME}\`)" 
  - "traefik.http.routers.dashboard.entrypoints=websecure" 
  - "traefik.http.routers.dashboard.tls=true" 
  - "traefik.http.routers.dashboard.tls.certresolver=le" 
  - "traefik.http.routers.dashboard.service=api@internal" 
  # Basic Authentication for the Traefik dashboard 
  - "traefik.http.routers.dashboard.middlewares=dashboard-auth@docker" 
  - "traefik.http.middlewares.dashboard-auth.basicauth.users=${TRAEFIK_BASIC_AUTH}" 
  # Specify the internal server port to the dashboard service 
  - "traefik.http.services.dashboard.loadbalancer.server.port=8080" 
  # Pass the original Host header to the backend 
  - "traefik.http.services.dashboard.loadbalancer.passhostheader=true" 

dockerproxy:
image: wollomatic/socket-proxy:1
restart: unless-stopped
user: "65534:999"
mem_limit: 64M
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges
command:
- '-loglevel=info'
- '-listenip=0.0.0.0'
- '-allowfrom=traefik'
- '-allowHEAD=/_ping'
- '-allowGET=/v1\..{1,2}/(version|containers/.*|events.*)'
- '-allowbindmountfrom=/var/log,/tmp'
- '-proxysocketendpoint=/socket/traefik.sock'
- '-proxysocketendpointfilemode=0600'
- '-watchdoginterval=3600'
- '-stoponwatchdog'
- '-shutdowngracetime=5'
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "${VOLUME_BASE_DIR}/${PROJECT_NAME}/socket:/socket"

whoami:
image: traefik/whoami
container_name: whoami
restart: unless-stopped
networks:
- traefik_proxy
# no health check, the image has nothing
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.rule=Host(`${TRAEFIK_HOSTNAME}`) && PathPre
fix(`/whoami`)"
- "traefik.http.middlewares.whoami-stripprefix.stripprefix.prefixes=/whoam
i"
- "traefik.http.routers.whoami.middlewares=whoami-stripprefix"
- "traefik.http.routers.whoami.entrypoints=websecure"
- "traefik.http.routers.whoami.tls=true"
- "traefik.http.routers.whoami.tls.certresolver=le"

sorry:
image: nginx:alpine
container_name: traefik_sorry
restart: unless-stopped
volumes:
- ./sorry:/usr/share/nginx/html:ro
networks:
- traefik_proxy
healthcheck:
test: ["CMD", "wget", "http://localhost/sorry.html", "--spider"]
interval: 60s
timeout: 5s
retries: 3
start_period: 5s
labels:
- "traefik.enable=true"
# Wildcard router: matches any hostname
- "traefik.http.routers.sorry.rule=HostRegexp(`{host:(.*?)}`)"
- "traefik.http.routers.sorry.entrypoints=websecure,proxysecure"
- "traefik.http.routers.sorry.service=sorry-service@file"
- "traefik.http.routers.sorry.tls=true"
- "traefik.http.routers.sorry.tls.certresolver=le"
- "traefik.http.routers.sorry.priority=1"

networks:
traefik_proxy:
external: true

traefik/taefik.yml

api:
dashboard: true
insecure: false

log:
level: INFO
accesslog: {}

providers:
docker:
endpoint: "unix:///socket/traefik.sock"
exposedByDefault: false
network: traefik_proxy
file:
filename: /etc/traefik/dynamic.yml

ping:
entryPoint: ping

entryPoints:
ping:
address: ":8082"

web:
address: ":80"
forwardedHeaders:
trustedIPs:
- "172.21.0.0/24"
http:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true

proxyweb:
address: ":10080"
forwardedHeaders:
trustedIPs:
- "172.21.0.0/24"
http:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true
proxyProtocol:
insecure: true
trustedIPs:
- "192.168.80.254/32"

websecure:
address: ":443"
forwardedHeaders:
trustedIPs:
- "172.21.0.0/24"
http:
middlewares:
- security-headers@file
- sorry-errorpage@file

proxysecure:
address: ":10443"
forwardedHeaders:
trustedIPs:
- "172.21.0.0/24"
http:
middlewares:
- security-headers@file
- sorry-errorpage@file
proxyProtocol:
insecure: true
trustedIPs:
- "192.168.80.254/32"

certificatesResolvers:
le:
acme:
email: ${TRAEFIK_ACME_EMAIL}
storage: /letsencrypt/acme.json
tlsChallenge: {}

traefik/dynamic.yml

http:
services:
sorry-service:
loadBalancer:
servers:
- url: "https://traefik_sorry:80/sorry.html"

middlewares:
security-headers:
headers:
#stsSeconds: 31536000 # 1 year
stsSeconds: 604800
stsIncludeSubdomains: true
forceSTSHeader: true
frameDeny: true
contentTypeNosniff: true
browserXssFilter: true
customFrameOptionsValue: "SAMEORIGIN"
referrerPolicy: "strict-origin-when-cross-origin"
permissionsPolicy: "camera=(), microphone=(), geolocation=()"

sorry-errorpage: 
  errors: 
    status: 
      - "404" 
      - "502" 
      - "503" 
    service: "sorry-service@file" 
    query: "/sorry/sorry.html"

Here is my web page directory:

$ ls -al sorry/
total 1,2M
drwxrwsr-x 2 mh dockeradm 4,0K Dez 26 11:03 ./
drwxr-sr-x 4 mh dockeradm 4,0K Dez 27 17:52 ../
-rw-rw-r-- 1 mh dockeradm 1,2M Dez 26 11:03 comiczug.png
-rw-rw-r-- 1 mh dockeradm 1,7K Dez 26 11:03 sorry.html

traefik logs:

:check_mark: Container traefik_sorry Recreated 0.0s
Attaching to traefik, dockerproxy-1, traefik_sorry, whoami
whoami | 2025/12/27 16:56:01 Starting up on port 80
dockerproxy-1 | time=2025-12-27T16:56:01.541Z level=INFO msg="starting socket-proxy" version=1.10.1 os=linux
arch=amd64 runtime=go1.25.4 URL=github.com/wollomatic/socket-proxy
dockerproxy-1 | time=2025-12-27T16:56:01.542Z level=INFO msg="configuration info" socketpath=/var/run/docker
.sock proxysocketendpoint=/socket/traefik.sock proxysocketendpointfilemode=-rw------- loglevel=INFO logjson=f
alse shutdowngracetime=5
dockerproxy-1 | time=2025-12-27T16:56:01.542Z level=INFO msg="proxysocketendpoint is set, so the TCP listene
r is deactivated"
dockerproxy-1 | time=2025-12-27T16:56:01.542Z level=INFO msg="watchdog enabled" interval=3600 stoponwatchdog
=true
dockerproxy-1 | time=2025-12-27T16:56:01.542Z level=INFO msg="Docker bind mount restrictions enabled" allowb
indmountfrom="[/var/log /tmp]"
dockerproxy-1 | Request allowlist:
dockerproxy-1 | Method Regex
dockerproxy-1 | GET ^/v1\..{1,2}/(version|containers/.*|events.*)$
dockerproxy-1 | HEAD ^/_ping$
dockerproxy-1 | time=2025-12-27T16:56:01.542Z level=INFO msg="socket-proxy running and listening..."
traefik_sorry | /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configura
tion
traefik_sorry | /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
traefik_sorry | /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
traefik_sorry | 10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.con
f
traefik_sorry | 10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.c
onf
traefik_sorry | /docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
traefik_sorry | /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
traefik_sorry | /docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
traefik_sorry | /docker-entrypoint.sh: Configuration complete; ready for start up
traefik_sorry | 2025/12/27 16:56:01 [notice] 1#1: using the "epoll" event method
traefik_sorry | 2025/12/27 16:56:01 [notice] 1#1: nginx/1.29.4
traefik_sorry | 2025/12/27 16:56:01 [notice] 1#1: built by gcc 15.2.0 (Alpine 15.2.0)
traefik_sorry | 2025/12/27 16:56:01 [notice] 1#1: OS: Linux 6.18-zgsrv20080
traefik_sorry | 2025/12/27 16:56:01 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1024:524288
traefik_sorry | 2025/12/27 16:56:01 [notice] 1#1: start worker processes
traefik_sorry | 2025/12/27 16:56:01 [notice] 1#1: start worker process 30
traefik_sorry | 2025/12/27 16:56:01 [notice] 1#1: start worker process 31
traefik | 2025-12-27T16:56:01Z WRN Starting with v3.6.3, Traefik now rejects some encoded characters i
n the request path by default. Refer to the documentation for more details: https://doc.traefik.io/traefik/mi
grate/v3/#encoded-characters-in-request-path
traefik | 2025-12-27T16:56:01Z INF Traefik version 3.6.5 built on 2025-12-16T14:56:48Z version=3.6.5
traefik | 2025-12-27T16:56:01Z INF Enabling ProxyProtocol without trusted IPs: Insecure entryPointName
=proxysecure
traefik | 2025-12-27T16:56:01Z INF Enabling ProxyProtocol without trusted IPs: Insecure entryPointName
=proxyweb
traefik | 2025-12-27T16:56:01Z INF Starting provider aggregator *aggregator.ProviderAggregator
traefik | 2025-12-27T16:56:01Z INF Starting provider *file.Provider
traefik | 2025-12-27T16:56:01Z INF Starting provider *traefik.Provider
traefik | 2025-12-27T16:56:01Z INF Starting provider *docker.Provider
traefik | 2025-12-27T16:56:01Z INF Starting provider *acme.ChallengeTLSALPN
traefik | 2025-12-27T16:56:01Z INF Starting provider *acme.Provider
traefik | 2025-12-27T16:56:01Z INF Testing certificate renew... acmeCA=https://acme-v02.api.letsencryp
t.org/directory providerName=le.acme
traefik_sorry | ::1 - - [27/Dec/2025:16:56:06 +0000] "GET /sorry.html HTTP/1.1" 200 1719 "-" "Wget" "-"
traefik | 2025-12-27T16:56:06Z WRN No domain found in rule HostRegexp(`{host:(.*?)}`), the TLS options
applied for this router will depend on the SNI of each request entryPointName=proxysecure routerName=proxyse
cure-sorry@docker
traefik | 2025-12-27T16:56:06Z WRN No domain found in rule HostRegexp(`{host:(.*?)}`), the TLS options
applied for this router will depend on the SNI of each request entryPointName=websecure routerName=websecure
-sorry@docker
traefik | 2025-12-27T16:56:09Z WRN No domain found in rule HostRegexp(`{host:(.*?)}`), the TLS options
applied for this router will depend on the SNI of each request entryPointName=proxysecure routerName=proxyse
cure-sorry@docker
traefik | 2025-12-27T16:56:09Z WRN No domain found in rule HostRegexp(`{host:(.*?)}`), the TLS options
applied for this router will depend on the SNI of each request entryPointName=websecure routerName=websecure
-sorry@docker
traefik | 172.21.0.1 - - [27/Dec/2025:16:56:13 +0000] "GET / HTTP/2.0" 404 19 "-" "-" 1 "-" "-" 0ms
traefik_sorry | ::1 - - [27/Dec/2025:17:03:07 +0000] "GET /sorry.html HTTP/1.1" 200 1719 "-" "Wget" "-"

What am I doing wrong here?

Greetings, Marc

You need to use 3 backticks before and after code/config to format it correctly, preserving spacing.

:wink: