Repository: validmind/documentation
Pull request #674: Add URL configuration to Docker images
Author: login: nrichers
Labels: name: infrastructure
description: Docs infra changes
Created: 2025-03-17T21:46:45Z | Merged: 2025-03-22T18:06:49Z

## Internal Notes for Reviewers

This PR lets you configure the Docker image for our static docs site via Kubernetes manifest or a config file. 

Two parameters can be configured, documented in [URL configuration for Docker](https://github.com/validmind/documentation/blob/nrichers/sc-8752/investigate-passing-in-environment-variables/README.md#url-configuration-for-docker):

- **VALIDMIND_URL** — where to access our platform
- **JUPYTERHUB_URL** — where to access JupyterHub

Configure via Kube manifest: **<<< SPENCER, VERIFY THIS WORKS BEYOND MY BASIC TESTING?** 🐹 

```
   apiVersion: apps/v1
   kind: Deployment
   metadata:
     name: validmind-docs
   spec:
     template:
       spec:
         containers:
         - name: validmind-docs
           env:
           - name: VALIDMIND_URL
             value: "https://your-custom-app.validmind.ai"
           - name: JUPYTERHUB_URL
             value: "https://your-custom-jupyter.validmind.ai"
```

Configure via `config.json`,  generated with the Docker image:

```
  {
     "VALIDMIND_URL": "https://your-custom-app.validmind.ai",
     "JUPYTERHUB_URL": "https://your-custom-jupyter.validmind.ai"
   }
```

## How it works

A new `_quarto-docker.yml` profile includes pre-render and post-render steps to generate a documentation site with configurable URL placeholders. When Docker starts the container image built from this docs site, the placeholder URLs are then replaced with the URLs you specify (or safe fallback URLs), making the site configurable. 

### Pre-render

We replace the relevant URLs in `_variables.yml` with unique placeholders that contain `configurable-url` and generate a `config.json` with the default URLs:

```sh
Found URLs in _variables.yml:
VALIDMIND_URL: https://app.prod.validmind.ai
JUPYTERHUB_URL: https://jupyterhub.validmind.ai
Successfully generated config.json from _variables.yml
Modified _variables.yml with placeholder values:
VALIDMIND_URL: https://app.prod.validmind-configurable-url.ai
JUPYTERHUB_URL: https://jupyterhub.validmind-configurable-url.ai
```

### Post-render

We restore _variables.yml. 

## Docker startup

When you start Docker after building the image, an entrypoint script configures the site using:

1. URL values specified via direct environment variables, e.g. a K8s manifest
2. URL values specified in `config.json`
3. As a fallback, hardcoded public URL defaults 

```sh
Starting the Docker container on http://localhost:4444/ ...
...
==== Start ValidMind URL configuration ====
Initializing ValidMind documentation site...
Looking for values in config file: /usr/share/nginx/html/config.json
Using VALIDMIND_URL from config: https://app.prod.validmind.ai
Using JUPYTERHUB_URL from config: https://jupyterhub.validmind.ai
Found placeholders:
14 instances of https://app.prod.validmind-configurable-url.ai
59 instances of https://jupyterhub.validmind-configurable-url.ai
Replacing URL placeholders in HTML files...
Replacing URL placeholders in search.json...
After replacement:
0 instances of https://app.prod.validmind-configurable-url.ai
0 instances of https://jupyterhub.validmind-configurable-url.ai
✓ URL replacement completed successfully
==== URL configuration complete ====
...
```

## How to test

```sh
gh pr checkout 674
cd site
make docker-build
```

Output in `site/_site` should now show placeholder URLs that contain `configurable-url`. For example:

```html
site/_site/developer/get-started-validmind-library.html:
  2267  </div>
  2268: <p>After you <a href="https://app.prod.validmind-configurable-url.ai"><strong>sign up</strong></a> for ValidMind to get access, try one of our getting started guides:</p>
  2269  <div id="listing-developer-getting-started" class="quarto-listing quarto-listing-container-grid">

site/_site/developer/samples-jupyter-notebooks.html:
  2070  <div class="w-30-ns mt2 pb3">
  2071: <p><a href="https://jupyterhub.validmind-configurable-url.ai/" class="button button-green" target="_blank"><i class="fa-solid fa-code" aria-label="code"></i> Try Notebooks on JupyterHub</a></p>
  2072  </div>
```

Next, start the Docker image: 

```sh
make docker-serve
```

Opening up the site on http://localhost:4444/ in your browser should show the actual, correct URLs specified in `config.json`. 

Inspect http://localhost:4444/developer/get-started-validmind-library.html:

```html
<a href="https://app.prod.validmind.ai/" data-original-href="https://app.prod.validmind.ai/" target="_blank" rel="noopener" class="external"><strong>sign up</strong></a>
```

Inspect http://localhost:4444/developer/samples-jupyter-notebooks.html:

```html
<p><a href="https://jupyterhub.validmind.ai/" class="button button-green external" target="_blank" data-original-href="https://jupyterhub.validmind.ai/" rel="noopener"><i class="fa-solid fa-code" aria-label="code"></i> Try Notebooks on JupyterHub</a></p>
```

<!--
PR instructions for release notes:

1. Pick at least one label:

- `internal` (skip Step 2, no release notes required)
- `highlight`
- `enhancement`
- `breaking-change`
- `deprecation`
- `bug`
- `documentation`

4. In the next section, describe the changes so that an external user can understand them. Keep it simple and link to the docs with [Learn more ...](<relative-link>), if available.
-->

## External Release Notes

You can  now configure the Docker image for our static docs site via Kubernetes manifest or a config file. Two parameters can be configured:

- **VALIDMIND_URL** — Where to access the ValidMind Platform
- **JUPYTERHUB_URL** — Where to access JupyterHub

This feature lets you configure the site’s Docker image to match your specific requirements, simplifying deployment in your own infrastructure.

You can set these URLs using one of the following supported methods: [URL configuration for Docker](https://github.com/validmind/documentation/blob/main/README.md#url-configuration-for-docker)


Diff:
diff --git a/.gitignore b/.gitignore
index ce0f93aab7..100fe0ad92 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,4 +16,7 @@ site/_freeze/
 site/notebooks/tutorials/my_tests/
 
 # release-scripts run cache
-release-scripts/__pycache__
\ No newline at end of file
+release-scripts/__pycache__
+
+# Generated file for Docker profile
+site/config.json
diff --git a/Dockerfile b/Dockerfile
index 6804fd5c85..556db72f93 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,10 +3,20 @@
 # Use the official Nginx image
 FROM nginx:alpine
 
+# Install jq for better JSON handling
+RUN apk add --no-cache jq
+
 # Copy the static site content to the Nginx HTML directory
 COPY site/_site /usr/share/nginx/html
 
+# Copy config.json for fallback URL values
+COPY site/config.json /usr/share/nginx/html/config.json
+
+# Copy the URL replacement script
+COPY site/scripts/docker_entrypoint.sh /docker-entrypoint.d/40-replace-url-placeholders.sh
+
+# Make the URL replacement script executable
+RUN chmod +x /docker-entrypoint.d/40-replace-url-placeholders.sh
+
 # Expose port 4444
 EXPOSE 4444
-
-# Nginx starts automatically as the default CMD
diff --git a/README.md b/README.md
index 54c4275e89..c840837606 100644
--- a/README.md
+++ b/README.md
@@ -174,3 +174,56 @@ make docker-serve
 ```
 
 Access the site locally: http://localhost:4444  
+
+### URL configuration for Docker
+
+If you need to change where our docs site links to ValidMind or JupyterHub, this section explains how.
+
+Two parameters can be configured:
+
+- `VALIDMIND_URL` — where to access the platform, defaults to our public ValidMind app
+- `JUPYTERHUB_URL` — where to access JupyterHub, defaults to our public instance
+
+#### How to configure
+
+Pass environment variables through a Kubernetes manifest, use a config file, or use public defaults if none are specified.
+
+Configure in a [Kubernetes manifest](validmind-docs.yaml):
+
+```yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: validmind-docs
+  labels:
+    app: validmind-docs
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: validmind-docs
+  template:
+    metadata:
+      labels:
+        app: validmind-docs
+    spec:
+      containers:
+      - name: validmind-docs
+        image: validmind/docs:latest
+        ports:
+        - containerPort: 80
+        env:
+        - name: VALIDMIND_URL
+          value: "https://your-custom-app.validmind.ai"
+        - name: JUPYTERHUB_URL
+          value: "https://your-custom-jupyter.validmind.ai"
+```
+
+Configure in `config.json`, generated with the Docker image:
+
+```json
+{
+   "VALIDMIND_URL": "https://your-custom-app.validmind.ai",
+   "JUPYTERHUB_URL": "https://your-custom-jupyter.validmind.ai"
+ }
+```
diff --git a/site/Dockerfile b/site/Dockerfile
new file mode 100644
index 0000000000..c7458d040e
--- /dev/null
+++ b/site/Dockerfile
@@ -0,0 +1,25 @@
+# Dockerfile to build the validmind-docs image
+
+# Use the official Nginx image
+FROM nginx:stable-alpine
+
+# Install jq for JSON parsing
+RUN apk add --no-cache jq bash
+
+# Copy the built static site
+COPY _site /usr/share/nginx/html
+
+# Copy config.json for fallback values
+COPY config.json /usr/share/nginx/html/
+
+# Copy the entrypoint script
+COPY scripts/docker_entrypoint.sh /
+
+# Make the entrypoint script executable
+RUN chmod +x /docker_entrypoint.sh
+
+# Use our custom entrypoint
+ENTRYPOINT ["/docker_entrypoint.sh"]
+
+# Default command
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/site/Makefile b/site/Makefile
index 47b65d401a..ed0510997a 100644
--- a/site/Makefile
+++ b/site/Makefile
@@ -9,7 +9,7 @@ PROFILE := exe-demo
 FILE_PATH := notebooks/tutorials/intro_for_model_developers_EXECUTED.ipynb 
 
 # Define .PHONY target for help section
-.PHONY: help clean clone notebooks python-docs docs-site deploy-demo-branch delete-demo-branch deploy-prod deploy-staging release-notes execute docker-build docker-serve
+.PHONY: help clean clone notebooks python-docs docs-site deploy-demo-branch delete-demo-branch deploy-prod deploy-staging release-notes execute docker-site docker-site-lite docker-build docker-serve 
 
 # Help section
 help:
@@ -25,12 +25,13 @@ help:
 	@echo "  delete-demo-branch   Delete docs demo site in s3://docs-ci-cd-demo/site/pr_previews/$(GIT_BRANCH)/"
 	@echo "  deploy-prod          Deploy docs prod site to s3://docs-ci-cd-prod/site/"
 	@echo "  deploy-staging       Deploy docs staging site to s3://docs-ci-cd-staging/site/"
+	@echo "  docker-site          Get source, render site with Docker profile, execute notebooks"
+	@echo "  docker-site-lite     Get source and render site with Docker profile (skips notebook execution)"
+	@echo "  docker-build         Builds a Docker image of the docs site"
+	@echo "  docker-serve         Serves the docs site at http://localhost:4444/"
 	@echo "  help                 Display this help message (default target)"
 	@echo "  release-notes        Generate release notes from pull requests since latest tag and update _quarto.yml"
 	@echo "  yearly-releases      Collate and move a specified year of releases into their own subdir and update _quarto.yml"
-	@echo "  execute              Execute a Jupyter Notebook (must have valid .env credentials set up in root)"
-	@echo "  docker-build         Builds a Docker image of the docs site"
-	@echo "  docker-serve         Serves the docs site at http://localhost:4444/"
 
 # Clean up source directory
 clean:
@@ -88,11 +89,6 @@ docs-site: get-source
 	quarto render --profile production
 	@$(MAKE) execute PROFILE=exe-prod
 
-# Get all source files
-docs-site-lite: get-source
-	@echo "\nRendering the static HTML site ..."
-	quarto render --profile production
-
 # Deploy PR branch to https://docs-demo.vm.validmind.ai/
 deploy-demo-branch:
 	@quarto render --profile development && aws s3 sync ./_site s3://docs-ci-cd-demo/site/pr_previews/$(GIT_BRANCH)/ --delete && aws cloudfront create-invalidation --distribution-id E38AINJY5CYN6P --paths "/*" --no-cli-pager > /dev/null; 
@@ -146,7 +142,16 @@ yearly-releases:
 execute:
 	quarto render --profile $(PROFILE) $(FILE_PATH)
 
-docker-build: docs-site-lite
+docker-site: get-source
+	@echo "\nRendering the configurable static HTML site for Docker ..."
+	quarto render --profile docker 2>&1 | grep -v "WARN:" 1>&2
+	@$(MAKE) execute PROFILE=exe-prod
+
+docker-site-lite: get-source
+	@echo "\nRendering the configurable static HTML site for Docker (skip notebook execution) ..."
+	quarto render --profile docker 2>&1 | grep -v "WARN:" 1>&2
+
+docker-build: docker-site-lite
 	@echo "\nBuilding the Docker image ..."
 	@docker build -f ../Dockerfile -t validmind-docs ..
 
diff --git a/site/_quarto-docker.yml b/site/_quarto-docker.yml
new file mode 100644
index 0000000000..e875219e71
--- /dev/null
+++ b/site/_quarto-docker.yml
@@ -0,0 +1,19 @@
+# Quarto docker profile configuration
+# This profile uses placeholders for dynamic URL configuration
+
+profile:
+  default: docker
+
+# Project settings with pre-render and post-render steps for Docker
+project:
+  type: website
+  pre-render: scripts/modify_urls.sh
+  post-render: bash -c "git restore _variables.yml >/dev/null 2>&1 && echo 'Restored _variables.yml'"
+
+# HTML format configuration
+format:
+  html:
+    # Include scripts and other resources in the header
+    include-in-header: 
+      - environments/heap-production.html
+      - zendesk-chat.html
diff --git a/site/_variables.yml b/site/_variables.yml
index efbaf97524..39e952e001 100644
--- a/site/_variables.yml
+++ b/site/_variables.yml
@@ -6,7 +6,8 @@
 # - Try to avoid abbreviations (e.g. `customer.address` NOT `cust.addr`) — Exceptions can be made for product/branding or standard conceptual phrases (e.g. `vm` for ValidMind, `aws` for Amazon Web Services)
 # - Include enough contextual information (e.g. `invoice.total` NOT `total`)
 
-
+# Variables for static site configuration URLs — DO NOT REMOVE 
+# These URLs are used when starting the Docker container or when rendering the docs site for deployment to AWS S3
 
 # PRODUCT NAMES
 # Names of ValidMind products or core features
@@ -45,8 +46,9 @@ link:
 # Rendered format: `{{< var url.us1 >}}`
 
 url:
-  us1: "https://app.prod.validmind.ai"
+  us1: "https://app.prod.validmind.ai"          # Docker: VALIDMIND_URL
   ca1: "https://app.ca1.validmind.ai"
+  jupyterhub: "https://jupyterhub.validmind.ai" # Docker: JUPYTERHUB_URL
 
 # Amazon Web Services Privatelink specific URLs
 # Rendered format: `{{< var aws.privatelink >}}`
diff --git a/site/developer/samples-jupyter-notebooks.qmd b/site/developer/samples-jupyter-notebooks.qmd
index 9b12bd60a7..0eb3f4bb81 100644
--- a/site/developer/samples-jupyter-notebooks.qmd
+++ b/site/developer/samples-jupyter-notebooks.qmd
@@ -21,7 +21,7 @@ Our Jupyter Notebook code samples showcase the capabilities and features of the
 
 ::: {.w-30-ns .mt2 .pb3}
 
-[{{< fa code >}} Try Notebooks on JupyterHub](https://jupyterhub.validmind.ai/){.button .button-green target="_blank"}
+[{{< fa code >}} Try Notebooks on JupyterHub]({{< var url.jupyterhub >}}/){.button .button-green target="_blank"}
 
 :::
 
@@ -52,7 +52,7 @@ JupyterHub
 : A a web-based platform when you can interact with Jupyter Notebook instances on a shared server. It is commonly used as a collaborative and interactive computing environment for data analysis, scientific research, and programming.
 
 ::: {.tc}
-[{{< fa code >}} Quickstart on JupyterHub](https://jupyterhub.validmind.ai/hub/user-redirect/lab/tree/quickstart_customer_churn_full_suite.ipynb){.button target="_blank"}
+[{{< fa code >}} Quickstart on JupyterHub]({{< var url.jupyterhub >}}/hub/user-redirect/lab/tree/quickstart_customer_churn_full_suite.ipynb){.button target="_blank"}
 
 :::
 
diff --git a/site/get-started/developer/try-with-jupyterhub.qmd b/site/get-started/developer/try-with-jupyterhub.qmd
index e869e577f7..7ffcb4729d 100644
--- a/site/get-started/developer/try-with-jupyterhub.qmd
+++ b/site/get-started/developer/try-with-jupyterhub.qmd
@@ -22,7 +22,7 @@ Signing up is FREE — {{< var link.register >}}
 
 ## Steps
 
-1. In a web browser, open [Quickstart for model documentation](https://jupyterhub.validmind.ai/hub/user-redirect/lab/tree/quickstart_customer_churn_full_suite.ipynb).
+1. In a web browser, open [Quickstart for model documentation]({{< var url.jupyterhub >}}/hub/user-redirect/lab/tree/quickstart_customer_churn_full_suite.ipynb).
 
    This link takes you to {{< var vm.product >}}'s JupyterHub instance where you can log in with the Auth0 credentials for your {{< var vm.product >}} account to access the **QuickStart for model documentation** notebook:
 
diff --git a/site/releases/2024/2024-may-22/release-notes.qmd b/site/releases/2024/2024-may-22/release-notes.qmd
index 01221a69ec..7fd60c20ce 100644
--- a/site/releases/2024/2024-may-22/release-notes.qmd
+++ b/site/releases/2024/2024-may-22/release-notes.qmd
@@ -42,7 +42,7 @@ You can use this notebook to learn how the end-to-end documentation process work
 
 ::: {.w-40-ns}
 
-[Try notebook on JupyterHub](https://jupyterhub.validmind.ai/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb){.button .button-green}
+[Try notebook on JupyterHub]({{< var url.jupyterhub >}}/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb){.button .button-green}
 
 :::
 
@@ -141,7 +141,7 @@ We introduced a new `metric` decorator that turns any function into a {{< var vm
 
 ::: {.w-30-ns}
 
-[Try notebook on JupyterHub](https://jupyterhub.validmind.ai/hub/user-redirect/lab/tree/code_samples/custom_tests/implement_custom_tests.ipynb){.button .button-green}
+[Try notebook on JupyterHub]({{< var url.jupyterhub >}}/hub/user-redirect/lab/tree/code_samples/custom_tests/implement_custom_tests.ipynb){.button .button-green}
 
 :::
 
@@ -573,7 +573,7 @@ Labels: enhancement
 
 ::: {.w-35-ns}
 
-[Try notebook on JupyterHub](https://jupyterhub.validmind.ai/hub/user-redirect/lab/tree/code_samples/custom_tests/integrate_external_test_providers.ipynb){.button}
+[Try notebook on JupyterHub]({{< var url.jupyterhub >}}/hub/user-redirect/lab/tree/code_samples/custom_tests/integrate_external_test_providers.ipynb){.button}
 
 :::
 
@@ -936,7 +936,7 @@ Many of our Jupyter Notebooks have received improvements to make them easier to
 :::
 
 ::: {.w-30-ns}
-[Try {{< var vm.product >}} on JupyterHub](https://jupyterhub.validmind.ai/){.button}
+[Try {{< var vm.product >}} on JupyterHub]({{< var url.jupyterhub >}}/){.button}
 
 :::
 
diff --git a/site/releases/2024/2024-oct-22/release-notes.qmd b/site/releases/2024/2024-oct-22/release-notes.qmd
index 2262c639ee..df760f4c26 100644
--- a/site/releases/2024/2024-oct-22/release-notes.qmd
+++ b/site/releases/2024/2024-oct-22/release-notes.qmd
@@ -68,7 +68,7 @@ This notebook provides a hands-on example of setting up and conducting monitorin
 
 ::: {.w-30-ns}
 
-[Try notebook on JupyterHub](https://jupyterhub.validmind.ai/hub/user-redirect/lab/tree/code_samples/ongoing_monitoring/quickstart_customer_churn_ongoing_monitoring.ipynb){.button .button-green}
+[Try notebook on JupyterHub]({{< var url.jupyterhub >}}/hub/user-redirect/lab/tree/code_samples/ongoing_monitoring/quickstart_customer_churn_ongoing_monitoring.ipynb){.button .button-green}
 
 :::
 
@@ -101,7 +101,7 @@ A new Jupyter Notebook guides you through building a credit risk model with inte
 :::
 
 ::: {.w-40-ns .tc}
-[Try notebook on JupyterHub](https://jupyterhub.validmind.ai/hub/user-redirect/lab/tree/code_samples/credit_risk/application_scorecard_with_bias.ipynb){.button .button-green}
+[Try notebook on JupyterHub]({{< var url.jupyterhub >}}/hub/user-redirect/lab/tree/code_samples/credit_risk/application_scorecard_with_bias.ipynb){.button .button-green}
 :::
 
 ::::
@@ -147,7 +147,7 @@ A new Jupyter Notebook demonstrates how to selectively include columns in your a
 :::
 
 ::: {.w-40-ns .tc}
-[Try notebook on JupyterHub](https://jupyterhub.validmind.ai/hub/user-redirect/lab/tree/how_to/filter_input_columns.ipynb){.button .button-green}
+[Try notebook on JupyterHub]({{< var url.jupyterhub >}}/hub/user-redirect/lab/tree/how_to/filter_input_columns.ipynb){.button .button-green}
 :::
 
 ::::
diff --git a/site/scripts/docker_entrypoint.sh b/site/scripts/docker_entrypoint.sh
new file mode 100755
index 0000000000..0594053c81
--- /dev/null
+++ b/site/scripts/docker_entrypoint.sh
@@ -0,0 +1,105 @@
+#!/bin/sh
+# Script to replace placeholders with actual URLs in HTML files and search.json
+
+set -e
+
+echo "==== Start ValidMind URL configuration ===="
+
+# Define paths
+CONFIG_FILE="/usr/share/nginx/html/config.json"
+HTML_DIR="/usr/share/nginx/html"
+
+# Define placeholder values, same as in modify_urls.sh
+VALIDMIND_PLACEHOLDER="https://app.prod.validmind-configurable-url.ai"
+JUPYTERHUB_PLACEHOLDER="https://jupyterhub.validmind-configurable-url.ai"
+
+echo "Initializing ValidMind documentation site..."
+
+# Initialize variables as empty
+VALIDMIND_URL=""
+JUPYTERHUB_URL=""
+
+# First check environment variables
+if [ -n "${VALIDMIND_URL:-}" ]; then
+    VALIDMIND_URL="$VALIDMIND_URL"
+    echo "Using VALIDMIND_URL from environment: $VALIDMIND_URL"
+fi
+
+if [ -n "${JUPYTERHUB_URL:-}" ]; then
+    JUPYTERHUB_URL="$JUPYTERHUB_URL"
+    echo "Using JUPYTERHUB_URL from environment: $JUPYTERHUB_URL"
+fi
+
+# If still empty, try config.json
+if { [ -z "$VALIDMIND_URL" ] || [ -z "$JUPYTERHUB_URL" ]; } && [ -f "$CONFIG_FILE" ]; then
+    echo "Looking for values in config file: $CONFIG_FILE"
+    
+    if command -v jq > /dev/null 2>&1; then
+        # Use jq if available
+        [ -z "$VALIDMIND_URL" ] && VALIDMIND_URL=$(jq -r '.VALIDMIND_URL // empty' "$CONFIG_FILE")
+        [ -z "$JUPYTERHUB_URL" ] && JUPYTERHUB_URL=$(jq -r '.JUPYTERHUB_URL // empty' "$CONFIG_FILE")
+    else
+        # Simple fallback if jq isn't available
+        [ -z "$VALIDMIND_URL" ] && VALIDMIND_URL=$(grep -o '"VALIDMIND_URL":"[^"]*"' "$CONFIG_FILE" 2>/dev/null | sed 's/"VALIDMIND_URL":"//;s/"//g')
+        [ -z "$JUPYTERHUB_URL" ] && JUPYTERHUB_URL=$(grep -o '"JUPYTERHUB_URL":"[^"]*"' "$CONFIG_FILE" 2>/dev/null | sed 's/"JUPYTERHUB_URL":"//;s/"//g')
+    fi
+    
+    [ -n "$VALIDMIND_URL" ] && echo "Using VALIDMIND_URL from config: $VALIDMIND_URL"
+    [ -n "$JUPYTERHUB_URL" ] && echo "Using JUPYTERHUB_URL from config: $JUPYTERHUB_URL"
+fi
+
+# Finally, use hardcoded defaults as a last resort
+if [ -z "$VALIDMIND_URL" ]; then
+    VALIDMIND_URL="https://app.prod.validmind.ai"
+    echo "Using default VALIDMIND_URL: $VALIDMIND_URL"
+fi
+
+if [ -z "$JUPYTERHUB_URL" ]; then
+    JUPYTERHUB_URL="https://jupyterhub.validmind.ai"
+    echo "Using default JUPYTERHUB_URL: $JUPYTERHUB_URL"
+fi
+
+# Final check to ensure we have non-empty values
+if [ -z "$VALIDMIND_URL" ] || [ -z "$JUPYTERHUB_URL" ]; then
+    echo "Error: One or both URLs are still empty. This will cause replacement issues."
+    exit 1
+fi
+
+# Count total placeholder instances
+VALIDMIND_COUNT=$(grep -r "$VALIDMIND_PLACEHOLDER" "$HTML_DIR" | wc -l)
+JUPYTERHUB_COUNT=$(grep -r "$JUPYTERHUB_PLACEHOLDER" "$HTML_DIR" | wc -l)
+echo "Found placeholders:"
+echo "$VALIDMIND_COUNT instances of $VALIDMIND_PLACEHOLDER"
+echo "$JUPYTERHUB_COUNT instances of $JUPYTERHUB_PLACEHOLDER"
+
+# Replace placeholders in HTML files
+echo "Replacing URL placeholders in HTML files..."
+find "$HTML_DIR" -type f -name "*.html" -exec sed -i "s|$VALIDMIND_PLACEHOLDER|$VALIDMIND_URL|g" {} \;
+find "$HTML_DIR" -type f -name "*.html" -exec sed -i "s|$JUPYTERHUB_PLACEHOLDER|$JUPYTERHUB_URL|g" {} \;
+
+# Replace placeholders in search.json file
+echo "Replacing URL placeholders in search.json..."
+if [ -f "$HTML_DIR/search.json" ]; then
+    sed -i "s|$VALIDMIND_PLACEHOLDER|$VALIDMIND_URL|g" "$HTML_DIR/search.json"
+    sed -i "s|$JUPYTERHUB_PLACEHOLDER|$JUPYTERHUB_URL|g" "$HTML_DIR/search.json"
+else
+    echo "search.json file not found"
+fi
+
+# Verify replacement
+VALIDMIND_AFTER=$(grep -r "$VALIDMIND_PLACEHOLDER" "$HTML_DIR" | wc -l)
+JUPYTERHUB_AFTER=$(grep -r "$JUPYTERHUB_PLACEHOLDER" "$HTML_DIR" | wc -l)
+echo "After replacement:"
+echo "$VALIDMIND_AFTER instances of $VALIDMIND_PLACEHOLDER"
+echo "$JUPYTERHUB_AFTER instances of $JUPYTERHUB_PLACEHOLDER"
+
+if [ "$VALIDMIND_AFTER" -eq 0 ] && [ "$JUPYTERHUB_AFTER" -eq 0 ]; then
+    echo "✓ URL replacement completed successfully"
+else
+    echo "⚠ Some placeholders might not have been replaced"
+    echo "Files still containing placeholders:"
+    grep -l "$VALIDMIND_PLACEHOLDER" "$HTML_DIR" | head -5
+    grep -l "$JUPYTERHUB_PLACEHOLDER" "$HTML_DIR" | head -5
+fi
+
+echo "==== URL configuration complete ===="
diff --git a/site/scripts/modify_urls.sh b/site/scripts/modify_urls.sh
new file mode 100755
index 0000000000..2383a6863c
--- /dev/null
+++ b/site/scripts/modify_urls.sh
@@ -0,0 +1,62 @@
+#!/bin/bash
+# Script to extract URL values and replace them with placeholders for Docker image
+
+# Define input and output paths
+VARIABLES_PATH="_variables.yml"
+CONFIG_PATH="config.json"
+
+# Define placeholder values, same as in docker_entrypoint.sh
+VALIDMIND_PLACEHOLDER="https://app.prod.validmind-configurable-url.ai"
+JUPYTERHUB_PLACEHOLDER="https://jupyterhub.validmind-configurable-url.ai"
+
+# Check if variables file exists
+if [ ! -f "$VARIABLES_PATH" ]; then
+    echo "Error: $VARIABLES_PATH not found"
+    exit 1
+fi
+
+# First, save the current state of the file in case we need to restore it
+git diff "$VARIABLES_PATH" > /dev/null 2>&1 || git add "$VARIABLES_PATH" -N
+
+# Use awk to directly extract URL values by looking for specific patterns
+VALIDMIND_URL=$(awk '/url:/{flag=1} flag && /us1:/{gsub(/"/, "", $2); print $2; exit}' "$VARIABLES_PATH")
+JUPYTERHUB_URL=$(awk '/url:/{flag=1} flag && /jupyterhub:/{gsub(/"/, "", $2); print $2; exit}' "$VARIABLES_PATH")
+
+# Check if values were found
+if [ -z "$VALIDMIND_URL" ] || [ -z "$JUPYTERHUB_URL" ]; then
+    echo "Error: Could not extract URLs from $VARIABLES_PATH"
+    echo "Current content of $VARIABLES_PATH:"
+    cat "$VARIABLES_PATH"
+    exit 1
+fi
+
+echo "Found URLs in $VARIABLES_PATH:"
+echo "VALIDMIND_URL: $VALIDMIND_URL"
+echo "JUPYTERHUB_URL: $JUPYTERHUB_URL"
+
+# Generate the JSON file with extracted values for fallback
+cat > "$CONFIG_PATH" << EOF
+{
+  "VALIDMIND_URL": "$VALIDMIND_URL",
+  "JUPYTERHUB_URL": "$JUPYTERHUB_URL"
+}
+EOF
+
+# Check if JSON file was created successfully
+if [ ! -f "$CONFIG_PATH" ]; then
+    echo "Error: Failed to generate $CONFIG_PATH"
+    exit 1
+fi
+
+echo "Successfully generated $CONFIG_PATH from $VARIABLES_PATH"
+
+# Replace the actual URLs with placeholder URLs
+sed -i'.tmp' -E "s|(us1:[ ]*\")([^\"]+)(\")|\1$VALIDMIND_PLACEHOLDER\3|g" "$VARIABLES_PATH"
+sed -i'.tmp' -E "s|(jupyterhub:[ ]*\")([^\"]+)(\")|\1$JUPYTERHUB_PLACEHOLDER\3|g" "$VARIABLES_PATH"
+
+# Remove temporary files created by sed
+rm -f "${VARIABLES_PATH}.tmp"
+
+echo "Modified $VARIABLES_PATH with placeholder values:"
+echo "VALIDMIND_URL: $VALIDMIND_PLACEHOLDER"
+echo "JUPYTERHUB_URL: $JUPYTERHUB_PLACEHOLDER"
\ No newline at end of file
diff --git a/site/training/_training-register.qmd b/site/training/_training-register.qmd
index 9c0ba675fd..5b380fa5c5 100644
--- a/site/training/_training-register.qmd
+++ b/site/training/_training-register.qmd
@@ -6,7 +6,7 @@
 
 2. **When you receive your access credentials**, make sure you can log in:
 
-   - **{{< var vm.product >}} JupyterHub** — <https://jupyterhub.validmind.ai/>
+   - **{{< var vm.product >}} JupyterHub** — <{{< var url.jupyterhub >}}/>
    - **{{< var validmind.platform >}}** — [{{< var url.us1 >}}]({{< var url.us1 >}})^[[Log in to {{< var vm.product >}}](/guide/configuration/log-in-to-validmind.qmd#public-internet)]
 
 3. **Join a free kick-off session**: Get an instructor-led hands-on demo.
diff --git a/site/training/developer-fundamentals/developer-fundamentals.qmd b/site/training/developer-fundamentals/developer-fundamentals.qmd
index 04b86ad528..683ac5ae50 100644
--- a/site/training/developer-fundamentals/developer-fundamentals.qmd
+++ b/site/training/developer-fundamentals/developer-fundamentals.qmd
@@ -71,7 +71,7 @@ To try out this course, you need to have been [onboarded](developer-fundamentals
 :::: {.columns}
 ::: {.column width="50%"}
 ::: {.tc}
-[Log in to JupyterHub](https://jupyterhub.validmind.ai/){.button target="_blank"}
+[Log in to JupyterHub]({{< var url.jupyterhub >}}/){.button target="_blank"}
 :::
 
 :::
@@ -90,7 +90,7 @@ Be sure to return to this page afterwards.
 
 # You're in — let's show you around.
 
-<!-- ## {background-iframe="https://jupyterhub.validmind.ai/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb" background-interactive="yes" data-preload="yes"}  -->
+<!-- ## {background-iframe="{{< var url.jupyterhub >}}/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb" background-interactive="yes" data-preload="yes"}  -->
 
 ## {background-iframe="/notebooks/tutorials/intro_for_model_developers_EXECUTED.html" background-interactive="yes" data-preload="yes"} 
 
@@ -150,12 +150,12 @@ When you're done, click [{{< fa chevron-right >}}]() to <br>continue.
 
 # Initialize the {{< var vm.developer >}} {background-color="#083E44" background-image="/training/assets/home-hero.svg"}
 
-<!-- ## {background-iframe="https://jupyterhub.validmind.ai/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb" background-interactive="yes" data-preload="yes"}  -->
+<!-- ## {background-iframe="{{< var url.jupyterhub >}}/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb" background-interactive="yes" data-preload="yes"}  -->
 
 ## {background-iframe="/notebooks/tutorials/intro_for_model_developers_EXECUTED.html#initializing-the-validmind-library" data-preload="yes"} 
 
 :::: {.absolute bottom=15 .w-100 .f3 .tc .pl4 .overlay}
-On JupyterHub: <a href="https://jupyterhub.validmind.ai/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb" target="_blank">Run the cells in **1. Initializing the {{< var validmind.developer >}}**</a>.
+On JupyterHub: <a href="{{< var url.jupyterhub >}}/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb" target="_blank">Run the cells in **1. Initializing the {{< var validmind.developer >}}**</a>.
 
 ::: {.f5 .nt2}
 When you are done, return to this page and click [{{< fa chevron-right >}}]() to continue.
@@ -164,12 +164,12 @@ When you are done, return to this page and click [{{< fa chevron-right >}}]() to
 
 # Start the model development process {background-color="#083E44" background-image="/training/assets/home-hero.svg"}
 
-<!-- ## {background-iframe="https://jupyterhub.validmind.ai/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb" background-interactive="yes" data-preload="yes"}  -->
+<!-- ## {background-iframe="{{< var url.jupyterhub >}}/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb" background-interactive="yes" data-preload="yes"}  -->
 
 ## {background-iframe="/notebooks/tutorials/intro_for_model_developers_EXECUTED.html#start-the-model-development-process-with-raw-data-run-out-of-the-box-tests-and-add-evidence-to-model-documentation" data-preload="yes"} 
 
 :::: {.absolute bottom=15 .w-100 .f3 .tc .pl4 .overlay}
-On JupyterHub: <a href="https://jupyterhub.validmind.ai/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb" target="_blank">Run the cells in **2. Start the model development process with raw data, run out-of-the box tests, and add evidence to model documentation**</a>. 
+On JupyterHub: <a href="{{< var url.jupyterhub >}}/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb" target="_blank">Run the cells in **2. Start the model development process with raw data, run out-of-the box tests, and add evidence to model documentation**</a>. 
 
 ::: {.f4}
 When you reach **Add individual test results to model documentation**, return to this page and click [{{< fa chevron-right >}}]() to continue.
@@ -271,7 +271,7 @@ Have a question? Collaborate with other developers or with your validator right
 
 ::::
 
-<!-- ## {background-iframe="https://jupyterhub.validmind.ai/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb" background-interactive="yes" data-preload="yes"}  -->
+<!-- ## {background-iframe="{{< var url.jupyterhub >}}/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb" background-interactive="yes" data-preload="yes"}  -->
 
 ## {background-iframe="https://app.prod.validmind.ai" background-interactive="yes" data-preload="yes"}
 
@@ -287,12 +287,12 @@ When you are done, click [{{< fa chevron-right >}}]() to continue.
 
 # Train a model {background-color="#083E44" background-image="/training/assets/home-hero.svg"}
 
-<!-- ## {background-iframe="https://jupyterhub.validmind.ai/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb" background-interactive="yes" data-preload="yes"}  -->
+<!-- ## {background-iframe="{{< var url.jupyterhub >}}/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb" background-interactive="yes" data-preload="yes"}  -->
 
 ## {background-iframe="/notebooks/tutorials/intro_for_model_developers_EXECUTED.html#model-testing" data-preload="yes"} 
 
 :::: {.absolute bottom=15 .w-100 .f3 .tc .pl4 .overlay}
-On JupyterHub: <a href="https://jupyterhub.validmind.ai/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb" target="_blank">Run the cells under **Model Testing**</a>.
+On JupyterHub: <a href="{{< var url.jupyterhub >}}/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb" target="_blank">Run the cells under **Model Testing**</a>.
 
 ::: {.f5 .nt2}
 When you are done, return to this page and click [{{< fa chevron-right >}}]() to continue.
@@ -301,12 +301,12 @@ When you are done, return to this page and click [{{< fa chevron-right >}}]() to
 
 # Implement custom tests and integrate external test providers {background-color="#083E44" background-image="/training/assets/home-hero.svg"}
 
-<!-- ## {background-iframe="https://jupyterhub.validmind.ai/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb" background-interactive="yes" data-preload="yes"}  -->
+<!-- ## {background-iframe="{{< var url.jupyterhub >}}/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb" background-interactive="yes" data-preload="yes"}  -->
 
 ## {background-iframe="/notebooks/tutorials/intro_for_model_developers_EXECUTED.html#implementing-custom-tests" data-preload="yes"} 
 
 :::: {.absolute bottom=15 .w-100 .f3 .tc .pl4 .overlay}
-On JupyterHub: <a href="https://jupyterhub.validmind.ai/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb" target="_blank">Run the cells in **3. Implementing custom tests**</a>.
+On JupyterHub: <a href="{{< var url.jupyterhub >}}/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb" target="_blank">Run the cells in **3. Implementing custom tests**</a>.
 
 ::: {.f5 .nt2}
 When you are done, return to this page and click [{{< fa chevron-right >}}]() to continue.
@@ -315,13 +315,13 @@ When you are done, return to this page and click [{{< fa chevron-right >}}]() to
 
 # Finalize testing and documentation {background-color="#083E44" background-image="/training/assets/home-hero.svg"}
 
-<!-- ## {background-iframe="https://jupyterhub.validmind.ai/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb" background-interactive="yes" data-preload="yes"}  -->
+<!-- ## {background-iframe="{{< var url.jupyterhub >}}/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb" background-interactive="yes" data-preload="yes"}  -->
 
 ## {background-iframe="/notebooks/tutorials/intro_for_model_developers_EXECUTED.html#finalize-testing-and-documentation" data-preload="yes"} 
 
 :::: {.absolute bottom=15 .w-100 .f3 .tc .pl4 .overlay}
 
-On JupyterHub: <a href="https://jupyterhub.validmind.ai/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb" target="_blank">Run the cells in **4. Finalize testing and documentation**</a>.
+On JupyterHub: <a href="{{< var url.jupyterhub >}}/hub/user-redirect/lab/tree/tutorials/intro_for_model_developers.ipynb" target="_blank">Run the cells in **4. Finalize testing and documentation**</a>.
 
 ::: {.f5 .nt2}
 When you are done, return to this page and click [{{< fa chevron-right >}}]() to continue.
diff --git a/site/training/program/learning-paths.qmd b/site/training/program/learning-paths.qmd
index 5de373bb44..164ef65af0 100644
--- a/site/training/program/learning-paths.qmd
+++ b/site/training/program/learning-paths.qmd
@@ -268,7 +268,7 @@ As a monitoring user who is new to ValidMind, learn how to generate ongoing moni
 ::: {.w-30-ns .pl4 .bl}
 **Notes**
 
-- Based on [ongoing monitoring notebook](https://jupyterhub.validmind.ai/hub/user-redirect/lab/tree/code_samples/ongoing_monitoring/application_scorecard_ongoing_monitoring.ipynb)
+- Based on [ongoing monitoring notebook]({{< var url.jupyterhub >}}/hub/user-redirect/lab/tree/code_samples/ongoing_monitoring/application_scorecard_ongoing_monitoring.ipynb)
 :::
 
 ::::
diff --git a/validmind-docs.yaml b/validmind-docs.yaml
new file mode 100644
index 0000000000..bd2fd91dd8
--- /dev/null
+++ b/validmind-docs.yaml
@@ -0,0 +1,76 @@
+---
+# Sample Kubernetes manifest for configuring the Docker docs site
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: validmind-docs
+  labels:
+    app: validmind-docs
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: validmind-docs
+  template:
+    metadata:
+      labels:
+        app: validmind-docs
+    spec:
+      containers:
+      - name: validmind-docs
+        image: validmind/docs:latest
+        ports:
+        - containerPort: 80
+        env:
+        - name: VALIDMIND_URL
+          value: "https://your-custom-app.validmind.ai"
+        - name: JUPYTERHUB_URL
+          value: "https://your-custom-jupyter.validmind.ai"
+---
+# Option 2: ConfigMap approach (more maintainable)
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: validmind-docs-config
+data:
+  VALIDMIND_URL: "https://your-custom-app.validmind.ai"
+  JUPYTERHUB_URL: "https://your-custom-jupyter.validmind.ai"
+---
+# Deployment using ConfigMap
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: validmind-docs-configmap
+  labels:
+    app: validmind-docs-configmap
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: validmind-docs-configmap
+  template:
+    metadata:
+      labels:
+        app: validmind-docs-configmap
+    spec:
+      containers:
+      - name: validmind-docs
+        image: validmind/docs:latest
+        ports:
+        - containerPort: 80
+        envFrom:
+        - configMapRef:
+            name: validmind-docs-config
+---
+# Service for accessing the documentation
+apiVersion: v1
+kind: Service
+metadata:
+  name: validmind-docs
+spec:
+  selector:
+    app: validmind-docs  # Change to validmind-docs-configmap if using the ConfigMap deployment
+  ports:
+  - port: 80
+    targetPort: 80
+  type: ClusterIP 
\ No newline at end of file


Reviews:
user: login: nrichers
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T21:59:58Z
user: login: nrichers
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T22:03:11Z
user: login: nibalizer
body: One note and one question.

Q: What are the startup time implications? Just a couple seconds or more? Does failing to template hard crash the server or just some links are broken?
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T18:45:25Z
user: login: nrichers
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-20T21:26:15Z
user: login: validbeck
body: This looks good to me, but the last time I checked any self-hosted stuff was not to be exposed to users/in general docs release notes, and the way this PR is tagged will have the notes show up in our collation.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-20T23:17:56Z
user: login: nrichers
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-21T22:31:55Z

Review comments:
diff_hunk: @@ -68,7 +68,7 @@ This notebook provides a hands-on example of setting up and conducting monitorin
 
 ::: {.w-30-ns}
 
-[Try notebook on JupyterHub](https://jupyterhub.validmind.ai/hub/user-redirect/lab/tree/code_samples/ongoing_monitoring/quickstart_customer_churn_ongoing_monitoring.ipynb){.button .button-green}
+[Try notebook on JupyterHub]({{< var url.jupyterhub >}}/hub/user-redirect/lab/tree/code_samples/ongoing_monitoring/quickstart_customer_churn_ongoing_monitoring.ipynb){.button .button-green}
path: site/releases/2024/2024-oct-22/release-notes.qmd
user: login: nrichers
body: @validbeck FYI, these variables in listings do not resolve, another quirk I've discovered — I've tried troubleshooting this for several hours and there might not be a good solution. Only two files are affected, this one and `training .qmd`.  

Possible workarounds/fixes:

- Don't use listings for these links 
- Don't use variables and hardcode the links to our JupyterHub instance
- Fix Quarto or create our own pre-processing LUA extension ...
created_at: 2025-03-19T21:59:57Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,76 @@
+---
path: validmind-docs.yaml
user: login: nrichers
body: @nibalizer as mentioned in the root comment of the PR, I'd love your help with verifying that this behaves as expected. I have tinkered a bit with `kubectl apply -f validmind-docs.yaml` but nothing like what a real-life environment might require. I'd like to make sure this passes muster with you before we ship. 
created_at: 2025-03-19T22:03:10Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,105 @@
+#!/bin/sh
+# Script to replace placeholders with actual URLs in HTML files and search.json
+
+set -e
+
+echo "==== Start ValidMind URL configuration ===="
+
+# Define paths
+CONFIG_FILE="/usr/share/nginx/html/config.json"
+HTML_DIR="/usr/share/nginx/html"
+
+# Define placeholder values, same as in modify_urls.sh
+VALIDMIND_PLACEHOLDER="https://app.prod.validmind-configurable-url.ai"
+JUPYTERHUB_PLACEHOLDER="https://jupyterhub.validmind-configurable-url.ai"
+
+echo "Initializing ValidMind documentation site..."
+
+# Initialize variables as empty
+VALIDMIND_URL=""
+JUPYTERHUB_URL=""
+
+# First check environment variables
+if [ -n "${VALIDMIND_URL:-}" ]; then
+    VALIDMIND_URL="$VALIDMIND_URL"
+    echo "Using VALIDMIND_URL from environment: $VALIDMIND_URL"
+fi
+
+if [ -n "${JUPYTERHUB_URL:-}" ]; then
+    JUPYTERHUB_URL="$JUPYTERHUB_URL"
+    echo "Using JUPYTERHUB_URL from environment: $JUPYTERHUB_URL"
+fi
+
+# If still empty, try config.json
+if { [ -z "$VALIDMIND_URL" ] || [ -z "$JUPYTERHUB_URL" ]; } && [ -f "$CONFIG_FILE" ]; then
+    echo "Looking for values in config file: $CONFIG_FILE"
+    
+    if command -v jq > /dev/null 2>&1; then
+        # Use jq if available
+        [ -z "$VALIDMIND_URL" ] && VALIDMIND_URL=$(jq -r '.VALIDMIND_URL // empty' "$CONFIG_FILE")
+        [ -z "$JUPYTERHUB_URL" ] && JUPYTERHUB_URL=$(jq -r '.JUPYTERHUB_URL // empty' "$CONFIG_FILE")
+    else
+        # Simple fallback if jq isn't available
+        [ -z "$VALIDMIND_URL" ] && VALIDMIND_URL=$(grep -o '"VALIDMIND_URL":"[^"]*"' "$CONFIG_FILE" 2>/dev/null | sed 's/"VALIDMIND_URL":"//;s/"//g')
+        [ -z "$JUPYTERHUB_URL" ] && JUPYTERHUB_URL=$(grep -o '"JUPYTERHUB_URL":"[^"]*"' "$CONFIG_FILE" 2>/dev/null | sed 's/"JUPYTERHUB_URL":"//;s/"//g')
+    fi
+    
+    [ -n "$VALIDMIND_URL" ] && echo "Using VALIDMIND_URL from config: $VALIDMIND_URL"
+    [ -n "$JUPYTERHUB_URL" ] && echo "Using JUPYTERHUB_URL from config: $JUPYTERHUB_URL"
+fi
+
+# Finally, use hardcoded defaults as a last resort
+if [ -z "$VALIDMIND_URL" ]; then
+    VALIDMIND_URL="https://app.prod.validmind.ai"
+    echo "Using default VALIDMIND_URL: $VALIDMIND_URL"
+fi
+
+if [ -z "$JUPYTERHUB_URL" ]; then
+    JUPYTERHUB_URL="https://jupyterhub.validmind.ai"
+    echo "Using default JUPYTERHUB_URL: $JUPYTERHUB_URL"
+fi
+
+# Final check to ensure we have non-empty values
+if [ -z "$VALIDMIND_URL" ] || [ -z "$JUPYTERHUB_URL" ]; then
+    echo "Error: One or both URLs are still empty. This will cause replacement issues."
+    exit 1
+fi
+
+# Count total placeholder instances
+VALIDMIND_COUNT=$(grep -r "$VALIDMIND_PLACEHOLDER" "$HTML_DIR" | wc -l)
+JUPYTERHUB_COUNT=$(grep -r "$JUPYTERHUB_PLACEHOLDER" "$HTML_DIR" | wc -l)
+echo "Found placeholders:"
+echo "$VALIDMIND_COUNT instances of $VALIDMIND_PLACEHOLDER"
+echo "$JUPYTERHUB_COUNT instances of $JUPYTERHUB_PLACEHOLDER"
+
+# Replace placeholders in HTML files
+echo "Replacing URL placeholders in HTML files..."
+find "$HTML_DIR" -type f -name "*.html" -exec sed -i "s|$VALIDMIND_PLACEHOLDER|$VALIDMIND_URL|g" {} \;
+find "$HTML_DIR" -type f -name "*.html" -exec sed -i "s|$JUPYTERHUB_PLACEHOLDER|$JUPYTERHUB_URL|g" {} \;
+
+# Replace placeholders in search.json file
+echo "Replacing URL placeholders in search.json..."
+if [ -f "$HTML_DIR/search.json" ]; then
+    sed -i "s|$VALIDMIND_PLACEHOLDER|$VALIDMIND_URL|g" "$HTML_DIR/search.json"
+    sed -i "s|$JUPYTERHUB_PLACEHOLDER|$JUPYTERHUB_URL|g" "$HTML_DIR/search.json"
+else
+    echo "search.json file not found"
+fi
+
+# Verify replacement
+VALIDMIND_AFTER=$(grep -r "$VALIDMIND_PLACEHOLDER" "$HTML_DIR" | wc -l)
+JUPYTERHUB_AFTER=$(grep -r "$JUPYTERHUB_PLACEHOLDER" "$HTML_DIR" | wc -l)
+echo "After replacement:"
+echo "$VALIDMIND_AFTER instances of $VALIDMIND_PLACEHOLDER"
+echo "$JUPYTERHUB_AFTER instances of $JUPYTERHUB_PLACEHOLDER"
+
+if [ "$VALIDMIND_AFTER" -eq 0 ] && [ "$JUPYTERHUB_AFTER" -eq 0 ]; then
+    echo "✓ URL replacement completed successfully"
+else
+    echo "⚠ Some placeholders might not have been replaced"
+    echo "Files still containing placeholders:"
+    grep -l "$VALIDMIND_PLACEHOLDER" "$HTML_DIR" | head -5
path: site/scripts/docker_entrypoint.sh
user: login: nibalizer
body: Is there a reason we have `| head -5` here?
created_at: 2025-03-20T18:42:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,76 @@
+---
path: validmind-docs.yaml
user: login: nibalizer
body: yep this seems fine. 
created_at: 2025-03-20T18:44:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,105 @@
+#!/bin/sh
+# Script to replace placeholders with actual URLs in HTML files and search.json
+
+set -e
+
+echo "==== Start ValidMind URL configuration ===="
+
+# Define paths
+CONFIG_FILE="/usr/share/nginx/html/config.json"
+HTML_DIR="/usr/share/nginx/html"
+
+# Define placeholder values, same as in modify_urls.sh
+VALIDMIND_PLACEHOLDER="https://app.prod.validmind-configurable-url.ai"
+JUPYTERHUB_PLACEHOLDER="https://jupyterhub.validmind-configurable-url.ai"
+
+echo "Initializing ValidMind documentation site..."
+
+# Initialize variables as empty
+VALIDMIND_URL=""
+JUPYTERHUB_URL=""
+
+# First check environment variables
+if [ -n "${VALIDMIND_URL:-}" ]; then
+    VALIDMIND_URL="$VALIDMIND_URL"
+    echo "Using VALIDMIND_URL from environment: $VALIDMIND_URL"
+fi
+
+if [ -n "${JUPYTERHUB_URL:-}" ]; then
+    JUPYTERHUB_URL="$JUPYTERHUB_URL"
+    echo "Using JUPYTERHUB_URL from environment: $JUPYTERHUB_URL"
+fi
+
+# If still empty, try config.json
+if { [ -z "$VALIDMIND_URL" ] || [ -z "$JUPYTERHUB_URL" ]; } && [ -f "$CONFIG_FILE" ]; then
+    echo "Looking for values in config file: $CONFIG_FILE"
+    
+    if command -v jq > /dev/null 2>&1; then
+        # Use jq if available
+        [ -z "$VALIDMIND_URL" ] && VALIDMIND_URL=$(jq -r '.VALIDMIND_URL // empty' "$CONFIG_FILE")
+        [ -z "$JUPYTERHUB_URL" ] && JUPYTERHUB_URL=$(jq -r '.JUPYTERHUB_URL // empty' "$CONFIG_FILE")
+    else
+        # Simple fallback if jq isn't available
+        [ -z "$VALIDMIND_URL" ] && VALIDMIND_URL=$(grep -o '"VALIDMIND_URL":"[^"]*"' "$CONFIG_FILE" 2>/dev/null | sed 's/"VALIDMIND_URL":"//;s/"//g')
+        [ -z "$JUPYTERHUB_URL" ] && JUPYTERHUB_URL=$(grep -o '"JUPYTERHUB_URL":"[^"]*"' "$CONFIG_FILE" 2>/dev/null | sed 's/"JUPYTERHUB_URL":"//;s/"//g')
+    fi
+    
+    [ -n "$VALIDMIND_URL" ] && echo "Using VALIDMIND_URL from config: $VALIDMIND_URL"
+    [ -n "$JUPYTERHUB_URL" ] && echo "Using JUPYTERHUB_URL from config: $JUPYTERHUB_URL"
+fi
+
+# Finally, use hardcoded defaults as a last resort
+if [ -z "$VALIDMIND_URL" ]; then
+    VALIDMIND_URL="https://app.prod.validmind.ai"
+    echo "Using default VALIDMIND_URL: $VALIDMIND_URL"
+fi
+
+if [ -z "$JUPYTERHUB_URL" ]; then
+    JUPYTERHUB_URL="https://jupyterhub.validmind.ai"
+    echo "Using default JUPYTERHUB_URL: $JUPYTERHUB_URL"
+fi
+
+# Final check to ensure we have non-empty values
+if [ -z "$VALIDMIND_URL" ] || [ -z "$JUPYTERHUB_URL" ]; then
+    echo "Error: One or both URLs are still empty. This will cause replacement issues."
+    exit 1
+fi
+
+# Count total placeholder instances
+VALIDMIND_COUNT=$(grep -r "$VALIDMIND_PLACEHOLDER" "$HTML_DIR" | wc -l)
+JUPYTERHUB_COUNT=$(grep -r "$JUPYTERHUB_PLACEHOLDER" "$HTML_DIR" | wc -l)
+echo "Found placeholders:"
+echo "$VALIDMIND_COUNT instances of $VALIDMIND_PLACEHOLDER"
+echo "$JUPYTERHUB_COUNT instances of $JUPYTERHUB_PLACEHOLDER"
+
+# Replace placeholders in HTML files
+echo "Replacing URL placeholders in HTML files..."
+find "$HTML_DIR" -type f -name "*.html" -exec sed -i "s|$VALIDMIND_PLACEHOLDER|$VALIDMIND_URL|g" {} \;
+find "$HTML_DIR" -type f -name "*.html" -exec sed -i "s|$JUPYTERHUB_PLACEHOLDER|$JUPYTERHUB_URL|g" {} \;
+
+# Replace placeholders in search.json file
+echo "Replacing URL placeholders in search.json..."
+if [ -f "$HTML_DIR/search.json" ]; then
+    sed -i "s|$VALIDMIND_PLACEHOLDER|$VALIDMIND_URL|g" "$HTML_DIR/search.json"
+    sed -i "s|$JUPYTERHUB_PLACEHOLDER|$JUPYTERHUB_URL|g" "$HTML_DIR/search.json"
+else
+    echo "search.json file not found"
+fi
+
+# Verify replacement
+VALIDMIND_AFTER=$(grep -r "$VALIDMIND_PLACEHOLDER" "$HTML_DIR" | wc -l)
+JUPYTERHUB_AFTER=$(grep -r "$JUPYTERHUB_PLACEHOLDER" "$HTML_DIR" | wc -l)
+echo "After replacement:"
+echo "$VALIDMIND_AFTER instances of $VALIDMIND_PLACEHOLDER"
+echo "$JUPYTERHUB_AFTER instances of $JUPYTERHUB_PLACEHOLDER"
+
+if [ "$VALIDMIND_AFTER" -eq 0 ] && [ "$JUPYTERHUB_AFTER" -eq 0 ]; then
+    echo "✓ URL replacement completed successfully"
+else
+    echo "⚠ Some placeholders might not have been replaced"
+    echo "Files still containing placeholders:"
+    grep -l "$VALIDMIND_PLACEHOLDER" "$HTML_DIR" | head -5
path: site/scripts/docker_entrypoint.sh
user: login: nrichers
body: Using `| head -5` here gives us a quick answer to "do we have a problem — yes/no?". We can remove it but it's here as an efficient way to provide a sample of problematic files rather than giving us the full list.
created_at: 2025-03-20T21:26:14Z
author_association: COLLABORATOR
diff_hunk: @@ -68,7 +68,7 @@ This notebook provides a hands-on example of setting up and conducting monitorin
 
 ::: {.w-30-ns}
 
-[Try notebook on JupyterHub](https://jupyterhub.validmind.ai/hub/user-redirect/lab/tree/code_samples/ongoing_monitoring/quickstart_customer_churn_ongoing_monitoring.ipynb){.button .button-green}
+[Try notebook on JupyterHub]({{< var url.jupyterhub >}}/hub/user-redirect/lab/tree/code_samples/ongoing_monitoring/quickstart_customer_churn_ongoing_monitoring.ipynb){.button .button-green}
path: site/releases/2024/2024-oct-22/release-notes.qmd
user: login: nrichers
body: Backed out these changes, leaving only hardcoded JupyterHub links to our public instance. 
created_at: 2025-03-21T22:31:55Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-19T21:52:25Z
body: # PR Summary
This pull request introduces several enhancements to the Docker configuration for the ValidMind documentation site. The key changes include:

1. **Dockerfile Enhancements**: 
   - Added installation of `jq` for JSON handling.
   - Introduced a new entrypoint script `docker_entrypoint.sh` to replace URL placeholders with actual values at runtime.
   - Made the entrypoint script executable.

2. **Dynamic URL Configuration**:
   - Introduced a `config.json` file to store default URL values for `VALIDMIND_URL` and `JUPYTERHUB_URL`.
   - Updated various documentation files to use placeholders for URLs, which are replaced at runtime by the entrypoint script.

3. **Makefile and Build Process**:
   - Added new Makefile targets `docker-site` and `docker-site-lite` for building the site with Docker-specific configurations.
   - Updated the `docker-build` target to use the new `docker-site-lite` process.

4. **Kubernetes Configuration**:
   - Added a sample Kubernetes manifest `validmind-docs.yaml` for deploying the documentation site with configurable URLs via environment variables or a ConfigMap.

5. **Scripts for URL Handling**:
   - Added `modify_urls.sh` to replace actual URLs with placeholders during the build process.
   - The `docker_entrypoint.sh` script handles the replacement of placeholders with actual URLs at runtime, using environment variables or `config.json` as sources.

These changes allow for more flexible deployment of the documentation site, enabling dynamic configuration of URLs based on the deployment environment.

## Test Suggestions
- Test the Docker image build process to ensure it completes without errors.
- Run the Docker container and verify that the URL placeholders are correctly replaced with actual values.
- Test the site functionality by accessing it through the configured URLs in a browser.
- Verify that the Kubernetes deployment using the provided manifest works as expected.
- Check that the `config.json` file is correctly generated and used by the entrypoint script.
- Ensure that the `modify_urls.sh` script correctly replaces URLs with placeholders during the build process.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-20T00:11:57Z
body: # PR Summary
This pull request introduces several enhancements to the Docker configuration for the ValidMind documentation site. The key changes include:

1. **Dockerfile Enhancements**: 
   - Added installation of `jq` for JSON handling.
   - Introduced a new entrypoint script `docker_entrypoint.sh` to replace URL placeholders with actual values.
   - Made the entrypoint script executable.

2. **Dynamic URL Configuration**:
   - Introduced a `config.json` file to store fallback URL values for `VALIDMIND_URL` and `JUPYTERHUB_URL`.
   - Added a script `modify_urls.sh` to replace actual URLs with placeholders during the Docker image build process.
   - Updated various documentation files to use URL placeholders, allowing dynamic URL configuration at runtime.

3. **Makefile and Build Process**:
   - Added new Makefile targets `docker-site` and `docker-site-lite` for rendering the site with Docker profiles.
   - Updated `docker-build` to use the new `docker-site-lite` target.

4. **Kubernetes Configuration**:
   - Added a sample Kubernetes manifest `validmind-docs.yaml` for deploying the documentation site with configurable URLs using environment variables or a ConfigMap.

5. **Documentation Updates**:
   - Updated README and other markdown files to reflect the new URL configuration process.

These changes aim to improve the flexibility and maintainability of the documentation site deployment, particularly in containerized environments.

## Test Suggestions
- Test the Docker image build process to ensure `jq` is installed and the entrypoint script is executable.
- Verify that the `docker_entrypoint.sh` script correctly replaces URL placeholders with environment variable values.
- Test the fallback mechanism using `config.json` when environment variables are not set.
- Ensure that the site renders correctly with the new Docker profile by running `make docker-site` and `make docker-site-lite`.
- Deploy the site using the provided Kubernetes manifest and verify that the URLs are correctly configured.
- Check that all links in the documentation resolve correctly when accessed through the Docker container.
author_association: CONTRIBUTOR
user: login: nrichers
created_at: 2025-03-20T21:30:49Z
body: > Q: What are the startup time implications? Just a couple seconds or more?

@nibalizer just checked and the script adds <10 seconds to the startup on my Mac with whatever default resources Docker Desktop assigns. 

> Does failing to template hard crash the server or just some links are broken?

We should fall back to the public defaults but if even this fails, you just get some URLs that 404. I verified this during script development.

author_association: COLLABORATOR
user: login: github-actions[bot]
created_at: 2025-03-21T22:52:07Z
body: A PR preview is available: [Preview URL](https://docs-demo.vm.validmind.ai/pr_previews/nrichers/sc-8752/investigate-passing-in-environment-variables/index.html)
author_association: CONTRIBUTOR
user: login: nrichers
created_at: 2025-03-22T18:05:59Z
body: > This looks good to me, but the last time I checked any self-hosted stuff was not to be exposed to users/in general docs release notes, and the way this PR is tagged will have the notes show up in our collation.

This is a good point — but the Docker image also has uses beyond self-hosting. I rewrote the release notes to mention testing during development, similar to how you can run the frontend on localhost, or accessing the full docs site locally. 
author_association: COLLABORATOR