The Basics:
- Nextcloud Server Version: 31.0.1
- Operating System: Ubuntu 24.04
- Web Server: nginx 1.24
- Reverse Proxy: nginx 1.24
- PHP Version: PHP 8.4 (FPM)
- Using Cloudflare/mod_security or similar: No
Summary of the Issue:
I’m running a multi‑VM Nextcloud deployment behind an Nginx reverse proxy. My setup includes two application servers:
- VM-A: Handles standard Nextcloud operations (file browsing, PHP processing, etc.).
- VM-B: Dedicated to handling heavy file uploads and chunked uploads
Both servers share the same Nextcloud configuration and data via NFS, with Redis managing file locking and sessions. The goal is to have all file uploads – particularly those using chunked uploads and webdav trafic – routed to VM‑B. This way, if the upload process becomes overloaded or crashes, browsing and file viewing and download functions (served by VM‑A) remain unaffected.
Despite various configuration attempts, uploads still end up on VM‑A (or folder not found when browsing files when webdav uploads were working in VM-B). Additionally, modifying the OPTIONS handling has led to errors (404s, 500s, folder not found, etc.). I’d appreciate any guidance on how to adjust my Nginx config to reliably split the traffic as intended.
Configuration:
Below is my original Nginx configuration :
# /etc/nginx/sites-available/site.conf
server {
listen 80;
server_name EXAMPLE.DOMAIN.COM;
# Allow Prometheus to access nginx_status without redirection
location /nginx_status {
stub_status;
allow 127.0.0.1;
deny all;
access_log off;
add_header Content-Type "text/plain";
}
# Redirect everything else to HTTPS, but not /nginx_status
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name EXAMPLE.DOMAIN.COM;
ssl_certificate /path/to/ssl/fullchain.pem;
ssl_certificate_key /path/to/ssl/privkey.pem;
# SSL settings
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
# Security headers
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
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-XSS-Protection "1; mode=block" always;
# CORS headers
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, PROPFIND, OPTIONS, REPORT, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK' always;
add_header Access-Control-Allow-Headers 'Authorization, Content-Type, Depth, Destination, DNT, If-Modified-Since, Keep-Alive, OCS-APIREQUEST, Origin, Referer, User-Agent, X-Requested-With, X-OC-Mtime' always;
add_header Access-Control-Allow-Credentials 'true' always;
# Upload limitations
client_max_body_size 800G;
client_body_timeout 7200s;
# Proxy buffer settings for large responses
proxy_buffers 8 4k;
proxy_buffer_size 4k;
# Common proxy settings
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 7200;
proxy_send_timeout 7200;
# Handle OPTIONS requests - ONLY match OPTIONS method specifically
location ~ ^/ {
if ($request_method = 'OPTIONS') {
add_header Access-Control-Max-Age 1728000;
add_header Content-Type 'text/plain charset=UTF-8';
add_header Content-Length 0;
return 204;
}
# This request isn't OPTIONS, so pass to the next matching location block
proxy_pass http://VM-A_IP;
}
# Static content handling
location ~* \.(?:css|js|woff2?|svg|gif|map|png|html|ttf|ico|jpg|jpeg|eot)$ {
proxy_pass http://VM-A_IP;
proxy_set_header Host $host;
proxy_buffering off;
proxy_request_buffering off;
proxy_cache_bypass $http_pragma;
proxy_cache_revalidate on;
expires 7d;
access_log off;
}
# PHP processing - specific to avoid loops
location ~ ^/(?:index|remote|public|cron|core/ajax/update|status|ocs/v[12]|updater|oc[ms]-provider|ocm-provider)\.php(?:$|/) {
proxy_pass http://VM-A_IP;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_max_temp_file_size 0;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffering off;
proxy_request_buffering off;
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
}
# WebDAV routes for all file operations including Photos app
location ~ ^/remote\.php/(?:webdav|dav|caldav|carddav|davs|files|photos|preview)(?:$|/) {
proxy_pass http://VM-B_IP;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header OCS-APIRequest "true";
proxy_set_header X-Requested-With $http_x_requested_with;
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
proxy_cookie_path / "/; secure; HttpOnly; SameSite=None";
proxy_buffering off;
proxy_request_buffering off;
proxy_max_temp_file_size 0;
client_body_buffer_size 512k;
proxy_read_timeout 7200;
proxy_connect_timeout 300;
proxy_send_timeout 300;
proxy_pass_header Content-Type;
proxy_hide_header set-cookie;
}
# Photos app specific handling
location ~ ^/index\.php/apps/photos/ {
proxy_pass http://VM-A_IP;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# API access
location ~ ^/ocs/v[12]\.php/ {
proxy_pass http://VM-A_IP;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header OCS-APIRequest "true";
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
}
# Preview generation
location ~ ^/(?:core|apps)/preview\.(?:php|png|jpg|webp)(?:$|/) {
proxy_pass http://VM-B_IP;
proxy_set_header Host $host;
proxy_buffering off;
proxy_request_buffering off;
}
# Chunked upload handling
location ~ ^/remote\.php/dav/uploads {
proxy_pass http://VM-B_IP;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Requested-With $http_x_requested_with;
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
proxy_buffering off;
proxy_request_buffering off;
proxy_max_temp_file_size 0;
client_body_buffer_size 512k;
proxy_connect_timeout 300;
proxy_send_timeout 300;
proxy_read_timeout 7200;
proxy_pass_header Content-Type;
}
# Default route
location / {
proxy_pass http://VM-A_IP;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_buffering off;
proxy_request_buffering off;
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
}
}
What I’ve Tried:
I’ve attempted to split the traffic by routing:
- All requests to
/remote.php/dav/uploads
(and related chunked upload endpoints) to VM‑B. - Other Nextcloud operations (static files, PHP processing, previews, etc.) remain on VM‑A.
- I also tried different OPTIONS handling blocks for CORS preflight, but this either routes uploads incorrectly or produces errors like 404/500, “folder not found,” etc.
Despite these efforts, uploads are not reliably directed to VM‑B, which is critical because heavy uploads can cause VM‑A to slow down or crash—affecting user browsing and downloads.
Any suggestions on how to tweak this configuration or an alternative approach to splitting the load would be greatly appreciated!