qdrant-web-ui icon indicating copy to clipboard operation
qdrant-web-ui copied to clipboard

Support For Non Root Path Hosting

Open rovermicrover opened this issue 2 years ago • 41 comments

The dashboard expects Qdrant to be hosted at the root of a domain otherwise it breaks. Our clusters work by hosting each service, or which Qdrant is one, on the same domain each with their own sub path. So for instance www.foo.com/qdrant.

rovermicrover avatar Aug 03 '23 16:08 rovermicrover

Should be fixed with https://github.com/qdrant/qdrant-web-ui/pull/113

generall avatar Sep 12 '23 15:09 generall

Closing this because we believe this is fixed in Qdrant v1.5.1. Please feel free to open this again if the issue persists.

timvisee avatar Sep 13 '23 15:09 timvisee

@timvisee exposing Qdrant via Nginx with location /qdrant { proxy_pass http://qdrant:6333/; proxy_set_header Host $http_host; proxy_redirect off; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header api-key $http_api_key; }

nothing has changed, my_host/qdrant/dashboard responds 404, while my_host/qdrant and all rests API are ok, as they were before the 1.5.1

matteosdocsity avatar Sep 13 '23 16:09 matteosdocsity

@timvisee exposing Qdrant via Nginx with location /qdrant { proxy_pass http://qdrant:6333/; proxy_set_header Host $http_host; proxy_redirect off; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header api-key $http_api_key; }

nothing has changed, my_host/qdrant/dashboard responds 404, while my_host/qdrant and all rests API are ok, as they were before the 1.5.1

You are correct.

I tried to reproduce this, and yes, the dashboard fails to load. It wrongly tries to load assets from the root path.

By the way, I believe that your Nginx snippet is wrong and that it should be something like the snippet below. But even then, the dashboard would still be unusable.

location ~ ^/qdrant/(.*) {
    proxy_pass http://127.0.0.1:6333/$1?$args;
    proxy_redirect off;
    proxy_http_version 1.1;
}

timvisee avatar Sep 14 '23 13:09 timvisee

Any solution for this? I'm unable to work behind ngnix as well..

Formartha avatar Mar 03 '24 13:03 Formartha

Any solution for this? I'm unable to work behind ngnix as well..

Could you elaborate why?

timvisee avatar Mar 04 '24 10:03 timvisee

Sure. Using NGNIX with docker compose dons't allow me to open the dashboard UI. The code is in this project: https://github.com/Formartha/ai1899

The exact affected files are:

  • https://github.com/Formartha/ai1899/blob/main/docker-compose.yaml
  • https://github.com/Formartha/ai1899/blob/main/nginx.conf

Formartha avatar Mar 04 '24 10:03 Formartha

the fix (getting the base url from window.location) above only works on the js part (and only if the js was already loaded successfully). but the initial load of dashboard/ contains all absolute URLs in the HTML (e.g., /dashboard/assets/index-99eb787e.js) which makes the dashboard try to access https://myurl.com/dashboard/assets/index-99eb787e.js instead of e.g., https://myurl.com/qdrant/dashboard/assets/index-99eb787e.js (if the subpath is qdrant). So the js cannot get loaded in the first place.

Is there a solution to this?

JosuaKrause avatar Mar 18 '24 14:03 JosuaKrause

I have the same problem. I want to expose the dashboard of the first node of the cluster behind Traefik, for example with the following route:

traefik.http.routers.qdrant-http.rule: "Host(server.name) && PathPrefix(/qdrant-test)"

But I can't do that, since the qdrant dashboard does not support an enviroment variable for the base_path=qdrant-test.

So I'm forced to do something like this:

traefik.http.routers.qdrant-http.rule: "Host(server.name) && PathPrefix(/qdrant,/cluster,/dashboard,/collections,/telemetry)"

traefik.http.middlewares.qdrant_sp.stripprefix.prefixes: "/qdrant"

And if I want to expose more instances of qdrant behind the same reverse proxy I'm forced to use different "server.name" for the Host.

fedecompa avatar Mar 18 '24 15:03 fedecompa

I agree, an env variable would be great for this

JosuaKrause avatar Mar 18 '24 17:03 JosuaKrause

Can we please have a fix for this?

shaileshj2803 avatar May 26 '24 07:05 shaileshj2803

I have same problem, without solution.

viniciusraupp avatar Jun 19 '24 23:06 viniciusraupp

same issue here

metalshanked avatar Jun 20 '24 22:06 metalshanked

Same problem here. I have 2 services with my server routed to root page and qdrant routed to /qdrant/ using reverse proxy on nginx. API seems to work but the dashboard won't come up (Shows white blank page) . Tried messing with the config on https://qdrant.tech/documentation/guides/configuration/ but no result. Anyone done this before?

oddFEELING avatar Jul 13 '24 13:07 oddFEELING

Sure. Using NGNIX with docker compose dons't allow me to open the dashboard UI. The code is in this project: https://github.com/Formartha/ai1899

The exact affected files are:

* https://github.com/Formartha/ai1899/blob/main/docker-compose.yaml

* https://github.com/Formartha/ai1899/blob/main/nginx.conf

Checked your repo now, the image i am using for qdrant is qdrant/qdrant, does the current image you pull from solve this issue? it's been a few months since the issue, what is your walk around?

oddFEELING avatar Jul 13 '24 13:07 oddFEELING

Same problem on kubernetes ingress-nginx. As a walk around I had to create an additional ingress object to provide paths to /dashboard, /issues, /collections and /telemetry. Similar to the @fedecompa 's solution (BTW, thanks!). Now I can access dashboard on both myhost.com/dashboard and myhost.com/qdrant/dashboard. There is a drawback indeed. We cannot serve more than one instance on one host.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/rewrite-target: /$2
  labels:
    app: qdrant
  name: qdrant
  namespace: qdrant
spec:
  rules:
  - host: myhost.com
    http:
      paths:
      - backend:
          service:
            name: qdrant
            port:
              number: 6333
        path: /qdrant(/|$)(.*)
        pathType: ImplementationSpecific

---

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: qdrant-dashboard
  namespace: qdrant
  labels:
    app: qdrant
  annotations:
    kubernetes.io/ingress.class: nginx
spec:
  ingressClassName: nginx
  rules:
    - host: myhost.com
      http:
        paths:
          - pathType: Prefix
            backend:
              service:
                name: qdrant
                port:
                  number: 6333
            path: /dashboard
          - pathType: Prefix
            backend:
              service:
                name: qdrant
                port:
                  number: 6333
            path: /issues
          - pathType: Prefix
            backend:
              service:
                name: qdrant
                port:
                  number: 6333
            path: /collections
          - pathType: Prefix
            backend:
              service:
                name: qdrant
                port:
                  number: 6333
            path: /telemetry

Enolerobotti avatar Sep 02 '24 11:09 Enolerobotti

same problem

prd-tuong-nguyen avatar Sep 13 '24 02:09 prd-tuong-nguyen

We are also having this issue still at work where we are hosting qdrant and the assets are not able to be loaded correctly when using a istio virtualservice. Is there a workaround/fix for rewriting the uri's here?

PylotLight avatar Oct 15 '24 00:10 PylotLight

same problem

pleomax0730 avatar Oct 23 '24 03:10 pleomax0730

I've solved this by building from source myself using: ~~bun --bun run build-qdrant --base '/subpath1/subpath2/dashboard/'~~ bun --bun run build This then uses './' path for assets which seems to work. Then I can package this and build qdrant from source with updated frontend.

The above works for assets, BUT:

  • Collections page api expects root path and ignores my custom pathing
  • Quickstart run items uses pull subpath where it should be different from assets/dashboard

PylotLight avatar Oct 28 '24 01:10 PylotLight

Of course, I don't understand why the qdrant team hasn't implemented this simple fix yet

fedecompa avatar Oct 28 '24 08:10 fedecompa

I resolved with this partial solution:

`location /vdb/ { proxy_pass http://0.0.0.0:6333/; 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_connect_timeout       605;
proxy_send_timeout          605;
proxy_read_timeout          605;
send_timeout                605;

proxy_buffering off;
proxy_cache off;

# Riscrive gli URL nel JavaScript
sub_filter 'fetch("/' 'fetch("/vdb/';
sub_filter 'url:"/' 'url:"/vdb/';
sub_filter_once off;
sub_filter_types *;

}

location ~ ^/(dashboard|collections|telemetry) { proxy_pass http://0.0.0.0:6333; 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_connect_timeout       605;
proxy_send_timeout          605;
proxy_read_timeout          605;
send_timeout                605;

proxy_buffering off;
proxy_cache off;

}`

mpsyscons avatar Nov 29 '24 15:11 mpsyscons

Please, please just implement a BASE_URL env var or build the frontend in a coherent way using relative paths. All the struggle people are having (including myself) and all these proposed workarounds would be completely unnecessary, if you guys would react and implement this simple fix.

It doesn't matter what kind of reverse_proxy is used, as just everyone using virtual paths (common practice) will face the very same problem. Is there a downside we are not aware of or what exactly is the blocking motivator here?

And no, it is NOT fixed in the latest version. At best, only partially.

bennyzen avatar Dec 08 '24 16:12 bennyzen

this is an open-source, contributions are welcome

generall avatar Dec 08 '24 23:12 generall

Oh, thank you so much for your encouraging words and this amazing project.

And yes, at least I've found my blocking (de)motivator.

bennyzen avatar Dec 09 '24 10:12 bennyzen

If you are using Apache, you can use this. I don't know the Nginx equivalent but the below the chatgpt o1. I hope this helps everyone.

This does not update the tutorials in the javascript.

apache

  LoadModule substitute_module modules/mod_substitute.so

  RequestHeader unset Accept-Encoding
  RequestHeader append Accept-Encoding "gzip, deflate"

  <Location /qdrant>
    # increase the max line length as the javascript is about 6M 
    SubstituteMaxLineLength 10m 
    AddOutputFilterByType INFLATE;SUBSTITUTE;DEFLATE text/html
    AddOutputFilterByType INFLATE;SUBSTITUTE;DEFLATE text/css
    AddOutputFilterByType INFLATE;SUBSTITUTE;DEFLATE application/javascript

    # dashboard to qdrant/dashboard - in index
    Substitute "s|\"/dashboard|\"/qdrant/dashboard|ni"
    # dashboard to qdrant/dashboard - in css
    Substitute "s|url(/dashboard|url(/qdrant/dashboard|ni"
    # collections to qdrant/collections and other paths to / - in javascript
    Substitute "s|path(\"|path(\"/qdrant|ni"

  </Location>

  ProxyPreserveHost On
  ProxyPass /qdrant/ http://somebackend.namespace.svc.cluster.local:6333/
  ProxyPassReverse /qdrant/ http://somebackend.namespace.svc.cluster.local:6333/

Nginx

http {
    # Enable gunzip: allows NGINX to decompress upstream gzipped responses for sub_filter
    gunzip on;

    # Optionally enable gzip to re-compress the final output back to clients
    gzip on;
    gzip_types text/plain text/css text/javascript application/javascript application/json application/xml text/html;

    server {
        listen 80;
        server_name example.com;

        location /qdrant/ {
            # Forward to your upstream
            proxy_pass http://somebackend.namespace.svc.cluster.local:6333;

            # Force upstream to send gzip or deflate (and not Brotli)
            # This overrides the client's Accept-Encoding
            proxy_set_header Accept-Encoding "gzip, deflate";

            # Decompress upstream response if gzipped
            gunzip on;

            # sub_filter: equivalent to Apache's Substitute directives
            # By default, sub_filter only applies to 'text/html' unless configured otherwise.
            # So we enable it for text/css, application/javascript, etc.:
            sub_filter_types text/html text/css application/javascript text/javascript;

            # NGINX by default replaces only the FIRST match per response chunk.
            # If you want to replace all occurrences, turn that off:
            sub_filter_once off;

            # Your specific replacements:
            # 1) "/dashboard" -> "/qdrant/dashboard"
            sub_filter "\"/dashboard" "\"/qdrant/dashboard";
            # 2) url(/dashboard -> url(/qdrant/dashboard
            sub_filter "url(/dashboard" "url(/qdrant/dashboard";
            # 3) path(" -> path("/qdrant
            sub_filter "path(\"" "path(\"/qdrant";

            # If you need to control large responses or multi-line issues, there's no direct 
            # sub_filter_max_line_length in NGINX. It processes in chunks. Usually "off" for 
            # sub_filter_once is enough for multiple matches.

            # Optionally re-compress the modified response for the client
            # (Already set at http {} level above with 'gzip on;')
        }
    }
}

jrespeto avatar Feb 05 '25 23:02 jrespeto

I've tried the solutions you provided. But I'm not successful. What could be wrong? @mpsyscons @jrespeto

docker-compose.yml

services:
  qdrant1:
    image: qdrant/qdrant:latest
    restart: always
    container_name: qdrant1
    environment:
      - QDRANT__CLUSTER__ENABLED=false
    expose:
      - "6333"
      - "6334"
      - "6335"
    volumes:
      - qdrant1_data:/qdrant/storage

  qdrant2:
    image: qdrant/qdrant:latest
    restart: always
    container_name: qdrant2
    environment:
      - QDRANT__CLUSTER__ENABLED=false
    expose:
      - "6333"
      - "6334"
      - "6335"
    volumes:
      - qdrant2_data:/qdrant/storage

  qdrant3:
    image: qdrant/qdrant:latest
    restart: always
    container_name: qdrant3
    environment:
      - QDRANT__CLUSTER__ENABLED=false
    expose:
      - "6333"
      - "6334"
      - "6335"
    volumes:
      - qdrant3_data:/qdrant/storage

  nginx:
    image: nginx:latest
    container_name: qdrant_nginx
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - qdrant1
      - qdrant2
      - qdrant3

volumes:
  qdrant1_data:
  qdrant2_data:
  qdrant3_data:

nginx.cof

events { }

http {
    gunzip on;
    gzip on;
    gzip_types text/plain text/css text/javascript application/javascript application/json application/xml text/html;

    server {
        listen 80;
        server_name localhost;  # Use your own domain or IP

        # Set the maximum request body size to unlimited
        client_max_body_size 0;

        # Block specific to Qdrant1:
        location /qdrant1/ {
            proxy_pass http://qdrant1:6333/;
            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_connect_timeout 605;
            proxy_send_timeout 605;
            proxy_read_timeout 605;
            send_timeout 605;
            proxy_buffering off;
            proxy_cache off;

            # Rewrite absolute URLs in dynamic JS requests inside the dashboard:
            sub_filter 'fetch("/' 'fetch("/qdrant1/';
            sub_filter 'url:"/' 'url:"/qdrant1/';
            sub_filter_once off;
            sub_filter_types *;
        }

        # Block specific to Qdrant2:
        location /qdrant2/ {
            proxy_pass http://qdrant2:6333/;
            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_connect_timeout 605;
            proxy_send_timeout 605;
            proxy_read_timeout 605;
            send_timeout 605;
            proxy_buffering off;
            proxy_cache off;

            sub_filter 'fetch("/' 'fetch("/qdrant2/';
            sub_filter 'url:"/' 'url:"/qdrant2/';
            sub_filter_once off;
            sub_filter_types *;
        }

        # Block specific to Qdrant3:
        location /qdrant3/ {
            proxy_pass http://qdrant3:6333/;
            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_connect_timeout 605;
            proxy_send_timeout 605;
            proxy_read_timeout 605;
            send_timeout 605;
            proxy_buffering off;
            proxy_cache off;

            sub_filter 'fetch("/' 'fetch("/qdrant3/';
            sub_filter 'url:"/' 'url:"/qdrant3/';
            sub_filter_once off;
            sub_filter_types *;
        }
    }
}

nginx cluster logs:

2025-02-17 01:23:49 2025/02/16 22:23:49 [error] 22#22: *4 open() "/etc/nginx/html/collections" failed (2: No such file or directory), client: 172.24.0.1, server: localhost, request: "GET /collections HTTP/1.1", host: "localhost", referrer: "http://localhost/qdrant2/dashboard"
2025-02-17 01:23:49 2025/02/16 22:23:49 [error] 22#22: *5 open() "/etc/nginx/html/telemetry" failed (2: No such file or directory), client: 172.24.0.1, server: localhost, request: "GET /telemetry HTTP/1.1", host: "localhost", referrer: "http://localhost/qdrant2/dashboard"
2025-02-17 01:23:49 2025/02/16 22:23:49 [error] 22#22: *1 open() "/etc/nginx/html/collections" failed (2: No such file or directory), client: 172.24.0.1, server: localhost, request: "GET /collections HTTP/1.1", host: "localhost", referrer: "http://localhost/qdrant2/dashboard"
2025-02-17 01:23:50 2025/02/16 22:23:50 [error] 22#22: *4 open() "/etc/nginx/html/telemetry" failed (2: No such file or directory), client: 172.24.0.1, server: localhost, request: "GET /telemetry HTTP/1.1", host: "localhost", referrer: "http://localhost/qdrant2/dashboard"
2025-02-17 01:23:50 2025/02/16 22:23:50 [error] 22#22: *5 open() "/etc/nginx/html/collections" failed (2: No such file or directory), client: 172.24.0.1, server: localhost, request: "GET /collections HTTP/1.1", host: "localhost", referrer: "http://localhost/qdrant2/dashboard"
2025-02-17 01:23:50 2025/02/16 22:23:50 [error] 22#22: *8 open() "/etc/nginx/html/collections" failed (2: No such file or directory), client: 172.24.0.1, server: localhost, request: "GET /collections HTTP/1.1", host: "localhost", referrer: "http://localhost/qdrant2/dashboard"
2025-02-17 01:24:03 2025/02/16 22:24:03 [error] 22#22: *5 open() "/etc/nginx/html/telemetry" failed (2: No such file or directory), client: 172.24.0.1, server: localhost, request: "GET /telemetry HTTP/1.1", host: "localhost", referrer: "http://localhost/qdrant2/dashboard"
2025-02-17 01:24:03 2025/02/16 22:24:03 [error] 22#22: *1 open() "/etc/nginx/html/collections" failed (2: No such file or directory), client: 172.24.0.1, server: localhost, request: "GET /collections HTTP/1.1", host: "localhost", referrer: "http://localhost/qdrant2/dashboard"
2025-02-17 01:25:13 2025/02/16 22:25:13 [error] 22#22: *13 open() "/etc/nginx/html/collections" failed (2: No such file or directory), client: 172.24.0.1, server: localhost, request: "GET /collections HTTP/1.1", host: "localhost", referrer: "http://localhost/qdrant3/dashboard"
2025-02-17 01:25:13 2025/02/16 22:25:13 [error] 22#22: *14 open() "/etc/nginx/html/telemetry" failed (2: No such file or directory), client: 172.24.0.1, server: localhost, request: "GET /telemetry HTTP/1.1", host: "localhost", referrer: "http://localhost/qdrant3/dashboard
```"

gururaser avatar Feb 16 '25 22:02 gururaser

@gururaser Usually, I'm using the networks definition into a yml file to using http://qdrant1 call between containers. I don't know if it's your case because it seems all correct for the rest of the code

mpsyscons avatar Feb 17 '25 14:02 mpsyscons

@generall

Here is a working nginx config and docker-compose just add the location blocks for other paths.

events {

}

http {
    # Enable gunzip: allows NGINX to decompress upstream gzipped responses for sub_filter
    gunzip on;

    # Optionally enable gzip to re-compress the final output back to clients
    gzip on;
    gzip_types text/plain text/css text/javascript application/javascript application/json application/xml;

    server {
        listen 80;
        server_name localhost;

        location /qdrant/ {
            # Forward to your upstream
            proxy_pass http://qdrant:6333/;
            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_connect_timeout 605;
            proxy_send_timeout 605;
            proxy_read_timeout 605;
            send_timeout 605;
            proxy_buffering on;
            proxy_buffers 8 16k;
            proxy_buffer_size 32k;
            proxy_cache off;


            # Ensure gzip decompression for sub_filter
            gunzip on;
            
            # Force upstream to send gzip or deflate (and not Brotli)
            # This overrides the client's Accept-Encoding
            proxy_set_header Accept-Encoding "";
            # proxy_set_header Accept-Encoding "gzip, deflate";

            # sub_filter: equivalent to Apache's Substitute directives
            # By default, sub_filter only applies to 'text/html' unless configured otherwise.
            # So we enable it for text/css, application/javascript, etc.:
            sub_filter_types text/html text/css text/javascript application/javascript application/json;


            # NGINX by default replaces only the FIRST match per response chunk.
            # If you want to replace all occurrences, turn that off:
            sub_filter_once off;

            # Your specific replacements:
            # 1) "/dashboard" -> "/qdrant/dashboard"
            sub_filter "\"/dashboard" "\"/qdrant/dashboard";
            # 2) url(/dashboard -> url(/qdrant/dashboard
            sub_filter "url(/dashboard" "url(/qdrant/dashboard";
            # 3) path(" -> path("/qdrant
            sub_filter "path(\"" "path(\"/qdrant";

            # If you need to control large responses or multi-line issues, there's no direct
            # sub_filter_max_line_length in NGINX. It processes in chunks. Usually "off" for
            # sub_filter_once is enough for multiple matches.

            # Optionally re-compress the modified response for the client
            # (Already set at http {} level above with 'gzip on;')
        }
    }
}

services:
  qdrant:
    image: qdrant/qdrant:latest
    restart: always
    container_name: qdrant
    volumes:
      - qdrant_data:/qdrant/storage

  nginx:
    image: docker.io/nginx:latest
    container_name: qdrant_nginx
    ports:
      - "2222:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro

volumes:
  qdrant_data:

jrespeto avatar Feb 18 '25 15:02 jrespeto

I am having the same issue. Its almost 2 years later - has nobody found a solution for this?

@jrespeto I tried your solution with nginx on K8s and couldn't get it working.

Pratish315 avatar Feb 27 '25 16:02 Pratish315