Caddyfile for Drupal 10+

1. The problem I’m having:

There is no specific problem so far, I would like to get help to convert the following nginx config file to Caddy v2 format: Drupal | NGINX

location = /favicon.ico {
        log_not_found off;
        access_log off;
    }

    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }

    # Very rarely should these ever be accessed outside of your lan
    location ~* \.(txt|log)$ {
        allow 192.168.0.0/16;
        deny all;
    }

    location ~ \..*/.*\.php$ {
        return 403;
    }

    location ~ ^/sites/.*/private/ {
        return 403;
    }

    # Block access to scripts in site files directory
    location ~ ^/sites/[^/]+/files/.*\.php$ {
        deny all;
    }

    # Allow "Well-Known URIs" as per RFC 5785
    location ~* ^/.well-known/ {
        allow all;
    }

    # Block access to "hidden" files and directories whose names begin with a
    # period. This includes directories used by version control systems such
    # as Subversion or Git to store control files.
    location ~ (^|/)\. {
        return 403;
    }

    location / {
        try_files $uri /index.php?$query_string;
    }

    location @rewrite {
        rewrite ^ /index.php;
    }

    # Don't allow direct access to PHP files in the vendor directory.
    location ~ /vendor/.*\.php$ {
        deny all;
        return 404;
    }

    # Protect files and directories from prying eyes.
    location ~* \.(engine|inc|install|make|module|profile|po|sh|.*sql|theme|twig|tpl(\.php)?|xtmpl|yml)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^(\.(?!well-known).*|Entries.*|Repository|Root|Tag|Template|composer\.(json|lock)|web\.config)$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig|\.save)$ {
        deny all;
        return 404;
    }

    location ~ \.php(/|$) {
        fastcgi_split_path_info ^(.+?\.php)(|/.*)$;
        try_files $fastcgi_script_name =404;
        include fastcgi_params;
        fastcgi_param HTTP_PROXY "";
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_param QUERY_STRING $query_string;
        fastcgi_intercept_errors on;
        # PHP socket location.
        fastcgi_pass unix:/var/run/php/php-fpm.sock;
    }

    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
        try_files $uri @rewrite;
        expires max;
        log_not_found off;
    }

    # Fighting with Styles? This little gem is amazing.
    location ~ ^/sites/.*/files/(css|js|styles)/ {
        try_files $uri @rewrite;
    }

    # Handle private files through Drupal. Private file's path can come
    # with a language prefix.
    location ~ ^(/[a-z\-]+)?/system/files/ {
        try_files $uri /index.php?$query_string;
    }

    # Enforce clean URLs
    # Removes index.php from urls like www.example.com/index.php/my-page --> www.example.com/my-page
    # Could be done with 301 for permanent or other redirect codes.
    if ($request_uri ~* "^(.*/)index\.php/(.*)") {
        return 307 $1$2;
    }

3. Caddy version:

2.7.5

4. How I installed and ran Caddy:

brew install caddy

a. System environment:

MacOS Sonoma 14.1 (23B74) (Apple Silicon CPU / arm64)

b. Command:

brew services start caddy

d. My complete Caddy config:

(ssl) {
    tls /opt/homebrew/etc/nginx/_wildcard.local.test+4.pem /opt/homebrew/etc/nginx/_wildcard.local.test+4-key.pem
}

(phpfpm) {
    encode zstd gzip
    php_fastcgi unix//tmp/php-fpm.sock
    file_server
}

(cache-static) {
	@static {
		file
		path *.avif *.css *.eot *.gif *.gz *.ico *.jpg *.jpeg *.js *.otf *.pdf *.png *.svg *.ttf *.webp *.woff *.woff2
	}
	header @static Cache-Control "max-age=31536000,public,immutable"
}

(drupal-10) {
    root * "/Users/asrob/Sites/{http.request.host.labels.2}/www"
    import ssl
    import phpfpm
    import cache-static
}

drupal10.local.test {
    import drupal-10
}

What part isn’t working? Please elaborate.

Well, I’ve tested my config based on nginx one, here is the current Caddyfile:

(ssl) {
    tls /opt/homebrew/etc/nginx/_wildcard.local.test+4.pem /opt/homebrew/etc/nginx/_wildcard.local.test+4-key.pem
}

(php-socket) {
    encode zstd gzip
    php_fastcgi unix//tmp/php-fpm.sock
    file_server
}

