Caddy configuration
Warning
Please note that webservers other than Apache 2.x are not officially supported.
Note
This page covers example Caddy configuration to run a Nextcloud server. These configuration examples were originally provided by @ntninja based on the NGINX configuration sample and are exclusively community-maintained. (Thank you contributors!)
This guide assumes you are using Caddy 2.6 or later and the presented sample configuration will not work on older versions without modification.
Caddy takes care of TLS certificate configuration and HTTP-to-HTTPS redirects automatically, so that is not covered here.
The example configuration makes use of the route directive which disables all directive reordering usually done by Caddy. This means that anything within that block should be read strictly top-to-bottom unlike what you may be used to from NGINX or regular (non-route) Caddy configurations.
Be careful about line breaks if you copy the examples, as long lines may be broken for page formatting.
Some environments might need a
cgi.fix_pathinfoset to1in theirphp.ini.
Note
If you are using FrankenPHP (an application server built on top of Caddy),
you can use the php_server directive instead of the php_fastcgi-based
approach described on this page. FrankenPHP handles the try-files logic and PHP
routing internally, which greatly simplifies the configuration. See the
FrankenPHP documentation and a community
example at https://gitlab.com/greyxor/nextcloud-docker for reference.
Nextcloud in the webroot of Caddy
The following configuration should be used when Nextcloud is placed in the
webroot of your Caddy installation. In this example it is
/var/www/nextcloud and it is accessed via http(s)://cloud.example.com/
cloud.example.com # Public server hostname
request_body {
max_size 10G
}
# Enable compression 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
}
}
# Add security-related headers
header {
Referrer-Policy no-referrer
Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;"
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"
# Remove X-Powered-By header (already removed by default in newer Caddy)
-X-Powered-By
}
# Path to the root of your installation
root * /var/www/nextcloud
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 {
skip_log
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
}
# Block access to internal/sensitive paths
@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 *.svg *.gif *.png *.jpg *.jpeg *.ico *.wasm *.tflite *.map *.wasm2
file {path} # Only if requested file exists on disk, otherwise /index.php will handle it
}
route @assets {
header Cache-Control "max-age=15552000" # Cache-Control policy borrowed from `.htaccess`
# Note: to give .woff2 files a shorter TTL, add a nested route for *.woff2
# with `header Cache-Control "max-age=604800"` before this one.
skip_log # 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, falling through to PHP handler 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 with "/index.php" unless they already
# match a known PHP entry point.
@unknownphppath not path \
/index.php /index.php/* \
/remote.php /remote.php/* \
/public.php /public.php/* \
/cron.php /cron.php/* \
/core/ajax/update.php /core/ajax/update.php/* \
/status.php /status.php/* \
/ocs/v1.php /ocs/v1.php/* \
/ocs/v2.php /ocs/v2.php/* \
/updater/*.php /updater/*.php/* \
/ocm-provider/*.php /ocm-provider/*.php/* \
/ocs-provider/*.php /ocs-provider/*.php/*
rewrite @unknownphppath /index.php{path}
# Let everything else be handled by the PHP-FPM component
# Adjust the address to match your PHP-FPM socket or TCP address
# (e.g. unix//var/run/php/php-fpm.sock or 127.0.0.1:9000)
php_fastcgi 127.0.0.1:9000 {
env modHeadersAvailable true # Avoid sending the security headers twice
env front_controller_active true # Enable pretty urls
}
}
Nextcloud in a subdir of the Caddy webroot
Serving Nextcloud from a subdirectory (e.g. https://cloud.example.com/nextcloud/)
requires extra steps with Caddy compared to NGINX, due to the way Caddy’s
handle_path strips the prefix from PATH_INFO but not from REQUEST_URI,
while Nextcloud relies on REQUEST_URI.
The recommended approach is:
Set
'overwritewebroot' => '/nextcloud'in your Nextcloudconfig/config.php.Wrap the main Caddyfile configuration in a
handle_path /nextcloud/* { … }block, or useuri strip_prefix /nextcloud.Capture the rewritten URI before the PHP handler and pass it as
REQUEST_URI:
handle_path /nextcloud/* {
# … (place the route block contents here) …
vars rewritten_uri {uri}
# Let everything else be handled by the PHP-FPM component
php_fastcgi app:9000 {
env modHeadersAvailable true
env front_controller_active true
env REQUEST_URI {vars.rewritten_uri}
}
}
Note
With FrankenPHP’s php_server directive and the
htaccess.IgnoreFrontController option, subdirectory support is handled
automatically without these workarounds.
Tips and tricks
Suppressing log messages
If you’re seeing meaningless messages in your logfile, for example client
denied by server configuration: /var/www/data/htaccesstest.txt, add this
section to your Caddy configuration to suppress them:
route {
# …
route /data/htaccesstest.txt {
skip_log # Silences logging for the matched path
file_server
}
# …
}