Help translating nginx config for Nextcloud in Docker

1. Caddy version (caddy version):

Latest Docker 2-alpine container (v2.3.0)

2. How I run Caddy:

a. System environment:

Docker (2-alpine)

b. Command:

Default container startup command which appears to be:

'caddy' 'run' '--config' '/etc/caddy/Caddyfile' '--adapter' 'caddyfile'

c. Service/unit/compose file:

N/A - Nextcloud and Caddy containers are managed via Portainer so no docker-compose files.

d. My complete Caddyfile or JSON config:

My initial attempt at translating the config, which is incomplete and doesn’t work (white page on request to /):

cloud.example.com {
	header * {
		# Remove actual server header for privacy
		Server "CERN HTTPd/0.1"
		# Enable cross-site filter (XSS) and tell browser to block detected attacks
		X-XSS-Protection "1; mode=block"
		# Disallow the site to be rendered within a frame (clickjacking protection) unless from same origin
		X-Frame-Options "SAMEORIGIN"
		# Prevent search engines from indexing
		X-Robots-Tag "none"
		# Strict transport security / force SSL
		Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;"
		# Remove the referrer when linking to external pages
		Referrer-Policy "no-referrer"
		# Other recommended headers
		X-Content-Type-Options "nosniff"
		X-Download-Options "noopen"
		X-Permitted-Cross-Domain-Policies "none"
	}
	# Compression
	encode zstd gzip
	# Redirect well-known requests
	redir /.well-known/carddav /remote.php/dav 301
	redir /.well-known/caldav /remote.php/dav 301
	# ?
	rewrite / /index.php

	# Send  to FastCGI
	php_fastcgi * nextcloud:9000 {
		env PATH /bin
		#env PATH_INFO {path}
		env SCRIPT_FILENAME /var/www/html/{file}
		env HTTPS on
		env modHeadersAvailable true
		env front_controller_active true
		dial_timeout 60s
		read_timeout 3600s
		write_timeout 300s
	}
}

3. The problem I’m having:

Nextcloud has 2 different docker containers, one of which contains Apache HTTPd to allow simply proxying HTTP requests while the other just runs FPM. Of course, the latter is lighter, and as such preferable, because I’m running on an ARM SBC. However, I’ve had some trouble getting this version to work. I read in many examples that it is necessary to make the static files visible to Caddy so that it can serve them, but I did not need to do this with NGINX, and I believe this is because Nextcloud supports serving them from the PHP script? Maybe? Anyway, I have been looking around for configuration that would work with my setup since yesterday, but to no avail (many were for Caddy v1 for example), and so, I decided it would be better to just translate my working NGINX config. Still, I have had trouble doing that also, and so decided to seek help here.

