[GH-ISSUE #1091] Consider allowing SITE_ROOT to be used to configure "base path" as well #758

Closed
opened 2026-02-25 23:43:29 +03:00 by kerem · 10 comments
Owner

Originally created by @chrishoage on GitHub (Nov 28, 2024).
Original GitHub issue: https://github.com/healthchecks/healthchecks/issues/1091

I would like to host healthchecks at example.com/healthchecks

I initially thought SITE_ROOT would allow for this, however in reality SITE_ROOT is more like SITE_ORIGIN

Please consider allowing for SITE_ROOT=https://example.com/healthchecks which would configure the routes to use /heathchecks (or whatever the user configured). Alternatively a new config option could be added: BASE_PATH where it can default to / and be configured to the user. In both cases this config would likely need to be communicated to the UI in order to construct links and properly load assets.

If this request is out of scope for this project then please consider clarifying in the documentation that SITE_ROOT can only be used for configuring the origin of the URL, not the full root of the hosted instance as it was not initially clear I couldn't configure the full "root" I wanted using this config option.

Originally created by @chrishoage on GitHub (Nov 28, 2024). Original GitHub issue: https://github.com/healthchecks/healthchecks/issues/1091 I would like to host healthchecks at `example.com/healthchecks` I initially thought `SITE_ROOT` would allow for this, however in reality `SITE_ROOT` is more like `SITE_ORIGIN` Please consider allowing for `SITE_ROOT=https://example.com/healthchecks` which would configure the routes to use `/heathchecks` (or whatever the user configured). Alternatively a new config option could be added: `BASE_PATH` where it can default to `/` and be configured to the user. In both cases this config would likely need to be communicated to the UI in order to construct links and properly load assets. If this request is out of scope for this project then please consider clarifying in the documentation that `SITE_ROOT` can only be used for configuring the origin of the URL, not the full root of the hosted instance as it was not initially clear I couldn't configure the full "root" I wanted using this config option.
kerem closed this issue 2026-02-25 23:43:29 +03:00
Author
Owner

@cuu508 commented on GitHub (Dec 3, 2024):

Thanks for the suggestion @chrishoage! I updated docs to say SITE_ROOT is only used for formatting absolute URLs, and serving on a subpath is not tested and not documented.

<!-- gh-comment-id:2513740363 --> @cuu508 commented on GitHub (Dec 3, 2024): Thanks for the suggestion @chrishoage! I updated docs to say `SITE_ROOT` is only used for formatting absolute URLs, and serving on a subpath is not tested and not documented.
Author
Owner

@chrishoage commented on GitHub (Dec 3, 2024):

Thank you!

I was unaware of SCRIPT_NAME / FORCE_SCRIPT_NAME (I'm just generally unfamiliar with django). I'll give that a shot!

I received some more comments in my email that I don't see here, I presume they were deleted.

I am running healthchecks in a docker container though a caddy proxy. I protect my "admin" services (of which I consider healthchecks one of) though Authelia under a special sub domain, which is why I was trying to see if SITE_ROOT would let me specify a sub path.

Much appreciated for the documentation update, and thank you for having healthchecks be selfhost-able!

<!-- gh-comment-id:2515081777 --> @chrishoage commented on GitHub (Dec 3, 2024): Thank you! I was unaware of `SCRIPT_NAME` / `FORCE_SCRIPT_NAME` (I'm just generally unfamiliar with django). I'll give that a shot! I received some more comments in my email that I don't see here, I presume they were deleted. I am running healthchecks in a docker container though a caddy proxy. I protect my "admin" services (of which I consider healthchecks one of) though Authelia under a special sub domain, which is why I was trying to see if SITE_ROOT would let me specify a sub path. Much appreciated for the documentation update, and thank you for having healthchecks be selfhost-able!
Author
Owner

@chrishoage commented on GitHub (Dec 4, 2024):

To any future google spelunkers who may come across this - it is possible to make this work!

Steps:

  1. In local_settings.py add the following
FORCE_SCRIPT_NAME = "/checks/"
STATIC_URL = "/checks/static/"
SITE_ROOT = "https://example.com"
PING_ENDPOINT = "https://example.com/checks/ping/"
  1. In your proxy it is important to configure the proxy to strip /checks. Example Caddy config:
example.com {
  rewrite /checks /checks/
  route /checks/* {
    uri strip_prefix /checks
    reverse_proxy healthchecks:8000
  }
}

That should be all that is required to make this function.

Notes

I am using REMOTE_USER_HEADER and Authelia - I have no idea if the above methods allow login to work. YMMV

In order for this to be "officially supported" (ideally users wouldn't need to strip the path before feeding it downstream) by a BASE_PATH config in local_settings.py the following would need to happen

  1. Auto configure FORCE_SCRIPT_NAME, STATIC_URL and PING_ENDPOINT based on a BASE_PATH value. This could likely default to / to make life easy
  2. Update hc/urls.py to "re-parent" the routes. path(BASE_PATH, urlpatterns), perhaps, that's just a guess (never worked with Django)

If someone enterprising enough wishes to open a PR it sounds like this project may accept it. However since I was able to make this function enough for my own use, I'm happy with the solution I landed at above.

Huge thanks to @cuu508 for the nudges that led me to be able to google what I needed to know to make the above work.

<!-- gh-comment-id:2516087227 --> @chrishoage commented on GitHub (Dec 4, 2024): To any future google spelunkers who may come across this - it is possible to make this work! ## Steps: 1. In local_settings.py add the following ```python FORCE_SCRIPT_NAME = "/checks/" STATIC_URL = "/checks/static/" SITE_ROOT = "https://example.com" PING_ENDPOINT = "https://example.com/checks/ping/" ``` 2. In your proxy it is important to configure the proxy to strip /checks. Example Caddy config: ```Caddyfile example.com { rewrite /checks /checks/ route /checks/* { uri strip_prefix /checks reverse_proxy healthchecks:8000 } } ``` That should be all that is required to make this _function_. ## Notes I am using REMOTE_USER_HEADER and Authelia - I have no idea if the above methods allow login to work. YMMV In order for this to be "officially supported" (ideally users wouldn't need to strip the path before feeding it downstream) by a `BASE_PATH` config in `local_settings.py` the following would need to happen 1. Auto configure `FORCE_SCRIPT_NAME`, `STATIC_URL` and `PING_ENDPOINT` based on a `BASE_PATH` value. This could likely default to `/` to make life easy 2. Update [`hc/urls.py` ](https://github.com/healthchecks/healthchecks/blob/master/hc/urls.py) to "re-parent" the routes. `path(BASE_PATH, urlpatterns),` perhaps, that's just a guess (never worked with Django) If someone enterprising enough wishes to open a PR it sounds like this project may accept it. However since I was able to make this function enough for my own use, I'm happy with the solution I landed at above. Huge thanks to @cuu508 for the nudges that led me to be able to google what I needed to know to make the above work.
Author
Owner

@cuu508 commented on GitHub (Dec 4, 2024):

I received some more comments in my email that I don't see here, I presume they were deleted.

Yes, I was (and still am) going forth and back between whether I want to spend effort on this or not.

<!-- gh-comment-id:2516366892 --> @cuu508 commented on GitHub (Dec 4, 2024): > I received some more comments in my email that I don't see here, I presume they were deleted. Yes, I was (and still am) going forth and back between whether I want to spend effort on this or not.
Author
Owner

@cuu508 commented on GitHub (Dec 4, 2024):

@chrishoage I pushed experimental changes that should allow Healthchecks to run on a subpath just by setting SITE_ROOT (as environment variable, setting it in local_settings.py will not work!).

I also published a docker image, tagged "master" (temporary, for testing only). I tested the image like so:

docker run \
  --name=healthchecks \
  -p 8000:8000 \
  --restart unless-stopped \
  -e ALLOWED_HOSTS=localhost \
  -e DB=sqlite \
  -e DB_NAME=/data/hc.sqlite \
  -e DEBUG=False \
  -e DEFAULT_FROM_EMAIL=fixme-email-address-here \
  -e EMAIL_HOST=fixme-smtp-host-here \
  -e EMAIL_HOST_PASSWORD=fixme-smtp-password-here \
  -e EMAIL_HOST_USER=fixme-smtp-username-here \
  -e EMAIL_PORT=587 \
  -e EMAIL_USE_TLS=True \
  -e SECRET_KEY=--- \
  -e SITE_ROOT=http://localhost/healthchecks \
  -v healthchecks-data:/data \
healthchecks/healthchecks:master

uwsgi inside the container runs on port 8000 (this is not affected by SITE_ROOT), and the above command exposes port 8000. So with this running, I visited http://localhost:8000/healthchecks/ and the login page loaded up.

Outside docker, I have nginx running, with the following minimal nginx.conf:

user www-data;
worker_processes auto;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log;

events {
        worker_connections 768;
}

http {
        include /etc/nginx/mime.types;
        default_type application/octet-stream;
        access_log /var/log/nginx/access.log;

        server {
            listen 80;

            location /healthchecks/ {               
                include proxy_params;
                proxy_pass http://localhost:8000;
            }
        }
}

I visited http://localhost/healthchecks/ and the login page loaded up via nginx also.

If you get a chance, perhaps you could test this image as well?

<!-- gh-comment-id:2517237025 --> @cuu508 commented on GitHub (Dec 4, 2024): @chrishoage I pushed experimental changes that should allow Healthchecks to run on a subpath just by setting `SITE_ROOT` (as environment variable, setting it in local_settings.py will not work!). I also published a docker image, tagged "master" (temporary, for testing only). I tested the image like so: ``` docker run \ --name=healthchecks \ -p 8000:8000 \ --restart unless-stopped \ -e ALLOWED_HOSTS=localhost \ -e DB=sqlite \ -e DB_NAME=/data/hc.sqlite \ -e DEBUG=False \ -e DEFAULT_FROM_EMAIL=fixme-email-address-here \ -e EMAIL_HOST=fixme-smtp-host-here \ -e EMAIL_HOST_PASSWORD=fixme-smtp-password-here \ -e EMAIL_HOST_USER=fixme-smtp-username-here \ -e EMAIL_PORT=587 \ -e EMAIL_USE_TLS=True \ -e SECRET_KEY=--- \ -e SITE_ROOT=http://localhost/healthchecks \ -v healthchecks-data:/data \ healthchecks/healthchecks:master ``` uwsgi inside the container runs on port 8000 (this is not affected by `SITE_ROOT`), and the above command exposes port 8000. So with this running, I visited http://localhost:8000/healthchecks/ and the login page loaded up. Outside docker, I have nginx running, with the following minimal nginx.conf: ``` user www-data; worker_processes auto; pid /run/nginx.pid; error_log /var/log/nginx/error.log; events { worker_connections 768; } http { include /etc/nginx/mime.types; default_type application/octet-stream; access_log /var/log/nginx/access.log; server { listen 80; location /healthchecks/ { include proxy_params; proxy_pass http://localhost:8000; } } } ``` I visited http://localhost/healthchecks/ and the login page loaded up via nginx also. If you get a chance, perhaps you could test this image as well?
Author
Owner

@chrishoage commented on GitHub (Dec 5, 2024):

@cuu508 following your example (adapted for my Caddy and Docker Compose setup) I am able to get a sub path to work by simply supplying SITE_ROOT!

For you to see, here is what I have

  healthchecks:
    container_name: healthchecks
    hostname: healthchecks
    image: healthchecks/healthchecks:master
    restart: ${RESTART-unless-stopped}
    volumes: # I left off a /data volume since for me this was an ephemeral test 
      - /etc/localtime:/etc/localtime:ro
    environment:
      - DB=sqlite
      - DB_NAME=/data/hc.sqlite
      - DEBUG=False
      - SITE_ROOT=https://example.com/checks
      - ALLOWED_HOSTS=healthchecks,example.com
      - EMAIL_HOST=smtp.sendgrid.net
      - EMAIL_HOST_USER=apikey
      - EMAIL_HOST_PASSWORD=${SG_MAIL_KEY:?required}
      - DEFAULT_FROM_EMAIL=noreply@example.com
      - REMOTE_USER_HEADER=HTTP_REMOTE_EMAIL
      - SECRET_KEY=askldfjaslkdfjasdlkfjasdklasdlkfjlska # example key 
# simplified for this demonstration  
example.com {
  rewrite /checks /checks/
  reverse_proxy /checks/ healthchecks:8000
}

In my testing, this works great! (honestly the workaround you pointed me to above was also functioning, but I'll never say no to first class support for the feature!)

Please let me know if I can help any further!

<!-- gh-comment-id:2518991756 --> @chrishoage commented on GitHub (Dec 5, 2024): @cuu508 following your example (adapted for my Caddy and Docker Compose setup) I am able to get a sub path to work by simply supplying `SITE_ROOT`! For you to see, here is what I have ```yaml healthchecks: container_name: healthchecks hostname: healthchecks image: healthchecks/healthchecks:master restart: ${RESTART-unless-stopped} volumes: # I left off a /data volume since for me this was an ephemeral test - /etc/localtime:/etc/localtime:ro environment: - DB=sqlite - DB_NAME=/data/hc.sqlite - DEBUG=False - SITE_ROOT=https://example.com/checks - ALLOWED_HOSTS=healthchecks,example.com - EMAIL_HOST=smtp.sendgrid.net - EMAIL_HOST_USER=apikey - EMAIL_HOST_PASSWORD=${SG_MAIL_KEY:?required} - DEFAULT_FROM_EMAIL=noreply@example.com - REMOTE_USER_HEADER=HTTP_REMOTE_EMAIL - SECRET_KEY=askldfjaslkdfjasdlkfjasdklasdlkfjlska # example key ``` ```Caddyfile # simplified for this demonstration example.com { rewrite /checks /checks/ reverse_proxy /checks/ healthchecks:8000 } ``` In my testing, this works great! (honestly the workaround you pointed me to above was also functioning, but I'll never say no to first class support for the feature!) Please let me know if I can help any further!
Author
Owner

@cuu508 commented on GitHub (Dec 9, 2024):

I've published v3.8. It has the support for path in SITE_ROOT, and has a few additional fixes related to serving Healthchecks on a path:

  • The URLs in email notifications were missing the path
  • Redirect to login page was missing the path and so redirecting the user to a 404 page (originally reported in #382)
  • The redirect after login was not working correctly too

I've now also removed the temporary image tagged as "master" from Docker Hub.

<!-- gh-comment-id:2529029935 --> @cuu508 commented on GitHub (Dec 9, 2024): I've published v3.8. It has the support for path in SITE_ROOT, and has a few additional fixes related to serving Healthchecks on a path: * The URLs in email notifications were missing the path * Redirect to login page was missing the path and so redirecting the user to a 404 page (originally reported in #382) * The redirect *after* login was not working correctly too I've now also removed the temporary image tagged as "master" from Docker Hub.
Author
Owner

@chrishoage commented on GitHub (Dec 16, 2024):

I went to update today to pull in these changes but I am no longer able to make it work.

I end up with the application thinking I'm loading http://healthchecks:8000/checks/checks when curling http://healthchecks:8000/checks

example (I am using the linuxserver docker image, but when I started having problems I switched back to the one distributed from this repo, and I have the same behavior):

❯ sudo docker exec -it healthchecks /bin/bash
root@healthchecks:/# curl http://healthchecks:8000/checks
<!DOCTYPE html>
<html lang="en">
<head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <title>Page not found at /checks</title>
  <meta name="robots" content="NONE,NOARCHIVE">
  <style>
    html * { padding:0; margin:0; }
    body * { padding:10px 20px; }
    body * * { padding:0; }
    body { font-family: sans-serif; background:#eee; color:#000; }
    body > :where(header, main, footer) { border-bottom:1px solid #ddd; }
    h1 { font-weight:normal; margin-bottom:.4em; }
    h1 small { font-size:60%; color:#666; font-weight:normal; }
    table { border:none; border-collapse: collapse; width:100%; }
    td, th { vertical-align:top; padding:2px 3px; }
    th { width:12em; text-align:right; color:#666; padding-right:.5em; }
    #info { background:#f6f6f6; }
    #info ol { margin: 0.5em 4em; }
    #info ol li { font-family: monospace; }
    #summary { background: #ffc; }
    #explanation { background:#eee; border-bottom: 0px none; }
    pre.exception_value { font-family: sans-serif; color: #575757; font-size: 1.5em; margin: 10px 0 10px 0; }
  </style>
</head>
<body>
  <header id="summary">
    <h1>Page not found <small>(404)</small></h1>

    <table class="meta">
      <tr>
        <th scope="row">Request Method:</th>
        <td>GET</td>
      </tr>
      <tr>
        <th scope="row">Request URL:</th>
        <td>http://healthchecks:8000/checks/checks</td>
      </tr>

Strangely however, the actual debug logs show

[pid: 159|app: 0|req: 6/6] 172.18.0.28 () {60 vars in 1084 bytes} [Mon Dec 16 02:35:11 2024] GET /checks/ => generated 10887 bytes in 11 msecs (HTTP/1.1 404) 7 headers in 230 bytes (1 switches on core 0)

For now I've gone back to the solution outlined in https://github.com/healthchecks/healthchecks/issues/1091#issuecomment-2516087227 which is working for me. However I think the last round of changes after I left my message may have broken something, as I am no longer able to make it work with the config outlined in https://github.com/healthchecks/healthchecks/issues/1091#issuecomment-2518991756

<!-- gh-comment-id:2544423344 --> @chrishoage commented on GitHub (Dec 16, 2024): I went to update today to pull in these changes but I am no longer able to make it work. I end up with the application thinking I'm loading `http://healthchecks:8000/checks/checks` when curling `http://healthchecks:8000/checks` example (I am using the linuxserver docker image, but when I started having problems I switched back to the one distributed from this repo, and I have the same behavior): ``` ❯ sudo docker exec -it healthchecks /bin/bash root@healthchecks:/# curl http://healthchecks:8000/checks <!DOCTYPE html> <html lang="en"> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8"> <title>Page not found at /checks</title> <meta name="robots" content="NONE,NOARCHIVE"> <style> html * { padding:0; margin:0; } body * { padding:10px 20px; } body * * { padding:0; } body { font-family: sans-serif; background:#eee; color:#000; } body > :where(header, main, footer) { border-bottom:1px solid #ddd; } h1 { font-weight:normal; margin-bottom:.4em; } h1 small { font-size:60%; color:#666; font-weight:normal; } table { border:none; border-collapse: collapse; width:100%; } td, th { vertical-align:top; padding:2px 3px; } th { width:12em; text-align:right; color:#666; padding-right:.5em; } #info { background:#f6f6f6; } #info ol { margin: 0.5em 4em; } #info ol li { font-family: monospace; } #summary { background: #ffc; } #explanation { background:#eee; border-bottom: 0px none; } pre.exception_value { font-family: sans-serif; color: #575757; font-size: 1.5em; margin: 10px 0 10px 0; } </style> </head> <body> <header id="summary"> <h1>Page not found <small>(404)</small></h1> <table class="meta"> <tr> <th scope="row">Request Method:</th> <td>GET</td> </tr> <tr> <th scope="row">Request URL:</th> <td>http://healthchecks:8000/checks/checks</td> </tr> ``` Strangely however, the actual debug logs show ``` [pid: 159|app: 0|req: 6/6] 172.18.0.28 () {60 vars in 1084 bytes} [Mon Dec 16 02:35:11 2024] GET /checks/ => generated 10887 bytes in 11 msecs (HTTP/1.1 404) 7 headers in 230 bytes (1 switches on core 0) ``` For now I've gone back to the solution outlined in https://github.com/healthchecks/healthchecks/issues/1091#issuecomment-2516087227 which is working for me. However I think the last round of changes after I left my message may have broken something, as I am no longer able to make it work with the config outlined in https://github.com/healthchecks/healthchecks/issues/1091#issuecomment-2518991756
Author
Owner

@cuu508 commented on GitHub (Dec 16, 2024):

@chrishoage did you pass SITE_ROOT as an environment variable to the container?

Specifying it in local_settings.py does not work, because uWSGI also needs to access it.

Here's how I intended it to work:

  • client requests http://healthchecks:8000/checks
  • uWSGI sets SCRIPT_NAME to "/checks"
  • uWSGI also sees PATH_INFO starts with "/checks" and chops it off
  • python code sees PATH_INFO="", SCRIPT_NAME="/checks"

The linuxserver image doesn't have the SCRIPT_NAME and PATH_INFO patching bits in their uwsgi.ini so I would not expect it to work.

<!-- gh-comment-id:2544797634 --> @cuu508 commented on GitHub (Dec 16, 2024): @chrishoage did you pass `SITE_ROOT` as an environment variable to the container? Specifying it in local_settings.py does not work, because [uWSGI also needs to access it](https://github.com/healthchecks/healthchecks/blob/14bcc84a781c91584e17ff9d1944465169fcee9a/docker/uwsgi.ini#L39). Here's how I intended it to work: * client requests http://healthchecks:8000/checks * uWSGI sets SCRIPT_NAME to "/checks" * uWSGI also sees PATH_INFO starts with "/checks" and chops it off * python code sees PATH_INFO="", SCRIPT_NAME="/checks" The linuxserver image doesn't have the SCRIPT_NAME and PATH_INFO patching bits in their uwsgi.ini so I would not expect it to work.
Author
Owner

@chrishoage commented on GitHub (Dec 16, 2024):

I tried to reproduce the behavior I was seeing on a different host and it was indeed working.

I think my issue was I had an outdated image on my production host. While at the time I thought I had pulled I obviously didn't, so must have been running an outdated image of healthchecks/healthchecks:latest

After pulling healthchecks/healthchecks:latest I am no longer able to reproduce. I will use this image from now on - thank you!

I apologize for the trouble!

<!-- gh-comment-id:2546181576 --> @chrishoage commented on GitHub (Dec 16, 2024): I tried to reproduce the behavior I was seeing on a different host and it was indeed working. I think my issue was I had an outdated image on my production host. While at the time I thought I had pulled I obviously didn't, so must have been running an outdated image of `healthchecks/healthchecks:latest` After pulling `healthchecks/healthchecks:latest` I am no longer able to reproduce. I will use this image from now on - thank you! I apologize for the trouble!
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/healthchecks#758
No description provided.