NGINX reverse proxy configuration after version 2.1

Hello there,

I have been using Duplicati a long time to backup my Next Cloud.
Duplicati is installed on the same host as Next Cloud so therefore I use the reverse proxy functionality of NGINX with a subdirectory (also the web server for Next Cloud) to reach the web interface of Duplicati and protect it with the same certificate as Next Cloud.
Now that I have updated to 2.1.0.3 the reverse proxy does not work anymore - did use it before without password authentication - and I tried many different configurations for NGINX.
I also configured a password with the necessary steps so I does work like this:
http://:8400

My old NGINX config working with version 2.0.x of Duplicati looks like this:

location ^~ /backup/ {
  proxy_pass http://<IP address of host>:8200/;
  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;
}

I also tried to issue a forever token (ServerUtil | Duplicati) and modified my NGINX config like this:

location ^~ /backup/ {
        proxy_set_header Authorization "Bearer <token>";
        proxy_pass http://<IP address>:8200/;
        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;
}

Is there anyone out there who had the same problem and does know how to solve this?

Thanks in advance,

Markus

Hi Markus,
I found here Can't access Duplicati through nginx - Docker - openmediavault a solution. You need to add “websocket” options to the proxy configuration.
After adding

    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";

to the nginx proxy configuration it worked for me.
Best regards,
Michael

Hi Micheal,

thank you so much for trying to help me.

First things first:
Duplicati is installed on Debian 12 without a GUI (headless server).

I added your two lines (tried something like this a few days ago after I “consulted” ChatGPT), but I am still stuck in a loop showing a message window with Duplicati in the background:

Connection lost: Connection to server was rejected due to invalid authentication.
Log in again, or re-open the page from the TrayIcon (if applicable)
Buttons: Help, Log in, and Reload

When I press reload or login, same message and screen appears in Chrome, in Safari I get to the login (pressing the login button), but after logging in getting back in the loop again.

I paste my complete nginx conf here for more information:

server {
  listen 443      ssl default_server;
  http2 on;
  server_name next.cloud;
  ssl_certificate /etc/ssl/certs/next.cloud.crt;
  ssl_certificate_key /etc/ssl/private/next.cloud.key;
  ssl_trusted_certificate /etc/ssl/certs/outerheavenlocal-rootCA-cert.pem;
  #ssl_certificate /etc/letsencrypt/rsa-certs/fullchain.pem;
  #ssl_certificate_key /etc/letsencrypt/rsa-certs/privkey.pem;
  #ssl_certificate /etc/letsencrypt/ecc-certs/fullchain.pem;
  #ssl_certificate_key /etc/letsencrypt/ecc-certs/privkey.pem;
  #ssl_trusted_certificate /etc/letsencrypt/ecc-certs/chain.pem;
  ssl_dhparam /etc/ssl/certs/dhparam.pem;
  ssl_session_timeout 1d;
  ssl_session_cache shared:SSL:50m;
  ssl_session_tickets off;
  ssl_protocols TLSv1.3 TLSv1.2;
  ssl_ciphers 'TLS-CHACHA20-POLY1305-SHA256:TLS-AES-256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384';
  ssl_ecdh_curve X448:secp521r1:secp384r1;
  ssl_prefer_server_ciphers on;
  ssl_stapling on;
  ssl_stapling_verify on;
  client_max_body_size 10G;
  client_body_timeout 3600s;
  client_body_buffer_size 512k;
  fastcgi_buffers 64 4K;
  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/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;
  add_header Strict-Transport-Security            "max-age=15768000; includeSubDomains; preload;" always;
  add_header Permissions-Policy                   "interest-cohort=()";
  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                         "noindex, nofollow" always;
  add_header X-XSS-Protection                     "1; mode=block" always;
  fastcgi_hide_header X-Powered-By;
  include mime.types;
  types {
    text/javascript mjs;
  }
  root /var/www/nextcloud;
  index index.php index.html /index.php$request_uri;
  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;
  }
  location ^~ /.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; }
    return 301 /index.php$request_uri;
  }
  location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/)  { return 404; }
  location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console)                { return 404; }
  location ~ \.php(?:$|/) {
    rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode\/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;
    fastcgi_param front_controller_active true;
    fastcgi_pass php-handler;
    fastcgi_intercept_errors on;
    fastcgi_request_buffering off;
    fastcgi_read_timeout 3600;
    fastcgi_send_timeout 3600;
    fastcgi_connect_timeout 3600;
    fastcgi_max_temp_file_size 0;
  }
  location ~ \.(?:css|js|mjs|svg|gif|png|jpg|ico|wasm|tflite|map)$ {
    try_files $uri /index.php$request_uri;
    add_header Cache-Control "public, max-age=15778463, $asset_immutable";
    expires 6M;
    access_log off;
    location ~ \.wasm$ {
      default_type application/wasm;
    }
  }
  location ~ \.woff2?$ {
    try_files $uri /index.php$request_uri;
    expires 7d;
    access_log off;
  }
  location /remote {
    return 301 /remote.php$request_uri;
  }
  
  location ^~ /backup/ {
    proxy_set_header Authorization "Bearer %token%";
    proxy_pass http://127.0.0.1:8200/;
    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 Upgrade $http_upgrade;
    proxy_set_header Connection “upgrade”;
  }
  
  location / {
    try_files $uri $uri/ /index.php$request_uri;
  }
}

