Nginx can't access files from nextcloud after upgrading from 29.0.16-fpm to 30.0.17-fpm

Support intro

Sorry to hear you’re facing problems. :slightly_frowning_face:

The community help forum (help.nextcloud.com) is for home and non-enterprise users. Support is provided by other community members on a best effort / “as available” basis. All of those responding are volunteering their time to help you.

If you’re using Nextcloud in a business/critical setting, paid and SLA-based support services can be accessed via portal.nextcloud.com where Nextcloud engineers can help ensure your business keeps running smoothly.

Getting help

In order to help you as efficiently (and quickly!) as possible, please fill in as much of the below requested information as you can.

Before clicking submit: Please check if your query is already addressed via the following resources:

(Utilizing these existing resources is typically faster. It also helps reduce the load on our generous volunteers while elevating the signal to noise ratio of the forums otherwise arising from the same queries being posted repeatedly).

Some or all of the below information will be requested if it isn’t supplied; for fastest response please provide as much as you can. :heart:

The Basics

  • Nextcloud Server version (e.g., 29.x.x):
    • 30.0.17-fpm(via docker)
  • Operating system and version (e.g., Ubuntu 24.04):
    • QNAP - Linux version 5.10.60-qnap (root@mini-builder-01) (x86_64-QNAP-linux-gnu-gcc (toolchain config: [gcc-4.9.2 binutils-2.25 glibc-2.21]) 4.9.2, GNU ld (GNU Binutils) 2.25) #1 SMP Sat Sep 13 00:59:01 CST 2025
  • Web server and version (e.g, Apache 2.4.25):
    • nginx:alpine-slim / caddy:latest (via docker)
  • Reverse proxy and version _(e.g. nginx 1.27.2)
    • nginx:alpine-slim / caddy:latest (via docker)
  • PHP version (e.g, 8.3):
    • PHP 8.3.27 via docker
  • Is this the first time you’ve seen this error? (Yes / No):
    • yes
  • When did this problem seem to first start?
    • when changing from version 29.0.16 to 30.0.17
  • Installation method (e.g. AlO, NCP, Bare Metal/Archive, etc.)
    • docker compose file on qnap
  • Are you using CloudfIare, mod_security, or similar? (Yes / No)
    • No

Summary of the issue you are facing:

I stopped the docker containers. I updated the docker compose file, replacing a ‘29.0.16-fpm’ with ‘30.0.17-fpm’. I rebuilt the containers.

The website no longer works, although the desktop app and iphone app work fine.

Upon investigation, I discovered that almost all of the files were getting a permission denied error in the nginx logs

for example:

web-1    | 2025/10/30 19:34:57 [error] 31#31: *120 open() "/var/www/html/apps/dashboard/css/dashboard.css" failed (13: Permission denied), client: 172.29.0.2, server: www.REDACTED.com, request: "GET /apps/dashboard/css/dashboard.css?v=e0231e2e-0 HTTP/1.1", host: "www.REDACTED.com"

When I go into the nginx docker and look at the files, they look like this:

-rw-------    1 33 33             938 Oct 30 16:07 dashboard.css

I have done some testing and confirmed that if I add permissions for everyone to read this file, then it works (for this file).

In the nextcloud docker, userid 33 is the user www-data, and groupid 33 is the group www-data.

In the nginx docker, there is no www-data user, and the www-data group is groupid 82.

I added the www-data user to the nginx docker with the following command in the DOCKER file:

RUN adduser -S -u 33 -G www-data www-data # Add user and assign to www-data group

this changed it to show the following in nginx

-rw-------    1 www-data 33             938 Oct 30 16:07 dashboard.css

but did not help with the issue.

In nginx docker, I also tried using groupmod to change the www-data group from 82 to 33, but groupmod was not installed.

Finally, I tried adding the following under web on my compose file:

      securityContext:
        fsGroup: 82
        runAsGroup: 33
        runAsNonRoot: true
        runAsUser: 33

and that didn’t work. So I then tried adding this to the compose file under the web section:

user: www-data

No luck.

My compose file:

