MauriceNino / dashdot

A simple, modern server dashboard, primarily used by smaller private servers
https://getdashdot.com/
MIT License
2.47k stars 92 forks source link

[Feature] Allow hosting from suburl #450

Open MauriceNino opened 1 year ago

MauriceNino commented 1 year ago

Description of the feature

Users want to host their applications from a suburl, instead of a subdomain (e.g. https://example.com/dashdot)

Additional context

Other applications support it as well: https://wiki.servarr.com/sonarr/settings

btxtiger commented 1 year ago

This would require setting a base-href in the frontend to have the correct urls in the frontend. So having dashdot running at a subroute /dashdot, the index.html would require having <base href="/dashdot/">. However the socket.io URL is still wrong, so it has also to be adjusted.

Quickfix would be checking an env variable in a start script and update the files accordingly.

theyurii commented 1 year ago

I was able to make it work.

Here is the diff of the fix:

diff --git a/apps/view/project.json b/apps/view/project.json
index d71c92ad..cc32fb11 100644
--- a/apps/view/project.json
+++ b/apps/view/project.json
@@ -11,7 +11,7 @@
         "compiler": "babel",
         "outputPath": "dist/apps/view",
         "index": "apps/view/src/index.html",
-        "baseHref": "/",
+        "baseHref": "",
         "main": "apps/view/src/index.tsx",
         "tsConfig": "apps/view/tsconfig.json",
         "assets": [
diff --git a/apps/view/src/index.css b/apps/view/src/index.css
index 7f87b5cf..38843217 100644
--- a/apps/view/src/index.css
+++ b/apps/view/src/index.css
@@ -2,7 +2,7 @@
   font-family: 'Inter';
   font-style: normal;
   font-display: swap;
-  src: url(/assets/Inter.ttf) format('woff2');
+  src: url(assets/Inter.ttf) format('woff2');
 }

 body {
diff --git a/apps/view/src/services/page-data.ts b/apps/view/src/services/page-data.ts
index f19c50cc..1711a762 100644
--- a/apps/view/src/services/page-data.ts
+++ b/apps/view/src/services/page-data.ts
@@ -23,7 +23,7 @@ export const usePageData = () => {
   const config = serverInfo?.config;

   useEffect(() => {
-    const socket = io(environment.backendUrl);
+    const socket = io(window.location.origin, { path: `${window.location.pathname}/socket.io/` });

     socket.on('static-info', data => {
       setServerInfo(data);
@@ -45,7 +45,7 @@ export const usePageData = () => {
   useEffect(() => {
     let socket: Socket | undefined;
     if (config) {
-      socket = io(environment.backendUrl);
+      socket = io(window.location.origin, { path: `${window.location.pathname}/socket.io/` });

       socket.on('cpu-load', data => {
         setCpuLoad(oldData => {

I tested it behind an nginx reverse proxy with the following config:

events {
  # defaults
}

http {
  server {
    listen 80;

    location /dashdot/ {
      proxy_pass http://dashdot:3001/;

      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
    }
  }
}

And ran with docker compose using this compose.yaml file:

services:
  dashdot:
    build: .
    networks:
      - common

  nginx:
    image: nginx:latest
    networks:
      - common
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    ports:
      - 80:80

networks:
  common:
MauriceNino commented 1 year ago

@theyurii

Hi there, thanks for your work. I have already started implementing this feature on my own on the branch feature/subpath-hosting, but did not get to testing it thoroughly yet.

What I can see is that the difference in your solution is that you are not changing any backend code to accept requests from the sub-url. Why is that, and how is it still working? When I tested that with traefik, it didn't work.

Can you maybe check out the image for that branch and see if that works for your setup?

theyurii commented 1 year ago

@MauriceNino the problem here is not in the backend but in the frontend, and is very well described by @btxtiger . Shortly, frontend has a lot of hardcoded absolute paths of loading resources instead of relative. I.e., browser always tries to make all requests to root / path instead of current relative path.

Ideally, backend should not care at all where it is hosted, at root path or subdirectory. Backend's purpose is to merely receive a request from a client at any location it listens and generate a response. It can load any needed module relatively to its location on a host. With a frontend, the situation is slightly different. Frontend serves a client with an initial document which later asks browser to load additional resources via separate requests. This is where everything fails.

If you put dashdot behind a reverse proxy and open inspection in any browser, you will quickly find out that the white screen you see is because of browser failing to load resources. This happens because dashdot asks the browser to load them from the root / path instead of a path relative to the current location (e.g, /dashdot/). So the solution is pretty simple: ask the browser to make requests to relative paths.

First, I fixed the <base href="/"> tag. It tells browser to load all links in a document relatively to the root path instead of the actual path. This is fixed by modifying baseHref value in apps/view/project.json. This makes <base> tag to have href attribute set to an empty string which tells browser to load all resources referenced in href and src relatively to the current path instead of root path.

Next, I fixed a hardcoded absolute path to Inter.ttf asset. Instead of loading it from the root path all the time, it's now being loaded relatively to the current path opened in the browser.

Lastly, there is another bug in opening an io socket in apps/view/src/services/page-data.ts. Official documentation says that new sockets always are pointed to an absolute /socket.io/ path no matter where exactly it is actually hosted. So I override this default value by appending an actual current path of an open browser window which makes io.socket communicating with the right endpoint.

After applying these fixes, the browser now makes all requests to dashdot backend correctly using a relative path instead of absolute root / path. Dashdot renders correctly and displays all widgets.

P.S.: imho, these problems should be fixed regardless of the goal. Using absolute paths is not good and makes code less flexible and less maintainable.

MauriceNino commented 1 year ago

@theyurii I think there is a misunderstanding. It's pretty clear why the frontend didn't work (absolute paths), but in my testing (on the other branch I mentioned), the backend also required to be served from the sub-url. Which, in hindsight, makes no sense - but it worked. So now I am wondering why we have two different solutions that both work in some scenario.

I will take your comment into consideration, when I look over this issue again (currently short on time), but for now I can't, in good conscience, push this update to main branch.

[...] Using absolute paths is not good [...]

Unfortunately, it's not so simple, and you will find that most resources actually advise against using a relative base href.

ChristianJacobsen commented 3 months ago

@MauriceNino, any chance of rebasing your feature branch and publishing a new image? I'd love to try it out, but it's 200+ commits behind 'main' and I'm sure I want to also have those changes.