Thanks in advance for your help.

Cheers,
Markus

It sounds like the websocket is failing to connect.

Can you try the developer tools in the browser and see in the network tab if there are any errors?

Hi,
sorry for taking so many days to answer but my youngest got an op last Friday.
I check the output in the developer tools and got this:

Refused to apply style from ‘https://next.cloud/backup/ngax/styles/dark.css’ because its MIME type (‘text/html’) is not a supported stylesheet MIME type, and strict MIME checking is enabled.

The same for default.css and then many like this:

GET https://next.cloud/backup/ngax/scripts/controllers/EditBackupController.js?v=2.1.0.4 net::ERR_ABORTED 404 (Not Found)Understand this errorAI
index.html:81

Funny thing though: I didn’t change the config of nginx after updating from 2.0.x. to 2.1.x

Thanks for your help in advance.

Cheers

I am thinking something else is being served here? The MIME type is always set by the Duplicati server. Could this be a text document? Error page or similar?

I can see from the config that there are is some extra configuration in the file that mentions .css files with a try_files command.

That is the correct path. I asked ChatGPT for a fix, and it suggest that you need a URL rewrite to map /backup/ngax/... to /ngax/...:

  location ^~ /backup/ {
    rewrite ^/backup/(.*)$ /$1 break;
    proxy_set_header Authorization "Bearer %token%";
    proxy_pass http://127.0.0.1:8200/;
    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 Upgrade $http_upgrade;
    proxy_set_header Connection “upgrade”;
  }

I have not tested it, but the answer makes sense to me.

Thank you so much for trying to help me.
I gave it a shot, but when I try https://next.cloud/backup I am redirected to https://next.cloud/backup/ngax/index.html and I get the “Connection lost” - with “Help”, “Log in”, and “Reload”. “Reload” loads the same screen and “Log in” redirects to https://next.cloud/login (no backup directory
). That results in a page not found from Next Cloud. When I enter https://next.cloud/backup/login then I reach the login screen, but after entering my credentials get to the “Connection lost” again


And developer mode shows: Failed to load resource: the server responded with a status of 401 () /backup/api/v1/auth/refresh:1

I also tried “Duplicati - 2.1.0.4_stable_2025-01-31” - same prob.

There are no changes to the way the UI/server works in that version, so it should behave the same.

When you enter the password on the /login page, that will set a cookie that is then passed to /backup/api/v1/auth/refresh. Since you get a 401, most likely that cookie is not being passed.
Do you see any error messages that could indicate why it is not there?

Can you confirm that the cookie is missing on the request? Can you see it being set on the login call?