services:
  db:
    image: mariadb:lts
    # image: mysql
    command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
    restart: always
    volumes:
      - /share/nc_db:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=REDACTED
      - MARIADB_AUTO_UPGRADE=1
      - MARIADB_DISABLE_UPGRADE_BACKUP=1
    env_file:
      - db.env

  redis:
    image: redis:alpine
    restart: always

  app:  
    image: nextcloud:30.0.17-fpm
    restart: always
    volumes:
      - /share/nc_main:/var/www/html:z
      - /share/nc_storage:/var/www/html/data
    environment:
      - MYSQL_HOST=db
      - REDIS_HOST=redis      
    env_file:
      - db.env
    depends_on:
      - db
      - redis
      - caddy

  web:
    build: ./web
    restart: always
    ports:
      - 8091:80      
    volumes:
      - /share/nc_main:/var/www/html:z,ro      
      - /share/nc_nginx:/srv
    depends_on:
      - app

  cron:
    image: nextcloud:fpm-alpine
    restart: always
    volumes:
      - /share/nc_main:/var/www/html:z
    entrypoint: /cron.sh
    depends_on:
      - db
      - redis

  caddy:
    image: caddy:latest
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
    ports:
      - "8090:80"
      - "4444:443"
      - "4444:443/udp"
    volumes:
      - /share/nc_init/caddy/Caddyfile:/etc/caddy/Caddyfile
      - /share/nc_init/caddy/srv:/srv
      - /share/nc_init/caddy/caddydata:/data
      - /share/nc_init/caddy/caddyconfig:/config

 
volumes:
  db:
  nextcloud:
  caddy_data:
    external: true
  caddy_config:

I appreciate any and all troubleshooting ideas in advance!

Steps to replicate it (hint: details matter!):

  1. Change versions of nextcloud from 29.0.16-fpm to 30.0.17-fpm in the docker compose file.

  2. Attempt to browse to your website

Log entries

Nextcloud

Please provide the log entries from your Nextcloud log that are generated during the time of problem (via the Copy raw option from Administration settings->Logging screen or from your nextcloud.log located in your data directory). Feel free to use a pastebin/gist service if necessary.

UNABLE

Web Browser

If the problem is related to the Web interface, open your browser inspector Console and Network tabs while refreshing (reloading) and reproducing the problem. Provide any relevant output/errors here that appear.

GET
	
scheme
	https
host
	www.REDACTED.com
filename
	/apps/activity/css/style.css
v
	892dbd9a-0
Address
	XXX.YYY.ZZZ.000:443
Status
403

Web server / Reverse Proxy

The output of your Apache/nginx/system log in /var/log/____:

SEE ABOVE

Configuration

Nextcloud

The output of occ config:list system or similar is best, but, if not possible, the contents of your config.php file from /path/to/nextcloud is fine (make sure to remove any identifiable information!):

{
    "system": {
        "memcache.local": "\\OC\\Memcache\\APCu",
        "apps_paths": [
            {
                "path": "\/var\/www\/html\/apps",
                "url": "\/apps",
                "writable": false
            },
            {
                "path": "\/var\/www\/html\/custom_apps",
                "url": "\/custom_apps",
                "writable": true
            }
        ],
        "memcache.locking": "\\OC\\Memcache\\Redis",
        "redis": {
            "host": "***REMOVED SENSITIVE VALUE***",
            "port": 6379
        },
        "instanceid": "***REMOVED SENSITIVE VALUE***",
        "passwordsalt": "***REMOVED SENSITIVE VALUE***",
        "secret": "***REMOVED SENSITIVE VALUE***",
        "trusted_domains": [
            "www.REDACTED.com",
            "192.168.1.230"
        ],
        "trusted_proxies": "***REMOVED SENSITIVE VALUE***",
        "datadirectory": "***REMOVED SENSITIVE VALUE***",
        "overwrite.cli.url": "https:\/\/www.REDACTED.com",
        "overwriteprotocol": "https",
        "dbtype": "mysql",
        "version": "30.0.17.2",
        "dbname": "***REMOVED SENSITIVE VALUE***",
        "dbhost": "***REMOVED SENSITIVE VALUE***",
        "dbport": "",
        "dbtableprefix": "",
        "mysql.utf8mb4": true,
        "dbuser": "***REMOVED SENSITIVE VALUE***",
        "dbpassword": "***REMOVED SENSITIVE VALUE***",
        "installed": true,
        "mail_smtpmode": "smtp",
        "mail_smtpauthtype": "LOGIN",
        "mail_from_address": "***REMOVED SENSITIVE VALUE***",
        "mail_domain": "***REMOVED SENSITIVE VALUE***",
        "mail_smtpauth": 1,
        "mail_smtphost": "***REMOVED SENSITIVE VALUE***",
        "mail_smtpsecure": "ssl",
        "mail_smtpport": "465",
        "mail_smtpname": "***REMOVED SENSITIVE VALUE***",
        "mail_smtppassword": "***REMOVED SENSITIVE VALUE***",
        "loglevel": 0,
        "maintenance": false,
        "theme": ""
    }
}