My current NGINX configuration is as follows (but I understand that some of these directives are unnecessary, such as those to do with SSL:

server {
    listen       443 ssl;
    listen       [::]:443 ssl;

    ssl_certificate /etc/letsencrypt/live/cloud.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cloud.example.com/privkey.pem;

    include /etc/nginx/snippets/options-ssl-nginx.conf;

    # This folder is actually blank, so this is probably unnecessary
    root /usr/share/nginx/html/cloud/;

    server_name cloud.example.com;

    access_log  /var/log/nginx/host.access.log  main;

    # Add headers to serve security related headers
    # Before enabling Strict-Transport-Security headers please read into this
    # topic first.
    add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;" always;
    #
    # 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.
    add_header Referrer-Policy "no-referrer" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Download-Options "noopen" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Permitted-Cross-Domain-Policies "none" always;
    add_header X-Robots-Tag "none" always;
    add_header X-XSS-Protection "1; mode=block" always;

    index index.php index.html index.htm;

    # Remove X-Powered-By, which is an information leak
    fastcgi_hide_header X-Powered-By;

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

        # The following 2 rules are only needed for the user_webfinger app.
        # Uncomment it if you're planning to use this app.
        #rewrite ^/.well-known/host-meta /public.php?service=host-meta last;
        #rewrite ^/.well-known/host-meta.json /public.php?service=host-meta-json last;

        # The following rule is only needed for the Social app.
        # Uncomment it if you're planning to use this app.
        #rewrite ^/.well-known/webfinger /public.php?service=webfinger last;

        location = /.well-known/carddav {
            return 301 $scheme://$host:$server_port/remote.php/dav;
        }

        location = /.well-known/caldav {
            return 301 $scheme://$host:$server_port/remote.php/dav;
        }

        # Set max upload size
        client_max_body_size 10G;
        fastcgi_buffers 64 4K;

        # Prevent 504 timeouts
        fastcgi_send_timeout 600;
        fastcgi_read_timeout 600;

        # Enable gzip but do not remove ETag headers
        gzip on;
        gzip_vary on;
        gzip_comp_level 4;
        gzip_min_length 256;
        gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
        gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;

        # Uncomment if your server is build with the ngx_pagespeed module
        # This module is currently not supported.
        #pagespeed off;

        location / {
            rewrite ^ /index.php;
        }

        location ~ ^\/(?:build|tests|config|lib|3rdparty|templates|data)\/ {
            deny all;
        }
        location ~ ^\/(?:\.|autotest|occ|issue|indie|db_|console) {
            deny all;
        }

        location ~ ^\/(?:index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|oc[ms]-provider\/.+)\.php(?:$|\/) {
            fastcgi_split_path_info ^(.+?\.php)(\/.*|)$;
            set $path_info $fastcgi_path_info;
            try_files $fastcgi_script_name =404;
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME /var/www/html/$fastcgi_script_name;
            fastcgi_param PATH_INFO $path_info;
            fastcgi_param HTTPS on;

            # Avoid sending the security headers twice
            fastcgi_param modHeadersAvailable true;

            # Enable pretty urls
            fastcgi_param front_controller_active true;
            fastcgi_pass nextcloud:9000;
            fastcgi_intercept_errors on;
            fastcgi_request_buffering off;
        }

        location ~ ^\/(?:updater|oc[ms]-provider)(?:$|\/) {
            try_files $uri/ =404;
            index index.php;
        }

        # Adding the cache control header for js, css and map files
        # Make sure it is BELOW the PHP block
        location ~ \.(?:css|js|woff2?|svg|gif|map)$ {
            try_files $uri /index.php$request_uri;
            add_header Cache-Control "public, max-age=15778463";
            # Add headers to serve security related headers (It is intended to
            # have those duplicated to the ones above)
            # Before enabling Strict-Transport-Security headers please read into
            # this topic first.
            #add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;" always;
            #
            # 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.
            add_header Referrer-Policy "no-referrer" always;
            add_header X-Content-Type-Options "nosniff" always;
            add_header X-Download-Options "noopen" always;
            add_header X-Frame-Options "SAMEORIGIN" always;
            add_header X-Permitted-Cross-Domain-Policies "none" always;
            add_header X-Robots-Tag "none" always;
            add_header X-XSS-Protection "1; mode=block" always;

            # Optional: Don't log access to assets
            access_log off;
        }

        location ~ \.(?:png|html|ttf|ico|jpg|jpeg|bcmap|mp4|webm)$ {
            try_files $uri /index.php$request_uri;
            # Optional: Don't log access to other assets
            access_log off;
        }

}

4. Error messages and/or full log output:

N/A

5. What I already tried:

See My Complete Caddyfile section

6. Links to relevant resources:

Recommended NGINX Nextcloud Config

Yep that’s correct. You’ll need to use volumes_from for Caddy to see the files, then add root and file_server directives to your Caddyfile config.

See an example here:

I can’t use volumes_from since I’m not using docker-compose. Still, that should be possible using Portainer. However, I’ve looked around some more and found a nice V1 config which seems to be exactly what I want, and not require mounting any Nextcloud volumes in the Caddy container, but being for V1, it has some rewrite directives which don’t work on V2. Would you be able to help me out translating these for V2? The config is as follows:

nextcloud.domain.tld {

        root   /var/www/nextcloud
        log    /var/log/nextcloud_access.log
        errors /var/log/nextcloud_errors.log

        fastcgi / 127.0.0.1:9000 php {
                env PATH /bin
                env modHeadersAvailable true
                env front_controller_active true
                connect_timeout 60s
                read_timeout 3600s
                send_timeout 300s
        }

        header / {
                Strict-Transport-Security               "max-age=15768000;"
                X-Content-Type-Options                  "nosniff"
                X-XSS-Protection                        "1; mode=block"
                X-Robots-Tag                            "none"
                X-Download-Options                      "noopen"
                X-Permitted-Cross-Domain-Policies       "none"
                Referrer-Policy                         "no-referrer"
        }

        header /core/fonts {
                Cache-Control                           "max-age=604800"
        }

        # checks for images
        rewrite {
                ext .png .html .ttf .ico .jpg .jpeg .css .js .woff .woff2 .svg .gif .map
                r ^/index.php/.*$
                to /{1} /index.php?{query}
        }

        rewrite {
                r ^/\.well-known/host-meta$
                to /public.php?service=host-meta&{query}
        }
        rewrite {
                r ^/\.well-known/host-meta\.json$
                to /public.php?service=host-meta-json&{query}
        }
        rewrite {
                r ^/\.well-known/webfinger$
                to /public.php?service=webfinger&{query}
        }

        rewrite {
                r ^/index.php/.*$
                to /index.php?{query}
        }

        rewrite / {
                if {path} not_starts_with /remote.php
                if {path} not_starts_with /public.php
                ext .png .html .ttf .ico .jpg .jpeg .css .js .woff .woff2 .svg .gif .map .html .ttf 
                r ^/(.*)$
                to /{1} /index.php{uri}
        }

        rewrite / {
                if {path} not /core/img/favicon.ico
                if {path} not /core/img/manifest.json
                if {path} not_starts_with /remote.php
                if {path} not_starts_with /public.php
                if {path} not_starts_with /cron.php
                if {path} not_starts_with /core/ajax/update.php
                if {path} not_starts_with /status.php
                if {path} not_starts_with /ocs/v1.php
                if {path} not_starts_with /ocs/v2.php
                if {path} not /robots.txt
                if {path} not_starts_with /updater/
                if {path} not_starts_with /ocs-provider/
                if {path} not_starts_with /ocm-provider/ 
                if {path} not_starts_with /.well-known/
                to /index.php{uri}
        }

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

        # remove trailing / as it causes errors with php-fpm
        rewrite {
                r ^/remote.php/(webdav|caldav|carddav|dav)(\/?)(\/?)$
                to /remote.php/{1}
        }

        rewrite {
                r ^/remote.php/(webdav|caldav|carddav|dav)/(.+?)(\/?)(\/?)$
                to /remote.php/{1}/{2}
        }

        rewrite {
                r ^/public.php/(dav|webdav|caldav|carddav)(\/?)(\/?)$
                to /public.php/{1}
        }

        rewrite {
                r ^/public.php/(dav|webdav|caldav|carddav)/(.+)(\/?)(\/?)$
                to /public.php/{1}/{2}
        }

        # .htaccess / data / config / ... shouldn't be accessible from outside
        status 404 {
                /.htaccess
                /data
                /config
                /db_structure
                /.xml
                /README
                /3rdparty
                /lib
                /templates
                /occ
                /console.php
        }

}

(Shamelessly stolen from another question: Nextcloud with FastCGI and Docker)

It does though:

It’s necessary for it to work.

The config in the thread I linked above is the translation of that Caddy v1 config.

Also, volumes_from is just a shortcut in docker-compose to copy the set of volumes defined for the other container. If you’re using the Docker CLI, just use a named volume for both containers… or just use docker-compose, because it’s the right tool for the job here.

Ah, okay, sorry about that. I’ve also double-checked my NGINX config and indeed the nextcloud volume is mounted - which I think was the biggest thing confusing me, because I thought it wasn’t. Anyway, will update the config and get back to you. Thanks!

All seems to be working just fine. Thanks for your help! I’m really surprised at the size of the config though. It’s tiny compared to that of NGINX.

2 Likes

Yep! That’s one of Caddy’s biggest strengths - having good defaults avoids a lot of boilerplate.

2 Likes