1. The problem I’m having:
Hello! I’ve been working on a highly custom Nextcloud stack for some time now, and at this point I have most things working except for http/2. I’ve tried a few different ways of accomplishing this, from hosting the web server with TLS directly to changing configurations around to attempt to force h2c.
I’m running the php-fpm version of Nextcloud with Caddy running the webserver through a php socket. I’ve managed to get internally managed TLS to work fine locally a couple times, but other than that I can’t get it to work through my reverse proxy, which is tsdproxy. So far this is the one problem that’s left me truly puzzled.
2. Error messages and/or full log output:
{"level":"info","ts":1743571160.791983,"logger":"http.log","msg":"server running","name":"srv0","protocols":["h1","h2c"]}
{"level":"info","ts":1743571160.7944508,"logger":"tls","msg":"storage cleaning happened too recently; skipping for now","storage":"FileStorage:/data/caddy","instance":"f781f668-54e7-4c38-b969-f025dcc1cbe9","try_again":1743657560.7944489,"try_again_in":86399.999999655}
{"level":"info","ts":1743571160.7945523,"logger":"tls","msg":"finished cleaning storage units"}
{"level":"info","ts":1743571160.8629727,"msg":"autosaved config (load with --resume flag)","file":"/config/caddy/autosave.json"}
{"level":"info","ts":1743571160.8630567,"msg":"serving initial configuration"}
{"level":"debug","ts":1743571168.10378,"logger":"http.handlers.rewrite","msg":"rewrote request","request":{"remote_ip":"174.39.0.10","remote_port":"52682","client_ip":"100.115.173.26","proto":"HTTP/1.1","method":"GET","host":"cloud.possum-godzilla.ts.net","uri":"/index.php","headers":{"User-Agent":["Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"],"Accept-Encoding":["gzip, deflate, br, zstd"],"X-Forwarded-Proto":["https"],"Cookie":["REDACTED"],"Sec-Fetch-Dest":["document"],"Sec-Fetch-Site":["none"],"Sec-Gpc":["1"],"Accept-Language":["en-US,en;q=0.5"],"Priority":["u=0, i"],"Sec-Fetch-Mode":["navigate"],"X-Forwarded-For":["100.115.173.26"],"Sec-Fetch-User":["?1"],"Upgrade-Insecure-Requests":["1"],"X-Forwarded-Host":["cloud.possum-godzilla.ts.net"]}},"method":"GET","uri":"/index.php"}
{"level":"debug","ts":1743571168.1039574,"logger":"http.handlers.reverse_proxy","msg":"selected upstream","dial":"/var/run/nextcloud/php-fpm.sock","total_upstreams":1}
{"level":"debug","ts":1743571168.1046672,"logger":"http.handlers.reverse_proxy","msg":"upstream roundtrip","upstream":"unix//var/run/nextcloud/php-fpm.sock","duration":0.000537566,"request":{"remote_ip":"174.39.0.10","remote_port":"52682","client_ip":"100.115.173.26","proto":"HTTP/1.1","method":"GET","host":"cloud.possum-godzilla.ts.net","uri":"/index.php","headers":{"X-Forwarded-Proto":["https"],"Sec-Fetch-Site":["none"],"Sec-Fetch-Dest":["document"],"Accept-Language":["en-US,en;q=0.5"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Priority":["u=0, i"],"X-Forwarded-For":["100.115.173.26, 174.39.0.10"],"X-Forwarded-Host":["cloud.possum-godzilla.ts.net"],"Sec-Fetch-User":["?1"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"],"Sec-Gpc":["1"],"Cookie":["REDACTED"],"Upgrade-Insecure-Requests":["1"],"User-Agent":["Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0"],"Sec-Fetch-Mode":["navigate"]}},"error":"read unix @->/var/run/nextcloud/php-fpm.sock: read: connection reset by peer"}
{"level":"error","ts":1743571168.1048815,"logger":"http.log.error","msg":"read unix @->/var/run/nextcloud/php-fpm.sock: read: connection reset by peer","request":{"remote_ip":"174.39.0.10","remote_port":"52682","client_ip":"100.115.173.26","proto":"HTTP/1.1","method":"GET","host":"cloud.possum-godzilla.ts.net","uri":"/","headers":{"Sec-Gpc":["1"],"X-Forwarded-For":["100.115.173.26"],"Sec-Fetch-Mode":["navigate"],"User-Agent":["Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Cookie":["REDACTED"],"Sec-Fetch-Site":["none"],"Upgrade-Insecure-Requests":["1"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"],"X-Forwarded-Proto":["https"],"Accept-Language":["en-US,en;q=0.5"],"Priority":["u=0, i"],"Sec-Fetch-User":["?1"],"X-Forwarded-Host":["cloud.possum-godzilla.ts.net"],"Sec-Fetch-Dest":["document"]}},"duration":0.001605305,"status":502,"err_id":"p8bdzn5fi","err_trace":"reverseproxy.statusError (reverseproxy.go:1373)"}
{"level":"debug","ts":1743571169.126225,"logger":"http.handlers.file_server","msg":"sanitized path join","site_root":"/var/www/html","fs":"","request_path":"/index.php/apps/files/preview-service-worker.js","result":"/var/www/html/index.php/apps/files/preview-service-worker.js"}
{"level":"debug","ts":1743571169.1267302,"logger":"http.handlers.rewrite","msg":"rewrote request","request":{"remote_ip":"174.39.0.10","remote_port":"52682","client_ip":"100.115.173.26","proto":"HTTP/1.1","method":"GET","host":"cloud.possum-godzilla.ts.net","uri":"/index.php","headers":{"Cookie":["REDACTED"],"X-Forwarded-Host":["cloud.possum-godzilla.ts.net"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Accept-Language":["en-US,en;q=0.5"],"Sec-Fetch-Dest":["serviceworker"],"X-Forwarded-For":["100.115.173.26"],"Accept":["*/*"],"Priority":["u=4"],"Sec-Fetch-Site":["same-origin"],"Sec-Gpc":["1"],"Service-Worker":["script"],"User-Agent":["Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0"],"Cache-Control":["max-age=0"],"Sec-Fetch-Mode":["same-origin"],"X-Forwarded-Proto":["https"]}},"method":"GET","uri":"/index.php"}
{"level":"debug","ts":1743571169.1268878,"logger":"http.handlers.reverse_proxy","msg":"selected upstream","dial":"/var/run/nextcloud/php-fpm.sock","total_upstreams":1}
{"level":"debug","ts":1743571169.1275764,"logger":"http.handlers.reverse_proxy","msg":"upstream roundtrip","upstream":"unix//var/run/nextcloud/php-fpm.sock","duration":0.000600114,"request":{"remote_ip":"174.39.0.10","remote_port":"52682","client_ip":"100.115.173.26","proto":"HTTP/1.1","method":"GET","host":"cloud.possum-godzilla.ts.net","uri":"/index.php","headers":{"Sec-Fetch-Site":["same-origin"],"Sec-Fetch-Mode":["same-origin"],"X-Forwarded-For":["100.115.173.26, 174.39.0.10"],"Accept-Language":["en-US,en;q=0.5"],"Accept":["*/*"],"Cache-Control":["max-age=0"],"X-Forwarded-Proto":["https"],"X-Forwarded-Host":["cloud.possum-godzilla.ts.net"],"Sec-Gpc":["1"],"User-Agent":["Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0"],"Priority":["u=4"],"Cookie":["REDACTED"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Sec-Fetch-Dest":["serviceworker"],"Service-Worker":["script"]}},"error":"read unix @->/var/run/nextcloud/php-fpm.sock: read: connection reset by peer"}
{"level":"error","ts":1743571169.1277883,"logger":"http.log.error","msg":"read unix @->/var/run/nextcloud/php-fpm.sock: read: connection reset by peer","request":{"remote_ip":"174.39.0.10","remote_port":"52682","client_ip":"100.115.173.26","proto":"HTTP/1.1","method":"GET","host":"cloud.possum-godzilla.ts.net","uri":"/index.php/apps/files/preview-service-worker.js","headers":{"Service-Worker":["script"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Accept":["*/*"],"Sec-Gpc":["1"],"Priority":["u=4"],"Cache-Control":["max-age=0"],"X-Forwarded-Proto":["https"],"X-Forwarded-Host":["cloud.possum-godzilla.ts.net"],"Sec-Fetch-Site":["same-origin"],"User-Agent":["Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0"],"Sec-Fetch-Mode":["same-origin"],"Cookie":["REDACTED"],"Accept-Language":["en-US,en;q=0.5"],"Sec-Fetch-Dest":["serviceworker"],"X-Forwarded-For":["100.115.173.26"]}},"duration":0.001771376,"status":502,"err_id":"3p8kiehmj","err_trace":"reverseproxy.statusError (reverseproxy.go:1373)"}
3. Caddy version:
v2.9.1 h1:OEYiZ7DbCzAWVb6TNEkjRcSCRGHVoZsJinoDR/n9oaY=
4. How I installed and ran Caddy:
As a docker compose stack.
a. System environment:
Host OS is Ubuntu 24.04.2 LTS
b. Command:
c. Service/unit/compose file:
networks:
nc-internal:
name: nc-internal
ipam:
driver: default
config:
- subnet: 174.53.0.0/24
tsdproxy-net:
external: true
services:
nc-web:
image: caddy:${CADDY_VERSION_TAG}
container_name: nc-web
ports:
- ${NEXTCLOUD_PORT}:80
networks:
nc-internal:
ipv4_address: 174.53.0.10
tsdproxy-net:
ipv4_address: 174.39.0.53
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- ./caddy:/data
- ./config:/var/www/html
- ./sockets/nextcloud:/var/run/nextcloud
- ./sockets/notify-push:/var/run/notify-push
restart: always
#depends_on:
#nc-app:
#condition: service_healthy
environment:
- HOST_HOSTNAME=${HOST_HOSTNAME}
- TRUSTED_PROXIES=${TRUSTED_PROXIES}
- PHP_UPLOAD_LIMIT=${PHP_UPLOAD_LIMIT}
labels:
- tsdproxy.enable=true
- tsdproxy.name=cloud
- tsdproxy.dash.icon=si/nextcloud
- tsdproxy.container_port=80
- tsdproxy.scheme=http
- tsdproxy.funnel=true
. . .
d. My complete Caddy config:
The key line here is php_fastcgi unix+h2c//var/run/nextcloud/php-fpm.sock {
This seems like the correct way to do it from what I can tell, however this just results in the connection reset by peer
error. It works fine if the +h2c
part it omitted.
{
log {
output file /data/access.log
level DEBUG
}
servers {
trusted_proxies static {$TRUSTED_PROXIES}
trusted_proxies_strict
client_ip_headers X-Forwarded-For X-Real-IP
protocols h1 h2c
}
}
:80 {
request_body {
max_size {$PHP_UPLOAD_LIMIT}
}
# Enable gzip but do not remove ETag headers
encode {
zstd
gzip 4
minimum_length 256
match {
header Content-Type application/atom+xml
header Content-Type application/javascript
header Content-Type application/json
header Content-Type application/ld+json
header Content-Type application/manifest+json
header Content-Type application/rss+xml
header Content-Type application/vnd.geo+json
header Content-Type application/vnd.ms-fontobject
header Content-Type application/wasm
header Content-Type application/x-font-ttf
header Content-Type application/x-web-app-manifest+json
header Content-Type application/xhtml+xml
header Content-Type application/xml
header Content-Type font/opentype
header Content-Type image/bmp
header Content-Type image/svg+xml
header Content-Type image/x-icon
header Content-Type text/cache-manifest
header Content-Type text/css
header Content-Type text/plain
header Content-Type text/vcard
header Content-Type text/vnd.rim.location.xloc
header Content-Type text/vtt
header Content-Type text/x-component
header Content-Type text/x-cross-domain-policy
}
}
header {
# Based on following source:
# https://raw.githubusercontent.com/nextcloud/docker/refs/heads/master/.examples/docker-compose/insecure/mariadb/fpm/web/nginx.conf
#
# HSTS settings
# WARNING: Only add the preload option once you read about
# the consequences in https://hstspreload.org/. This option
# will add the domain to a hardcoded list that is shipped
# in all major browsers and getting removed from this list
# could take several months.
# Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;"
Strict-Transport-Security: "max-age=31536000; includeSubDomains;"
# HTTP response headers borrowed from Nextcloud `.htaccess`
Referrer-Policy no-referrer
X-Content-Type-Options nosniff
X-Download-Options noopen
X-Frame-Options SAMEORIGIN
X-Permitted-Cross-Domain-Policies none
X-Robots-Tag "noindex,nofollow"
X-XSS-Protection "1; mode=block"
Permissions-Policy "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()"
}
# Path to the root of your installation
root * /var/www/html
handle_path /push/* {
reverse_proxy unix//var/run/notify-push/notify-push.sock
}
route {
# Rule borrowed from `.htaccess` to handle Microsoft DAV clients
@msftdavclient {
header User-Agent DavClnt*
path /
}
redir @msftdavclient /remote.php/webdav/ temporary
route /robots.txt {
log_skip
file_server
}
# Add exception for `/.well-known` so that clients can still access it
# despite the existence of the `error @internal 404` rule which would
# otherwise handle requests for `/.well-known` below
route /.well-known/* {
redir /.well-known/carddav /remote.php/dav/ permanent
redir /.well-known/caldav /remote.php/dav/ permanent
@well-known-static path \
/.well-known/acme-challenge /.well-known/acme-challenge/* \
/.well-known/pki-validation /.well-known/pki-validation/*
route @well-known-static {
try_files {path} {path}/ =404
file_server
}
redir * /index.php{path} permanent
}
@internal path \
/build /build/* \
/tests /tests/* \
/config /config/* \
/lib /lib/* \
/3rdparty /3rdparty/* \
/templates /templates/* \
/data /data/* \
\
/.* \
/autotest* \
/occ* \
/issue* \
/indie* \
/db_* \
/console*
error @internal 404
@assets {
path *.css *.js *.mjs *.js.map *.svg *.gif *.png *.jpg *.jpeg *.ico *.wasm *.tflite *.map *.wasm2
file {path} # Only if requested file exists on disk, otherwise /index.php will take care of it
}
route @assets {
header /* Cache-Control "max-age=15552000" # Cache-Control policy borrowed from `.htaccess`
header /*.woff2 Cache-Control "max-age=604800" # Cache-Control policy borrowed from `.htaccess`
log_skip # Optional: Don't log access to assets
file_server {
precompressed gzip
}
}
# Rule borrowed from `.htaccess`
redir /remote/* /remote.php{path} permanent
# Serve found static files, continuing to the PHP default handler below if not found
try_files {path} {path}/
@notphpordir not path /*.php /*.php/* / /*/
file_server @notphpordir {
pass_thru
}
# Required for legacy support
#
# Rewrites all other requests to be prepended by “/index.php” unless they match a known-valid PHP file path.
@notlegacy {
path *.php *.php/
not path /index*
not path /remote*
not path /public*
not path /cron*
not path /core/ajax/update*
not path /status*
not path /ocs/v1*
not path /ocs/v2*
not path /ocs-provider/*
not path /updater/*
not path */richdocumentscode/proxy*
}
rewrite @notlegacy /index.php{uri}
# Let everything else be handled by the PHP-FPM component
php_fastcgi unix+h2c//var/run/nextcloud/php-fpm.sock {
env modHeadersAvailable true # Avoid sending the security headers twice
env front_controller_active true # Enable pretty urls
}
}
}