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_pathinfo set to 1 in their php.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:

  1. Set 'overwritewebroot' => '/nextcloud' in your Nextcloud config/config.php.

  2. Wrap the main Caddyfile configuration in a handle_path /nextcloud/* { } block, or use uri strip_prefix /nextcloud.

  3. 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
  }

  # …
}