qdrant-web-ui
qdrant-web-ui copied to clipboard
Support For Non Root Path Hosting
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.
Should be fixed with https://github.com/qdrant/qdrant-web-ui/pull/113
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 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
@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;
}
Any solution for this? I'm unable to work behind ngnix as well..
Any solution for this? I'm unable to work behind ngnix as well..
Could you elaborate why?
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
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?
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.
I agree, an env variable would be great for this
Can we please have a fix for this?
I have same problem, without solution.
same issue here
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?
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?
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
same problem
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?
same problem
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
Of course, I don't understand why the qdrant team hasn't implemented this simple fix yet
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;
}`
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.
this is an open-source, contributions are welcome
Oh, thank you so much for your encouraging words and this amazing project.
And yes, at least I've found my blocking (de)motivator.
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;')
}
}
}
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 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
@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:
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.