Apps

The output of occ app:list (if possible).

Enabled:
  - activity: 3.0.0
  - app_api: 4.0.6
  - circles: 30.0.0
  - cloud_federation_api: 1.13.0
  - comments: 1.20.1
  - contactsinteraction: 1.11.0
  - dashboard: 7.10.0
  - dav: 1.31.1
  - federatedfilesharing: 1.20.0
  - federation: 1.20.0
  - files: 2.2.0
  - files_downloadlimit: 3.0.0
  - files_pdfviewer: 3.0.0
  - files_reminders: 1.3.0
  - files_sharing: 1.22.0
  - files_trashbin: 1.20.1
  - files_versions: 1.23.0
  - firstrunwizard: 3.0.0
  - logreader: 3.0.0
  - lookup_server_connector: 1.18.0
  - nextcloud_announcements: 2.0.0
  - notifications: 3.0.0
  - oauth2: 1.18.1
  - password_policy: 2.0.0
  - photos: 3.0.2
  - privacy: 2.0.0
  - provisioning_api: 1.20.0
  - recommendations: 3.0.0
  - related_resources: 1.5.0
  - serverinfo: 2.0.0
  - settings: 1.13.0
  - sharebymail: 1.20.0
  - support: 2.0.0
  - survey_client: 2.0.0
  - systemtags: 1.20.0
  - text: 4.1.0
  - theming: 2.6.0
  - twofactor_backupcodes: 1.19.0
  - updatenotification: 1.20.0
  - user_status: 1.10.0
  - viewer: 3.0.0
  - weather_status: 1.10.0
  - webhook_listeners: 1.1.0-dev
  - workflowengine: 2.12.0
Disabled:
  - admin_audit: 1.20.0
  - bruteforcesettings: 3.0.0 (installed 2.0.1)
  - encryption: 2.18.0
  - files_external: 1.22.0
  - files_rightclick: 0.15.1 (installed 1.6.0)
  - suspicious_login: 8.0.0
  - twofactor_nextcloud_notification: 4.0.0
  - twofactor_totp: 12.0.0-dev
  - user_ldap: 1.21.0

Tips for increasing the likelihood of a response

  • Use the preformatted text formatting option in the editor for all log entries and configuration output.
  • If screenshots are useful, feel free to include them.
    • If possible, also include key error output in text form so it can be searched for.
  • Try to edit log output only minimally (if at all) so that it can be ran through analyzers / formatters by those trying to help you.

The cron and app containers need to be the same images (same version, same variant). This is causing your permission problems (easily fixed generally). It’s also possibly introducing two different versions of Nextcloud in your environment (bad; may or may not lead to bigger problems). Also, your cron container needs your data directory to be mounted on it in the same manner/place as it is on your app container.

Permissions: At the moment you’re deploying the nextcloud:30.0.17-fpm, which uses 33:33. You’re also deploying simultaneously the nextcloud:fpm-alpine image which uses 82:82.

See the docs and examples: https://github.com/nextcloud/docker/tree/master

2 Likes

Thanks for helping, jtr.

I have changed my compose file to be:

services:
  db:
    image: mariadb:lts
    # image: mysql
    command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
    restart: always
    volumes:
      - /share/nc_db:/var/lib/mysql:z
    environment:
      - MYSQL_ROOT_PASSWORD=REDACTED
      - MARIADB_AUTO_UPGRADE=1
      - MARIADB_DISABLE_UPGRADE_BACKUP=1
    env_file:
      - db.env

  redis:
    image: redis:alpine
    restart: always

  app:  
    image: nextcloud:30.0.17-fpm
    restart: always
    volumes:
      - /share/nc_main:/var/www/html:z
      - /share/nc_storage:/var/www/html/data:z
    environment:
      - MYSQL_HOST=db
      - REDIS_HOST=redis      
    env_file:
      - db.env
    depends_on:
      - db
      - redis
      - caddy

  web:
    # using nginx
    build: ./web
    restart: always
    ports:
      - 8091:80      
    volumes:
      - /share/nc_main:/var/www/html:z,ro
      - /share/nc_nginx:/srv
    depends_on:
      - app

  cron:
    image: nextcloud:30.0.17-fpm
    restart: always
    volumes:
      - /share/nc_main:/var/www/html:z
      - /share/nc_storage:/var/www/html/data:z
    entrypoint: /cron.sh
    depends_on:
      - db
      - redis

  caddy:
    image: caddy:latest
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
    ports:
      - "8090:80"
      - "4444:443"
      - "4444:443/udp"
    volumes:
      - /share/nc_init/caddy/Caddyfile:/etc/caddy/Caddyfile
      - /share/nc_init/caddy/srv:/srv
      - /share/nc_init/caddy/caddydata:/data
      - /share/nc_init/caddy/caddyconfig:/config

 
volumes:
  db:
  nextcloud:
  caddy_data:
    external: true
  caddy_config:

My nginx.conf is:

