1. The problem I’m having:
I am trying to setup logic, that will render error pages issues by Caddy itself using the index.php, which is also used in case “everything is good”. Essentially equivalent of ErrorDocument 404 /httperror/404/
in Apache.
I tried doing this:
"errors": {
"routes": [
{
"handle": [
{
"handler": "rewrite",
"uri": "/httperror/{http.error.status_code}"
}
],
"terminal": true
}
]
}
but it does not seem to do anything at all.
This
"errors": {
"routes": [
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"/httperror/{http.error.status_code}"
]
},
"status_code": 308
}
],
"terminal": true
}
]
}
does work, so I know that it reaches this place, at least, and is able to execute things, but I do not need a redirect (unless it’s an internal one, similar to Apache).
I also tried
"errors": {
"routes": [
{
"handle": [
{
"handler": "rewrite",
"uri": "/httperror/{http.error.status_code}"
}
]
},
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.orig_uri.path}/"
]
},
"status_code": 308
}
],
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{
"path": [
"*/"
]
}
]
}
]
},
{
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
],
"match": [
{
"file": {
"split_path": [
".php"
],
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
]
}
}
]
},
{
"handle": [
{
"handler": "php",
"split_path": [
".php"
]
}
],
"match": [
{
"path": [
"*.php"
]
}
]
},
{
"handle": [
{
"handler": "file_server"
}
]
}
]
}
with calling PHP again, but it also did not seem to do anything.
A simple example with rewrite below config would be trying to access https://localhost/.htaccess (503 error, for some reason not visible in log) or https://localhost/sitemap/test2.xml (404 error, is visible in log).
Tried asking GPT, it suggests using reverse_proxy here, but I am not even sure it’s a valid suggestion, since (and it also does not seem to work, but I am unsure if I am setting it up properly, even)
2. Error messages and/or full log output:
No obvious errors, at least, not as far as I can see. Removed message about “adjusted config”, to fit forum limit
{"level":"info","ts":1718641456.964099,"logger":"admin","msg":"admin endpoint started","address":"localhost:2019","enforce_origin":false,"origins":["//localhost:2019","//[::1]:2019","//127.0.0.1:2019"]}
{"level":"info","ts":1718641456.966987,"logger":"http.auto_https","msg":"enabling automatic HTTP->HTTPS redirects","server_name":"server_0"}
{"level":"info","ts":1718641456.9670248,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0xc00006e700"}
{"level":"info","ts":1718641456.9993875,"msg":"FrankenPHP started 🐘","php_version":"8.3.8","num_threads":16}
{"level":"info","ts":1718641456.9995823,"logger":"pki.ca.local","msg":"root certificate is already trusted by system","path":"storage:pki/authorities/local/root.crt"}
{"level":"debug","ts":1718641456.9999049,"logger":"http","msg":"starting server loop","address":"[::]:80","tls":false,"http3":false}
{"level":"info","ts":1718641457.0001225,"logger":"http","msg":"enabling HTTP/3 listener","addr":":443"}
{"level":"debug","ts":1718641457.0006013,"logger":"http","msg":"starting server loop","address":"[::]:443","tls":true,"http3":true}
{"level":"info","ts":1718641457.0008163,"logger":"http.log","msg":"server running","name":"server_0","protocols":["h1","h2","h3"]}
{"level":"info","ts":1718641457.0010471,"logger":"http","msg":"enabling automatic TLS certificate management","domains":["localhost"]}
{"level":"info","ts":1718641457.0113857,"logger":"tls","msg":"storage cleaning happened too recently; skipping for now","storage":"FileStorage:/data/caddy","instance":"4c66bbff-e6dc-4806-aaa3-f33ad2a851ac","try_again":1718727857.0113842,"try_again_in":86399.99999976}
{"level":"info","ts":1718641457.0134008,"logger":"tls","msg":"finished cleaning storage units"}
{"level":"warn","ts":1718641457.0136337,"logger":"tls","msg":"stapling OCSP","error":"no OCSP stapling for [localhost]: no OCSP server specified in certificate","identifiers":["localhost"]}
{"level":"debug","ts":1718641457.0137696,"logger":"tls.cache","msg":"added certificate to cache","subjects":["localhost"],"expiration":1718678853,"managed":true,"issuer_key":"local","hash":"c36253111c2dfca8ed4102d8c686bfd11bc80044be04e791e12e236381256b5b","cache_size":1,"cache_capacity":10000}
{"level":"debug","ts":1718641457.013942,"logger":"events","msg":"event","name":"cached_managed_cert","id":"62debf41-0381-4a26-b563-2cf0a8f3a5c1","origin":"tls","data":{"sans":["localhost"]}}
{"level":"info","ts":1718641457.017641,"msg":"autosaved config (load with --resume flag)","file":"/config/caddy/autosave.json"}
{"level":"info","ts":1718641457.017788,"msg":"serving initial configuration"}
{"level":"debug","ts":1718641468.532496,"logger":"events","msg":"event","name":"tls_get_certificate","id":"13837e03-28ad-4ecd-b66e-fa343a84b08c","origin":"tls","data":{"client_hello":{"CipherSuites":[23130,4865,4866,4867,49195,49199,49196,49200,52393,52392,49171,49172,156,157,47,53],"ServerName":"localhost","SupportedCurves":[14906,25497,29,23,24],"SupportedPoints":"AA==","SignatureSchemes":[1027,2052,1025,1283,2053,1281,2054,1537],"SupportedProtos":["h2","http/1.1"],"SupportedVersions":[35466,772,771],"RemoteAddr":{"IP":"172.21.0.1","Port":57924,"Zone":""},"LocalAddr":{"IP":"172.21.0.3","Port":443,"Zone":""}}}}
{"level":"debug","ts":1718641468.532717,"logger":"tls.handshake","msg":"choosing certificate","identifier":"localhost","num_choices":1}
{"level":"debug","ts":1718641468.5329177,"logger":"tls.handshake","msg":"default certificate selection results","identifier":"localhost","subjects":["localhost"],"managed":true,"issuer_key":"local","hash":"c36253111c2dfca8ed4102d8c686bfd11bc80044be04e791e12e236381256b5b"}
{"level":"debug","ts":1718641468.5330834,"logger":"tls.handshake","msg":"matched certificate in cache","remote_ip":"172.21.0.1","remote_port":"57924","subjects":["localhost"],"managed":true,"expiration":1718678853,"hash":"c36253111c2dfca8ed4102d8c686bfd11bc80044be04e791e12e236381256b5b"}
{"level":"debug","ts":1718641468.5341866,"logger":"http.stdlib","msg":"http: TLS handshake error from 172.21.0.1:57924: remote error: tls: unknown certificate"}
{"level":"debug","ts":1718641468.5409431,"logger":"events","msg":"event","name":"tls_get_certificate","id":"a4c7d6c2-ce46-4031-b9b7-c8844c2f475f","origin":"tls","data":{"client_hello":{"CipherSuites":[27242,4865,4866,4867,49195,49199,49196,49200,52393,52392,49171,49172,156,157,47,53],"ServerName":"localhost","SupportedCurves":[19018,25497,29,23,24],"SupportedPoints":"AA==","SignatureSchemes":[1027,2052,1025,1283,2053,1281,2054,1537],"SupportedProtos":["h2","http/1.1"],"SupportedVersions":[35466,772,771],"RemoteAddr":{"IP":"172.21.0.1","Port":57934,"Zone":""},"LocalAddr":{"IP":"172.21.0.3","Port":443,"Zone":""}}}}
{"level":"debug","ts":1718641468.5411015,"logger":"tls.handshake","msg":"choosing certificate","identifier":"localhost","num_choices":1}
{"level":"debug","ts":1718641468.5412588,"logger":"tls.handshake","msg":"default certificate selection results","identifier":"localhost","subjects":["localhost"],"managed":true,"issuer_key":"local","hash":"c36253111c2dfca8ed4102d8c686bfd11bc80044be04e791e12e236381256b5b"}
{"level":"debug","ts":1718641468.5414119,"logger":"tls.handshake","msg":"matched certificate in cache","remote_ip":"172.21.0.1","remote_port":"57934","subjects":["localhost"],"managed":true,"expiration":1718678853,"hash":"c36253111c2dfca8ed4102d8c686bfd11bc80044be04e791e12e236381256b5b"}
{"level":"debug","ts":1718641481.667576,"logger":"http.handlers.file_server","msg":"sanitized path join","site_root":"/app/data","fs":"","request_path":"/sitemap/test2.xml","result":"/app/data/sitemap/test2.xml"}
{"level":"debug","ts":1718641481.6682606,"logger":"http.handlers.rewrite","msg":"rewrote request","request":{"remote_ip":"172.21.0.1","remote_port":"57934","client_ip":"172.21.0.1","proto":"HTTP/2.0","method":"GET","host":"localhost","uri":"/sitemap/test2.xml","headers":{"Accept-Encoding":["gzip, deflate, br, zstd"],"Accept-Language":["en-US,en;q=0.9,ru;q=0.8"],"Cookie":["REDACTED"],"Priority":["u=0, i"],"Sec-Fetch-Site":["none"],"Sec-Fetch-User":["?1"],"Sec-Fetch-Mode":["navigate"],"Sec-Ch-Ua-Platform":["\"Windows\""],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"Sec-Ch-Ua-Mobile":["?0"],"Dnt":["1"],"Upgrade-Insecure-Requests":["1"],"User-Agent":["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0"],"Sec-Fetch-Dest":["document"],"Cache-Control":["max-age=0"],"Sec-Ch-Ua":["\"Microsoft Edge\";v=\"125\", \"Chromium\";v=\"125\", \"Not.A/Brand\";v=\"24\""]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"h2","server_name":"localhost"}},"method":"GET","uri":"/httperror/404"}
{"level":"debug","ts":1718641481.668909,"logger":"http.handlers.file_server","msg":"sanitized path join","site_root":".","fs":"","request_path":"/httperror/404","result":"httperror/404"}
{"level":"error","ts":1718641481.6691475,"logger":"http.log.error","msg":"error handling handler error","request":{"remote_ip":"172.21.0.1","remote_port":"57934","client_ip":"172.21.0.1","proto":"HTTP/2.0","method":"GET","host":"localhost","uri":"/sitemap/test2.xml","headers":{"Sec-Ch-Ua-Platform":["\"Windows\""],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"Sec-Fetch-Mode":["navigate"],"Sec-Ch-Ua-Mobile":["?0"],"Dnt":["1"],"User-Agent":["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0"],"Sec-Fetch-Dest":["document"],"Cache-Control":["max-age=0"],"Sec-Ch-Ua":["\"Microsoft Edge\";v=\"125\", \"Chromium\";v=\"125\", \"Not.A/Brand\";v=\"24\""],"Upgrade-Insecure-Requests":["1"],"Accept-Language":["en-US,en;q=0.9,ru;q=0.8"],"Cookie":["REDACTED"],"Priority":["u=0, i"],"Sec-Fetch-Site":["none"],"Sec-Fetch-User":["?1"],"Accept-Encoding":["gzip, deflate, br, zstd"]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"h2","server_name":"localhost"}},"duration":0.028359804,"error":"{id=qixk641i4} fileserver.(*FileServer).notFound (staticfiles.go:651): HTTP 404","first_error":{"msg":"{id=cse0zhkiq} fileserver.(*FileServer).notFound (staticfiles.go:651): HTTP 404","status":404,"err_id":"cse0zhkiq","err_trace":"fileserver.(*FileServer).notFound (staticfiles.go:651)"}}
3. Caddy version:
v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk=
4. How I installed and ran Caddy:
a. System environment:
Docker based on FrankenPHP image (with JSON5 adapter for Caddy and some extra PHP extensions:
# Based on https://frankenphp.dev/docs/docker/
FROM dunglas/frankenphp:1.2.0-builder-php8.3.8-bookworm AS builder
# Copy xcaddy in the builder image
COPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy
# CGO must be enabled to build FrankenPHP
ENV CGO_ENABLED=1 XCADDY_SETCAP=1 XCADDY_GO_BUILD_FLAGS="-ldflags '-w -s'"
RUN xcaddy build \
--output /usr/local/bin/frankenphp \
--with github.com/dunglas/frankenphp=./ \
--with github.com/dunglas/frankenphp/caddy=./caddy/ \
# Mercure and Vulcain are included in the official build, but feel free to remove them
--with github.com/dunglas/caddy-cbrotli \
--with github.com/dunglas/mercure/caddy \
--with github.com/dunglas/vulcain/caddy \
# Add extra Caddy modules here
--with github.com/caddyserver/json5-adapter
FROM dunglas/frankenphp:1.2.0-php8.3.8-bookworm AS runner
# Replace the official binary by the one contained your custom modules
COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
# Add additional extensions
RUN install-php-extensions \
mysqli \
pdo_mysql \
gd \
zip \
brotli \
zstd \
apcu \
intl
# Completely custom
# Add composer
COPY --from=composer/composer /usr/bin/composer /usr/bin/composer
RUN composer self-update
# Git and unzip are useful for Composer
RUN apt-get -y update&&apt-get -y upgrade&&apt-get -y install git-all unzip&&apt-get -y autoremove
# Use custom caddy config
CMD ["--config", "/config/config.json5", "--adapter", "json5"]
b. Command:
N/A, accessing website from browser
c. Service/unit/compose file:
Removed local.simbiat.dev (this is MariaDB, not relevant to the issue)
name: simbiat-dev
# Looks like subnet may change in some cases for no apparent reason, so trying to force it
networks:
webserver:
driver: bridge
ipam:
config:
- subnet: 172.21.0.0/16
services:
frankenphp:
container_name: frankenphp
# uncomment the following line if you want to use a custom Dockerfile
build: ./config/frankenphp
# uncomment the following line if you want to run this in a production environment
restart: unless-stopped
env_file: "caddy.env"
networks:
- webserver
ports:
- "80:80" # HTTP
- "443:443" # HTTPS
- "443:443/udp" # HTTP/3
volumes:
- ./:/app:rw
- ./config/frankenphp:/usr/local/php/config:ro
- ./data/caddy:/data:rw
- ./config/caddy:/config:rw
links:
- local.simbiat.dev:mysql
- local.simbiat.dev:database
# comment the following line in production, it allows to have nice human-readable logs in dev
tty: true
d. My complete Caddy config:
JSON5 config below. Removed a huge chunk where I am forcing MIME types, since it’s definitely irrelevant, and with it config is 18k+ lines. And later at editing stage removed even more (headers, some redirects and some rewrites) to meet the forum limit.
{
"logging": {
"logs": {
"default": {
"writer": {
"filename": "/app/caddy.log",
"output": "file",
"roll": true,
"roll_size_mb": 10,
"roll_keep": 10,
"roll_keep_days": 30
},
"level": "WARN"
}
}
},
"apps": {
"tls": {
"automation": {
"policies": [
{
"issuers": [
{
"module": "acme",
"email": "simbiat@outlook.com"
}
],
"must_staple": true
}
]
}
},
"frankenphp": {},
"http": {
"grace_period": "30s",
"shutdown_delay": "10s",
"servers": {
"server_0": {
"listen": [
":80",
":443"
],
"read_timeout": "10s",
"read_header_timeout": "10s",
"write_timeout": "90s",
"routes": [
{
"match": [
{
"host": [
"{env.WEB_SERVER_HOST}"
]
}
],
"handle": [
//Some redirects
{
"handler": "subroute",
"routes": [
//Sitemap with format (legacy)
{
"match": [
{
"path_regexp": {
"pattern": "/sitemap/(xml|txt|html)(.*)",
"name": "sitemapFormat"
}
}
],
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"/sitemap{http.regexp.sitemapFormat.2}"
]
},
"status_code": 308
}
]
},
//Sitemap without file specified
{
"match": [
{
"path_regexp": {
"pattern": "/sitemap/?$",
"name": "sitemapNoFile"
}
}
],
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"/sitemap/index.xml"
]
},
"status_code": 308
}
]
},
//Sitemap without .xml
{
"match": [
{
//Negative look behind does not seem to work here
//Thrown "invalid named capture" error, so using "not", too
"path_regexp": {
"pattern": "/sitemap/(.+)$",
"name": "sitemapNoExtension"
},
"not": [
{
"path": [
"*.xml"
]
}
]
}
],
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"/sitemap/{http.regexp.sitemapNoExtension.1}.xml"
]
},
"status_code": 308
}
]
}
]
},
//Rewrite rules
{
"handler": "subroute",
"routes": [
//Sitemap (This is an attempt to force Google Search recognize the sitemaps)
{
"match": [
{
"path": [
"/sitemap.xml"
]
}
],
"handle": [
{
"handler": "rewrite",
"uri": "/sitemap/index.xml"
}
]
},
//Feed sitemaps from actual data directory
{
"match": [
{
"path_regexp": {
"name": "sitemapRewrite",
"pattern": "/sitemap/(.+)"
}
}
],
"handle": [
{
"handler": "file_server",
"root": "/app/data"
}
]
}
]
},
{
"handler": "subroute",
"routes": [
//Deny TRACE method for all requests and POST method for requests using HTTP below 1.1
{
"handle": [
{
"handler": "static_response",
"status_code": 405,
"close": true
}
],
"match": [
{
"protocol": "http/0.9",
"method": [
"POST"
]
},
{
"protocol": "http/1.0",
"method": [
"POST"
]
},
{
"method": [
"TRACE"
]
}
]
},
//Deny access to certain folders. They should not be in /public, but just as a precaution
{
"handle": [
{
"handler": "static_response",
"status_code": 403
}
],
"match": [
{
"path": [
"/config*",
"/lib*",
"/node_modules*",
"/twig*",
"/vendor*",
"/.*",
"/data*"
],
"not": [
{
"path": [
//Access to TinyMCE folder is required for loading of the editor components
"/vendor/tinymce/*",
//well-known folder is allowed to be accessed
"/.well-known/*",
//It is ok to access certain data folders
"/data/sitemap*",
"/data/mergedcrests*",
"/data/ffstatistics*",
"/data/uploaded*",
"/data/uploadedimages*"
]
}
]
}
]
},
//Compression settings
{
"handle": [
{
"handler": "vars",
"root": "public/"
},
{
"handler": "encode",
"encodings": {
"br": {
"quality": 6,
"lgwin": 0
},
"gzip": {
"level": 6
},
"zstd": {}
},
"prefer": [
"zstd",
"br",
"gzip"
],
"minimum_length": 256,
"match": {
"headers": {
"Content-Type": [
"application/atom+xml*",
"application/eot*",
"application/font*",
"application/geo+json*",
"application/graphql+json*",
"application/javascript*",
"application/json*",
"application/ld+json*",
"application/manifest+json*",
"application/opentype*",
"application/otf*",
"application/rdf+xml",
"application/rss+xml*",
"application/schema+json",
"application/truetype*",
"application/ttf*",
"application/vnd.api+json*",
"application/vnd.ms-fontobject*",
"application/wasm*",
"application/x-font-ttf",
"application/xhtml+xml*",
"application/x-httpd-cgi*",
"application/x-javascript*",
"application/xml*",
"application/x-opentype*",
"application/x-otf*",
"application/x-perl*",
"application/x-protobuf*",
"application/x-ttf*",
"application/x-web-app-manifest+json",
"font/*",
"image/bmp",
"image/svg+xml*",
"image/vnd.microsoft.icon*",
"image/x-icon*",
"multipart/bag*",
"multipart/mixed*",
"text/*"
]
}
}
}
]
},
//FrankenPHP defaults
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.orig_uri.path}/"
]
},
"status_code": 308
}
],
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{
"path": [
"*/"
]
}
]
}
]
},
{
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
],
"match": [
{
"file": {
"split_path": [
".php"
],
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
]
}
}
]
},
{
"handle": [
{
"handler": "php",
"split_path": [
".php"
]
}
],
"match": [
{
"path": [
"*.php"
]
}
]
},
{
"handle": [
{
"handler": "file_server"
}
]
}
]
}
],
"terminal": true
}
],
"errors": {
"routes": [
{
"handle": [
{
"handler": "rewrite",
"uri": "/httperror/{http.error.status_code}"
}
]
},
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.orig_uri.path}/"
]
},
"status_code": 308
}
],
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{
"path": [
"*/"
]
}
]
}
]
},
{
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
],
"match": [
{
"file": {
"split_path": [
".php"
],
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
]
}
}
]
},
{
"handle": [
{
"handler": "php",
"split_path": [
".php"
]
}
],
"match": [
{
"path": [
"*.php"
]
}
]
},
{
"handle": [
{
"handler": "file_server"
}
]
}
]
}
}
}
}
}
}
5. Links to relevant resources:
N/A