Caddy v2 configuration: Nextcloud Docker PHP-FPM with rules from .htaccess

TL;DR: Here I paste you my (semi-opinionated) Caddyfile for Nextcloud in Docker with PHP-FPM.

I’ve seen a lot of configurations for Nextcloud but none of them included all (or mostly all) rules from .htaccess file. So after “reverse engineering” the .htaccess file and with the official Nextcloud Nginx configuration I offer my findings to this forum with some comments. I will try to keep it updated as the official .htaccess file is updated.

It might seem complex but it is what it is, better safe than sorry. Please feel free to correct any mistake or improve this configuration: {
	encode zstd gzip
	root * /var/www/nextcloud # Notice the root directory is not /var/www/html, i've mounted this one in Docker

	# Rules here are all from .htaccess
	redir /.well-known/carddav /remote.php/dav 301
	redir /.well-known/caldav /remote.php/dav 301
	redir /.well-known/* /index.php{uri} 301 # Nextcloud front-controller handles routes to /.well-known
	redir /remote/* /remote.php{uri} 301

	# Secure headers, all from .htaccess except Permissions-Policy, STS and X-Powered-By
	header {
		Strict-Transport-Security max-age=31536000
		Permissions-Policy interest-cohort=()
		X-Content-Type-Options nosniff
		X-Frame-Options SAMEORIGIN
		Referrer-Policy no-referrer
		X-XSS-Protection "1; mode=block"
		X-Permitted-Cross-Domain-Policies none
		X-Robots-Tag "noindex, nofollow"

	# Uncomment this block if you use the high speed files backend:
	#handle_path /push/* {
	#	reverse_proxy unix//run/notify_push/notify_push.sock # I love Unix sockets, but you can do :7867 also

	# Uncomment this block if you use onlyoffice:
	#handle_path /onlyoffice/* {
	#	reverse_proxy {$ONLYOFFICE_IP}:{$ONLYOFFICE_PORT} {
	#		header_up X-Forwarded-Host {host}/onlyoffice
	#	}

	# PHP block
	# As I said before, i'm a fan of Unix sockets, so I use them as much as I can. But you can do :9000 also for TCP.
	php_fastcgi unix//run/nextcloud/nextcloud.sock {
		root /var/www/html # This is needed because inside the container the root directory is different from the one I put in the "root" directive of this Caddyfile. If you don't change this, php-fpm will not be able to find the files to process.
		env front_controller_active true # Enable pretty urls
		env modHeadersAvailable true # Avoid sending the security headers twice

	# From .htaccess, deny access to sensible files and directories
	@forbidden {
		path /build/* /tests/* /config/* /lib/* /3rdparty/* /templates/* /data/*
		path /.* /autotest* /occ* /issue* /indie* /db_* /console*
		not path /.well-known/*
	error @forbidden 404

	# From .htaccess, set cache for versioned static files (cache-busting)
	@immutable {
		path *.css *.js *.mjs *.svg *.gif *.png *.jpg *.ico *.wasm *.tflite
		query v=*
	header @immutable Cache-Control "max-age=15778463, immutable"

	# From .htaccess, set cache for normal static files
	@static {
		path *.css *.js *.mjs *.svg *.gif *.png *.jpg *.ico *.wasm *.tflite
		not query v=*
	header @static Cache-Control "max-age=15778463"

	# From .htaccess, cache fonts for 1 week
	@woff2 path *.woff2
	header @woff2 Cache-Control "max-age=604800"



  • The only rewrite rule that i’m not able to reproduce, because of lack of time to try, is the one for Dav clients for Windows: In the Nginx config is the “if” block with “$http_user_agent ~ ^DavClnt”
  • I also have no idea how to do the mime.tipe “if’s” blocks from the Nginx config (application/wasm, text/javascript js mjs). I’m supposing that Caddy already includes these mime.types. :man_shrugging:t4:

I hope this information is useful and feel free to destroy me if I did any mistake. :slightly_smiling_face:

Looks pretty good! I made a few edits for best-practices.

First of all, thanks for the improvement.

I tested your changes and:

handle_path /onlyoffice/* {

Throws 404 error in onlyoffice, but:

handle_path /onlyoffice/* {
	reverse_proxy {
		header_up X-Forwarded-Host {}/onlyoffice


It is my original config but changing “route” for “handle_path” and removing the “strip_prefix” as you proposed. It seems that header is needed, otherwise onlyoffice doesn’t work and throws 404.

Also, what happened to the “@static” matcher? You removed it but kept the “header @static” directive.

Can you please write a Caddyfile that removes index.php from url?

This doesn’t make sense. The host should not include a path. Onlyoffice must be doing some really non-standard stuff. They should use a different mechanism for configuring the base path.

Anyway, re-added it :man_shrugging: sigh

Whoops, sorry about that. Fixed.

Try to read the comments I posted in the configuration, also I suggest you reading this: Installation on Linux — Nextcloud latest Administration Manual latest documentation

Hi @plexman
Thanks a lot for your example Caddyfile to setup Nextcloud without a NGINX/ Apache server and just using Caddy.
I am currently trying to implement this on my own and struggeling with the UNIX sockets thing.
Can you elaborate a bit more on how you’ve set up the sockets, maybe by showing your docker-compose file (if you use it) or explain which (PHP?) configuration variables need to be changed and which files need to be mounted.

Currently I am using Caddy as a reverse proxy in front of a NGINX webserver, which I think generates quite a bit of overhead.
But I never tried on getting rid of NGINX in the past, just because the configuration looks so complicated :laughing:

Of course, I use the Nextcloud PHP-FPM with plain Docker cli (no docker-compose). This container by default exposes port 9000 to handle connections to PHP-FPM. What I do is I mount a custom configuration to modify the PHP-FPM behaviour so it can listen to unix sockets:

-v /etc/docker-php-fpm/nextcloud.conf:/usr/local/etc/php-fpm.d/zzz-custom.conf:ro

And the content of the nextcloud.conf file is:

user = 33
group = 33

listen = /run/nextcloud/nextcloud.sock
listen.owner = 33 = 33

pm = dynamic
pm.max_children = 160
pm.start_servers = 20
pm.min_spare_servers = 20
pm.max_spare_servers = 120

You also have to mount /run/nextcloud to make it accesible to your host and to Caddy. Also, make sure Caddy has the right permissions to read/write the uniz socket, in my case I simply added the caddy user to the http (id: 33) group.

Hi @plexman
Thanks a lot for this valuable information.
Indeed, I was able to get rid of NGINX using your configuration and a bit of trial and error.

I guess the missing Caddy stuff is this NGINX configuration, correct?

 location = / {
        if ( $http_user_agent ~ ^DavClnt ) {
            return 302 /remote.php/webdav/$is_args$args;

And I was not able to get the notify push app working using this NGINX configuration (neither using UNIX sockets, nor TCP):
Whenever I try to set the notify push server using the OCC the following error appears:

I have no name!@ffa7da42c3aa:/var/www/html$ ./occ notify_push:setup
✓ redis is configured
✓ push server is receiving redis messages
✓ push server can load mount info from database
✓ push server can connect to the Nextcloud server
🗴 push server is not a trusted proxy, please add '(public IP of my server)' to the list of trusted proxies or configure any existing reverse proxy to forward the 'x-forwarded-for' send by the push server.
  See for how to set trusted proxies.
  The following trusted proxies are currently configured: ""
  The following x-forwarded-for header was received by Nextcloud: (public IP of my server)
    from the following remote: (public IP of my server)

  If you're having issues getting the trusted proxy setup working, you can try bypassing any existing reverse proxy
  in your setup by setting the `NEXTCLOUD_URL` environment variable to point directly to the internal Nextcloud webserver url
  (You will still need the ip address of the push server added as trusted proxy)

I think something is wrong with the x-forwarded-for header that maybe automatically gets set by Caddy when using the reverse_proxy directive? Should it be the public IP?
Which IP address should i add to the trusted proxies then? My server’s IP address is dynamic, unfortunately.
But I really have no idea, maybe you can help me out?

Yes it happened to me also, no matter what IP I set on trusted proxies it gives me the same error. Also setting the correct hostname as it suggests, nothing, same error. I think that is something related to notify_push itself.

Edit: Correct for the missing Dav configuration, my knowledge is limited and I’ve haven’t been able to reproduce that for Caddy

Okay thanks for the confirmation.
I will ask the notify push guys if they have an idea.

About the Dav configuration, maybe @francislavoie can help out here?

I have got the same problem, if you run nextcloud via Unix domain socket the x-forwarded-for header is not correct for nextcloud, I have got to the state where it does rate limit my clients as it assumes a bad reverse proxy. I would believe prepending a in front of the caddy generated header might fix the problem, but I am mobile right now and cannot test.

A post was split to a new topic: NextCloud rewrite

Wouldn’t that work?

	@msdavclients {
		header_regexp useragent_msdavclients User-Agent "^DavClnt"
		path /
	redir @msdavclients /remote.php/webdav{uri}

Btw, getting back to the original Nextcloud .htaccess, it was rewrites, not redir:

<IfModule mod_rewrite.c>
  RewriteEngine on
  RewriteCond %{HTTP_USER_AGENT} DavClnt
  RewriteRule ^$ /remote.php/webdav/ [L,R=302]
  RewriteRule .* - [env=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
  RewriteRule ^\.well-known/carddav /remote.php/dav/ [R=301,L]
  RewriteRule ^\.well-known/caldav /remote.php/dav/ [R=301,L]
  RewriteRule ^remote/(.*) remote.php [QSA,L]
  RewriteRule ^(?:build|tests|config|lib|3rdparty|templates)/.* - [R=404,L]
  RewriteRule ^\.well-known/(?!acme-challenge|pki-validation) /index.php [QSA,L]
  RewriteRule ^ocm-provider/?$ index.php [QSA,L]
  RewriteRule ^(?:\.(?!well-known)|autotest|occ|issue|indie|db_|console).* - [R=404,L]

I know Nextcloud themselves transposed these to redirs in nginx conf documentation.

Wouldn’t it be cleaner to translate them into rewrites in Caddy?

	# debug
	email youremailhere
	# default_sni yourhostnamehere

yourhostnamehere {
	root * /usr/local/www/nextcloud
	log {
		output file /var/log/yourhostnamehere.log

	php_fastcgi {
		env front_controller_active true

	header {
		# enable HSTS
		# Strict-Transport-Security max-age=31536000;

	# client support (e.g. os x calendar / contacts)
	redir /.well-known/carddav /remote.php/dav 301
	redir /.well-known/caldav /remote.php/dav 301
	redir /.well-known/webfinger /index.php/.well-known/webfinger 301
	redir /.well-known/nodeinfo /index.php/.well-known/nodeinfo 301

	# Required for legacy
	@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 /updater/*
		not path /ocs-provider/*
		not path */richdocumentscode/proxy*
	rewrite @notlegacy /index.php{uri}

	# .htaccess / data / config / ... shouldn't be accessible from outside
	@forbidden {
		path /.htaccess
		path /data/*
		path /config/*
		path /db_structure
		path /.xml
		path /README
		path /3rdparty/*
		path /lib/*
		path /templates/*
		path /occ
		path /console.php

	respond @forbidden 404

I’ve been using this Caddyfile for the longest time without issues. I get push notification to my iOS phone through the Nextcloud app as well. Or am I not understanding properly?

@francislavoie suggested in another thread this, which looks quite close:

@davclnt header_regexp User-Agent (?i)^DavClnt
redir @devclnt /remote.php/webdav{uri} 302

About the rewrites/ redirects thing, I am also using redirects for a long time now and didn’t get any errors, so it doesn’t seem to make much of a difference/ is both supported by the clients.
Still, you have a point that it would be closer to the original Apache config.