worker_processes auto;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include mime.types;
    default_type  application/octet-stream;
    types {
        text/javascript mjs;
    }

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

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

    sendfile        on;
    #tcp_nopush     on;

    # Prevent nginx HTTP Server Detection
    server_tokens   off;

    keepalive_timeout  65;

    # Set the `immutable` cache control options only for assets with a cache busting `v` argument
    map $arg_v $asset_immutable {
        "" "";
    default ", immutable";
    }

    #gzip  on;

    resolver 127.0.0.11 valid=2s;
    upstream php-handler {
        zone backends 64k;
        server app:9000 resolve;
    }

    server {
        listen 80;	

        # HSTS settings
        # 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 Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;" always;

        # set max upload size and increase upload timeout:
        client_max_body_size 10G;
        client_body_timeout 300s;
        fastcgi_buffers 64 4K;

        # The settings allows you to optimize the HTTP2 bandwidth.
        # See https://blog.cloudflare.com/delivering-http-2-upload-speed-improvements/
        # for tuning hints
        client_body_buffer_size 512k;

        # 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 text/javascript application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/wasm 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;

        # Pagespeed is not supported by Nextcloud, so if your server is built
        # with the `ngx_pagespeed` module, uncomment this line to disable it.
        #pagespeed off;

        # HTTP response headers borrowed from Nextcloud `.htaccess`
        add_header Referrer-Policy                      "no-referrer"       always;
        add_header X-Content-Type-Options               "nosniff"           always;
        add_header X-Frame-Options                      "SAMEORIGIN"        always;
        add_header X-Permitted-Cross-Domain-Policies    "none"              always;
        add_header X-Robots-Tag                         "noindex, nofollow" always;
        add_header X-XSS-Protection                     "1; mode=block"     always;

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

        # Path to the root of your installation
        root /var/www/html;

        # Specify how to handle directories -- specifying `/index.php$request_uri`
        # here as the fallback means that Nginx always exhibits the desired behaviour
        # when a client requests a path that corresponds to a directory that exists
        # on the server. In particular, if that directory contains an index.php file,
        # that file is correctly served; if it doesn't, then the request is passed to
        # the front-end controller. This consistent behaviour means that we don't need
        # to specify custom rules for certain paths (e.g. images and other assets,
        # `/updater`, `/ocm-provider`, `/ocs-provider`), and thus
        # `try_files $uri $uri/ /index.php$request_uri`
        # always provides the desired behaviour.
        index index.php index.html /index.php$request_uri;

        # Rule borrowed from `.htaccess` to handle Microsoft DAV clients
        location = / {
            if ( $http_user_agent ~ ^DavClnt ) {
                return 302 /remote.php/webdav/$is_args$args;
            }
        }

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

        # Make a regex exception for `/.well-known` so that clients can still
        # access it despite the existence of the regex rule
        # `location ~ /(\.|autotest|...)` which would otherwise handle requests
        # for `/.well-known`.
        location ^~ /.well-known {
            # The rules in this block are an adaptation of the rules
            # in `.htaccess` that concern `/.well-known`.

            location = /.well-known/carddav { return 301 /remote.php/dav/; }
            location = /.well-known/caldav  { return 301 /remote.php/dav/; }

            location /.well-known/acme-challenge    { try_files $uri $uri/ =404; }
            location /.well-known/pki-validation    { try_files $uri $uri/ =404; }

            # Let Nextcloud's API for `/.well-known` URIs handle all other
            # requests by passing them to the front-end controller.
            return 301 /index.php$request_uri;
        }

        # Rules borrowed from `.htaccess` to hide certain paths from clients
        location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/)  { return 404; }
        location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console)                { return 404; }

        # Ensure this block, which passes PHP files to the PHP process, is above the blocks
        # which handle static assets (as seen below). If this block is not declared first,
        # then Nginx will encounter an infinite rewriting loop when it prepends `/index.php`
        # to the URI, resulting in a HTTP 500 error response.
        location ~ \.php(?:$|/) {
            # Required for legacy support
            rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode(_arm64)?\/proxy) /index.php$request_uri;

            fastcgi_split_path_info ^(.+?\.php)(/.*)$;
            set $path_info $fastcgi_path_info;

            try_files $fastcgi_script_name =404;

            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_param PATH_INFO $path_info;
            fastcgi_param HTTPS on;

            fastcgi_param modHeadersAvailable true;         # Avoid sending the security headers twice
            fastcgi_param front_controller_active true;     # Enable pretty urls
            fastcgi_pass php-handler;

            fastcgi_intercept_errors on;
            fastcgi_request_buffering off;

            fastcgi_max_temp_file_size 0;
        }

        # Serve static files
        location ~ \.(?:css|js|mjs|svg|gif|ico|jpg|png|webp|wasm|tflite|map|ogg|flac)$ {
            try_files $uri /index.php$request_uri;
            add_header Cache-Control "public, max-age=15778463$asset_immutable";
            add_header Referrer-Policy                   "no-referrer"       always;
            add_header X-Content-Type-Options            "nosniff"           always;
            add_header X-Frame-Options                   "SAMEORIGIN"        always;
            add_header X-Permitted-Cross-Domain-Policies "none"              always;
            add_header X-Robots-Tag                      "noindex, nofollow" always;
            add_header X-XSS-Protection                  "1; mode=block"     always;
            access_log off;     # Optional: Don't log access to assets

            location ~ \.wasm$ {
                default_type application/wasm;
            }
        }

        location ~ \.(otf|woff2?)$ {
            try_files $uri /index.php$request_uri;
            expires 7d;         # Cache-Control policy borrowed from `.htaccess`
            access_log off;     # Optional: Don't log access to assets
        }

        # Rule borrowed from `.htaccess`
        location /remote {
            return 301 /remote.php$request_uri;
        }

        location / {
            try_files $uri $uri/ /index.php$request_uri;
        }
    }
}

The user www-data is now showing on /var/www/html when I exec into the nginx docker instance.

My desktop app is still working. My phone app is still working. My web access console still shows the 403 forbidden response. Thanks in advance for any more ideas!

What are the permissions on this volume/mount point now:

/share/nc_main:/var/www/html:z

They should be 750 (or 755). Likely they’re wrong due to whatever the fallout was from the prior things you’ve now cleaned up. You may need to manually adjust those permissions to get back to a good state.