(drupal) {
    import ssl
    import php-socket

    @hiddenFilesRegexp {
        path_regexp (^|/)\.
    }
    respond @hiddenFilesRegexp 403

    @hiddenPhpFilesRegexp {
        path_regexp \..*/.*\.php$
    }
    respond @hiddenPhpFilesRegexp 403

    @notfoundPhpFiles {
        path /vendor/.*\.php$
    }
    respond @notfoundPhpFiles 404

    @notfoundPhpFilesRegexp {
        path_regexp ^/sites/[^/]+/files/.*\.php$
    }
    respond @notfoundPhpFilesRegexp 404

    @privateDirRegexp {
        path_regexp ^/sites/.*/private/
    }
    respond @privateDirRegexp 403

    @protectedFilesRegexp {
        path_regexp \.(engine|inc|install|make|module|profile|po|sh|.*sql|theme|twig|tpl(\.php)?|xtmpl|yml)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^(\.(?!well-known).*|Entries.*|Repository|Root|Tag|Template|composer\.(json|lock)|web\.config)$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig|\.save)$
    }
    respond protectedFilesRegexp 404

    @static {
		file
		path *.avif *.css *.eot *.gif *.gz *.ico *.jpg *.jpeg *.js *.otf *.pdf *.png *.svg *.ttf *.webp *.woff *.woff2
	}
	header @static Cache-Control "max-age=31536000,public,immutable"

    handle / {
        try_files {path} /index.php?{query}
        rewrite * /index.php
    }
}

drupal-10.local.test {
    root * "/Users/asrob/Sites/{http.request.host.labels.2}/www"
    import drupal
}

I’m not sure how I implement the following:

    # Fighting with Styles? This little gem is amazing.
    location ~ ^/sites/.*/files/(css|js|)styles/ {
        try_files $uri @rewrite;
    }

    # Handle private files through Drupal. Private file's path can come
    # with a language prefix.
    location ~ ^(/[a-z\-]+)?/system/files/ {
        try_files $uri /index.php?$query_string;
    }

    # Enforce clean URLs
    # Removes index.php from urls like www.example.com/index.php/my-page --> www.example.com/my-page
    # Could be done with 301 for permanent or other redirect codes.
    if ($request_uri ~* "^(.*/)index\.php/(.*)") {
        return 307 $1$2;
    }

Or do I skip these because Caddy handles it correctly?

I’ve just updated my Caddyfile:

(ssl) {
    tls /opt/homebrew/etc/nginx/_wildcard.local.test+4.pem /opt/homebrew/etc/nginx/_wildcard.local.test+4-key.pem
}

(php-socket) {
    encode zstd gzip
    php_fastcgi unix//tmp/php-fpm.sock
    file_server
}

(drupal) {
    import ssl
    import php-socket

    @hiddenFilesRegexp {
        path_regexp (^|/)\.
    }
    respond @hiddenFilesRegexp 403

    @hiddenPhpFilesRegexp {
        path_regexp \..*/.*\.php$
    }
    respond @hiddenPhpFilesRegexp 403

    @notfoundPhpFiles {
        path /vendor/.*\.php$
    }
    respond @notfoundPhpFiles 404

    @notfoundPhpFilesRegexp {
        path_regexp ^/sites/[^/]+/files/.*\.php$
    }
    respond @notfoundPhpFilesRegexp 404

    @privateDirRegexp {
        path_regexp ^/sites/.*/private/
    }
    respond @privateDirRegexp 403

    @protectedFilesRegexp {
        path_regexp \.(engine|inc|install|make|module|profile|po|sh|.*sql|theme|twig|tpl(\.php)?|xtmpl|yml)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^(\.(?!well-known).*|Entries.*|Repository|Root|Tag|Template|composer\.(json|lock)|web\.config)$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig|\.save)$
    }
    respond protectedFilesRegexp 404

    @static {
		file
		path *.avif *.css *.eot *.gif *.gz *.ico *.jpg *.jpeg *.js *.otf *.pdf *.png *.svg *.ttf *.webp *.woff *.woff2
	}
	header @static Cache-Control "max-age=31536000,public,immutable"

    handle / {
        try_files {path} /index.php?{query}
        rewrite * /index.php
    }

    @cssJsStyles path_regexp ^/sites/.*/files/(css|js|styles)/
    handle @cssJsStyles {
        try_files {path} /index.php?{query}
        rewrite * /index.php
    }

    @privateFiles path_regexp ^(/[a-z\-]+)?/system/files/
    handle @privateFiles {
        try_files {path} /index.php?{query}
    }
}

drupal-10.local.test {
    root * "/Users/asrob/Sites/{http.request.host.labels.2}/www"
    import drupal
}

I tried to implement the rules that I mentioned my previous comment. Caddy is running, no errors in log file but I’m not sure my Caddyfile is good and secure enough or there are bad practices. :slight_smile:

I would be grateful if you can review my Caddyfile and provide feedback. Thanks in advance!

This doesn’t so anything useful at all, because php_fastcgi already has try_files built in. You can remove all handle which have only try_files and rewrite like that because they’re ineffective.

You can simplify these by making the matcher a one-liner:

	@hiddenFilesRegexp path_regexp (^|/)\.
	respond @hiddenFilesRegexp 403

Also, you might consider using error instead of respond, which allows you to use handle_errors to respond with a custom error page in a unified way.

You can just use tls internal instead, and Caddy will generate a TLS cert of its own. You can add Caddy’s CA to your system’s trust store when testing locally. Very simple.

1 Like

Thank you very much! I’ve done that you suggested, it works well so far. :slight_smile:

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.