Hi,
I tried my best and also made use of ChatGPT and now I can login when using https://next.cloud/backup/login.html, but whenever I use https://next.cloud/backup I am redirected to “Help”, “Log in”, and “Reload” again. Then reload is a loop and login redirects to https://next.cloud/login.html. ChatGPT tried to solve this with subfilters but that did not work. Here my current nginx configuration for the Duplciati proxy part:
location ^~ /backup/ {
proxy_pass http://127.0.0.1:8200/;
proxy_http_version 1.1;
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 Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection “upgrade”;

proxy_cookie_path / /backup/;

}

The additional did not help:
proxy_redirect ~^(http[s]?://[^/]+)(/.*)$ $1/backup$2;

# Enable response modification
sub_filter_once off;
sub_filter '/login.html' '/backup/login.html';
sub_filter 'location.href = "/login.html"' 'location.href = "/backup/login.html"';

# Ensure sub_filter works for text/html and JavaScript files
proxy_set_header Accept-Encoding "";

Thanks for joining me :slight_smile:

Can you try to enable Developer Tools in the browser and see what errors you get? This will point to the problem with the login, and we can hopefully fix it once we know what is going wrong.

Good evening, well, when I logout I get redirected to https://next.cloud/login.html and the developer tools show:

GET https://next.cloud/login.html 404 (Not Found)

(anonymous) @ backup/ngax/scripts/
ler.js?v=2.1.0.4:43
(anonymous) @ backup/ngax/scripts/
/angular.min.js:120
$eval @ backup/ngax/scripts/
/angular.min.js:134
$digest @ backup/ngax/scripts/
/angular.min.js:132
$apply @ backup/ngax/scripts/
/angular.min.js:135
l @ backup/ngax/scripts/
r/angular.min.js:87
F @ backup/ngax/scripts/
r/angular.min.js:91
(anonymous) @ backup/ngax/scripts/
r/angular.min.js:92
XMLHttpRequest.send
(anonymous) @ backup/ngax/scripts/
r/angular.min.js:92
n @ backup/ngax/scripts/
r/angular.min.js:88
(anonymous) @ backup/ngax/scripts/
r/angular.min.js:86
(anonymous) @ backup/ngax/scripts/
/angular.min.js:120
$eval @ backup/ngax/scripts/
/angular.min.js:134
$digest @ backup/ngax/scripts/
/angular.min.js:132
$apply @ backup/ngax/scripts/
/angular.min.js:135
(anonymous) @ backup/ngax/scripts/
/angular.min.js:252
dispatch @ backup/ngax/scripts/libs/jquery.min.js:3
n.event.add.r.handle @ backup/ngax/scripts/libs/jquery.min.js:3

For login.html the developer tools show the following output:

Request URL:
https://next.cloud/login.html
Request Method:
GET
Status Code:
404 Not Found
Remote Address:
10.70.4.253:443
Referrer Policy:
no-referrer
cache-control:
no-cache, no-store, must-revalidate
content-encoding:
gzip
content-security-policy:
default-src ‘none’;base-uri ‘none’;manifest-src ‘self’;script-src ‘nonce-A9ADmtlWMQI2Jn8w79dL3O4b1e5bqo+ioviIbPOF5B4=’;script-src-elem ‘strict-dynamic’ ‘nonce-A9ADmtlWMQI2Jn8w79dL3O4b1e5bqo+ioviIbPOF5B4=’;style-src ‘self’ ‘unsafe-inline’;img-src ‘self’ data: blob: https://.tile.openstreetmap.org;font-src ‘self’ data:;connect-src ‘self’;media-src ‘self’;frame-src ‘self’;frame-ancestors ‘self’;form-action ‘self’
content-type:
text/html; charset=UTF-8
date:
Fri, 14 Feb 2025 19:48:07 GMT
feature-policy:
autoplay ‘self’;camera ‘none’;fullscreen ‘self’;geolocation ‘none’;microphone ‘none’;payment ‘none’
referrer-policy:
no-referrer
server:
nginx
strict-transport-security:
max-age=15768000; includeSubDomains; preload;
vary:
Accept-Encoding
x-content-type-options:
nosniff
x-download-options:
noopen
x-frame-options:
SAMEORIGIN
x-permitted-cross-domain-policies:
none
x-request-id:
wg2B5zNEY51DqsEoUJXp
x-robots-tag:
noindex, nofollow
x-robots-tag:
noindex, nofollow
x-xss-protection:
1; mode=block
:authority:
next.cloud
:method:
GET
:path:
/login.html
:scheme:
https
accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,
/*;q=0.8,application/signed-exchange;v=b3;q=0.7
accept-encoding:
gzip, deflate, br, zstd
accept-language:
en-DE,en;q=0.9,en-US;q=0.8,de;q=0.7
cookie:
__Host-nc_sameSiteCookielax=true; __Host-nc_sameSiteCookiestrict=true; nc_username=seraphia; default-theme=ngax; oc_sessionPassphrase=iZuIeyFuLHopAcrlXVYDI%2FQq2hgKrm3BG7jIuyOPrxTPSwPqrNt3TGYPYuLOfpH9Nrlt6%2FZG%2F4uFxjeiolgfqeMOLb3C26YuFauhsmDw98Ujm1LkbS4vwLwvgD5CIuq3; oc4jao98pgcr=n6uc27dsvmlavq9m7ekg3ta0cm; nc_token=uq2YIWejvIfzNe9ay17fknXFgPzqoETI; nc_session_id=n6uc27dsvmlavq9m7ekg3ta0cm
priority:
u=0, i
sec-ch-ua:
“Not(A:Brand”;v=“99”, “Google Chrome”;v=“133”, “Chromium”;v=“133”
sec-ch-ua-mobile:
?0
sec-ch-ua-platform:
“macOS”
sec-fetch-dest:
document
sec-fetch-mode:
navigate
sec-fetch-site:
same-origin
sec-fetch-user:
?1
upgrade-insecure-requests:
1
user-agent:
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
ï»ż
session-heartbeat.js:83 session heartbeat polling started

I don’t really know what else I should look for


By the way - with the following enabled in the nginx configuration the error message is empty:

# Enable response modification
sub_filter_once off;
sub_filter '/login.html' '/backup/login.html';
sub_filter 'location.href = "/login.html"' 'location.href = "/backup/login.html"';

# Ensure sub_filter works for text/html and JavaScript files
proxy_set_header Accept-Encoding "";

There is clearly some path rewrite that is breaking. Looking at your configuration, I would think that:

https://next.cloud/backup -> http://localhost:8200

With that logic, the following should hold:

https://next.cloud/backup/index.html -> http://localhost:8200/index.html
https://next.cloud/backup/login.html -> http://localhost:8200/login.html

The trace you posted is for https://next.cloud/login.html which is clearly wrong, as it hits the proxy server and not Duplicati. If this is emitted by Duplicati, we should fix it to be more proxy friendly.

Can you try again with the developer tools and go (manually if needed) to https://next.cloud/backup/login.html and try the login there?

This should call (XHR) https://next.cloud/backup/api/v1/auth/login and return a cookie.
Then the page should redirect to https://next.cloud/backup/ngax/index.html.
This should call (XHR) https://next.cloud/backup/api/v1/auth/refresh and return an access token.
Finally, this should be used to open a websocket on https://next.cloud/backup/notifications.

Let me know where there are errors or where it diverges from the expected flow.

I go to https://next.cloud/backup/login.html, enter my password and access the main page.
Now these are the infos / errors from the login process via https://next.cloud/backup/login.html:

  • only error is: GET https://next.cloud/login.html 404 (Not Found)
    Something is wrong with the redirect / sub filtering when using logout or the “Log in” button from the “Connection Lost” window.

Current nginx config for these tests after consulting ChatGPT again:

location ^~ /backup/ {
    proxy_pass http://127.0.0.1:8200/;
    proxy_http_version 1.1;
    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 Host $host;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";

    # Fix cookie paths
    proxy_cookie_path / /backup/;

    # Enable sub_filter for multiple types
    sub_filter_once off;
    sub_filter_types text/html text/javascript application/javascript;

    # Debugging: Modify title on login page
    sub_filter 'Duplicati Login' 'Duplicati Login - Modified by Nginx';

    # Fix JavaScript redirects from login
    sub_filter 'window.location = "./";' 'window.location = "/backup/";';
    sub_filter 'window.location="./";' 'window.location="/backup/";';

    # Fix relative paths for login page and resources (login.html)
    sub_filter 'href="login/' 'href="/backup/login/';
    sub_filter 'src="login/' 'src="/backup/login/';
    sub_filter 'href="oem/root/login/' 'href="/backup/oem/root/login/';
    sub_filter 'src="oem/root/login/' 'src="/backup/oem/root/login/';

    # Fix login.html absolute URL redirects
    sub_filter 'location.href = "/login.html"' 'location.href = "/backup/login.html"';
    sub_filter 'location.href="/login.html"' 'location.href="/backup/login.html"';

    # Ensure upstream isn't sending compressed responses
    proxy_set_header Accept-Encoding "identity";

    # Debugging logs
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log debug;
}

location ^~ /backup/api/ {
    proxy_pass http://127.0.0.1:8200/api/;
    proxy_http_version 1.1;

    proxy_set_header Host $host;

    # Preserve cookies
    proxy_cookie_path / /backup/;
}

The mouse over the log in button in “Connection Lost” shows the URL https://next.cloud/backup/ngax/index.html

That one is an unrelated error. It seems that the UI sometimes uses an absolute redirect, and that trips up the page. This will just affect the login, and you have to manually adjust the URL by adding back the missing /backup/ part.

It is perhaps possible to fix this with elaborate rewrite rules, but I would argue that it is not worth it, and the problem should be fixed in Duplicati.

That is a different request and it is just fetching a static page.

Here you can see the request for notifications on my test setup. I think this request is failing on your system:

On your system, the request should be made to

wss://next.cloud/backup/notifications?token=...

If the response is not “101 Switching Protocols” then this is where it fails, and this needs to be handled with proxy configuration.

Thanks for engagement. I don’t really know what you expect me to do now?
I logged out and logged in again while keeping the developer mode open.
There is nothing with wss (any hint for the web socket) and switching protocols. Furthermore I shared my nginx configuration.

location ^~ /backup/ {
proxy_pass http://127.0.0.1:8200/;
proxy_http_version 1.1;

proxy_redirect ~^(/login.html)$ /backup$1;

# Increase timeouts
proxy_connect_timeout 120s;
proxy_send_timeout 120s;
proxy_read_timeout 120s;

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 Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

This already contains everything necessary for proxy web socket.

Gave ChatGPT another try and this works now:

location /login.html {
        return 301 /backup/login.html;
}

location ^~ /backup/ {
    proxy_pass http://localhost:8200/;
    proxy_http_version 1.1;

    proxy_redirect ~^(/login.html)$ /backup$1;

    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 Host $host;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";

    # Fix cookie paths
    proxy_cookie_path / /backup/;

    # Enable sub_filter for multiple types
    sub_filter_once off;
    sub_filter_types text/html text/javascript application/javascript;

    # Fix JavaScript redirects from login
    sub_filter 'window.location = "./";' 'window.location = "/backup/";';
    sub_filter 'window.location="./";' 'window.location="/backup/";';

    # Fix relative paths for login page and resources (login.html)
    sub_filter 'href="/login.html' 'href="/backup/login.html';
    sub_filter 'src="/login.html' 'src="/backup/login.html';

    # Fix login.html absolute URL redirects
    sub_filter 'location.href = "/login.html"' 'location.href = "/backup/login.html"';
    sub_filter 'location.href="/login.html"' 'location.href="/backup/login.html"';

    # Fix logout redirect
    sub_filter 'window.location = "./login.html";' 'window.location = "/backup/login.html";';
    sub_filter 'window.location="./login.html";' 'window.location="/backup/login.html";';

    # Ensure upstream isn't sending compressed responses
    proxy_set_header Accept-Encoding "identity";

    # Debugging logs
    error_log /var/log/nginx/error.log debug;
}

location ^~ /backup/api/ {
    proxy_pass http://localhost:8200/api/;
    proxy_http_version 1.1;

    proxy_set_header Host $host;

    # Preserve cookies
    proxy_cookie_path / /backup/;
}
1 Like