From ad4eb14adbeb50c1870b0a00f07c5ab8ad2cf942 Mon Sep 17 00:00:00 2001 From: "ryan.stephen" Date: Tue, 1 Sep 2026 00:15:07 +0000 Subject: [PATCH 1/7] docs(self-hosted): document per-deployment runtime env values, drop --only-deps Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../docs/pages/changelog/2026-09-01.mdx | 7 ++ .../pages/navigation/site-level-settings.mdx | 2 + .../pages/self-hosted/self-hosted-set-up.mdx | 105 +++++++----------- 3 files changed, 52 insertions(+), 62 deletions(-) create mode 100644 fern/products/docs/pages/changelog/2026-09-01.mdx diff --git a/fern/products/docs/pages/changelog/2026-09-01.mdx b/fern/products/docs/pages/changelog/2026-09-01.mdx new file mode 100644 index 0000000000..801d0b9424 --- /dev/null +++ b/fern/products/docs/pages/changelog/2026-09-01.mdx @@ -0,0 +1,7 @@ +## Per-deployment values in self-hosted docs + +self-hosted, customization + +A self-hosted container can now resolve environment variables in the pages it serves, on each request. Values that differ per deployment stay in the generated site as placeholders, so one image serves every environment instead of being rebuilt for each. Only names listed in `FERN_RUNTIME_ENV_VARS` are resolved. + + diff --git a/fern/products/docs/pages/navigation/site-level-settings.mdx b/fern/products/docs/pages/navigation/site-level-settings.mdx index d58f9b40aa..03039d3194 100644 --- a/fern/products/docs/pages/navigation/site-level-settings.mdx +++ b/fern/products/docs/pages/navigation/site-level-settings.mdx @@ -830,6 +830,8 @@ settings: To output a literal `${VAR}`, escape it as `\$\{VAR\}`. + A [self-hosted](/learn/docs/self-hosted/set-up#per-deployment-values) container can resolve a variable on each request instead, so one image serves deployments that differ only in those values. + During local preview (`fern docs dev`), undefined variables resolve to empty strings. During publishing, undefined variables cause the build to fail. diff --git a/fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx b/fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx index 532e2315e1..4094480ca5 100644 --- a/fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx +++ b/fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx @@ -76,7 +76,7 @@ RUN fern-generate ``` -`fern-generate` is a command available inside the Docker image that renders your documentation to static HTML at build time, enabling faster container startup, air-gapped deployment, and a smaller attack surface. It's not a command you run on your host machine. You can alternatively [defer generation to runtime](#runtime-generation). +`fern-generate` is a command available inside the Docker image that renders your documentation to static HTML at build time, enabling faster container startup, air-gapped deployment, and a smaller attack surface. It's not a command you run on your host machine. @@ -140,6 +140,7 @@ Configure the self-hosted container's behavior by setting environment variables | Variable | Description | Default | |---|---|---| | `CUSTOM_DOMAIN` | Override the `custom-domain` from `docs.yml` at runtime. Useful when the hostname where the docs are actually served differs from the domain in `docs.yml`. Accepts a bare hostname (e.g., `docs.plantstore.dev`); any `https://` or `http://` prefix is stripped automatically. | Value from `docs.yml` `custom-domain` | +| `FERN_RUNTIME_ENV_VARS` | Comma-separated list of variable names the container resolves in served content on each request. See [Per-deployment values](#per-deployment-values). | none | | `FERN_LOG_LEVEL` | Log level for the Fern CLI during docs generation. Options: `debug`, `info`, `warn`, `error`. | `debug` | | `NEXT_PUBLIC_BASE_PATH` | Override the base path inferred from your `docs.yml` sub-path, or serve from a sub-path without configuring `docs.yml`. The value must start with `/` and have no trailing slash (e.g., `/docs`). See [Base path](#base-path) for details. | Inferred from `docs.yml` sub-path (else serves from `/`) | @@ -247,35 +248,60 @@ Pass `NEXT_PUBLIC_BASE_PATH` when starting the container. docker run -p 3000:3000 -e NEXT_PUBLIC_BASE_PATH=/docs self-hosted-docs ``` -The base path is compiled into every URL of the static site, so a runtime base path that differs from the one the image was built with re-renders the site at startup. Re-rendering writes into the container filesystem, so it's not compatible with `readOnlyRootFilesystem: true` in Kubernetes, and it requires the image to retain the site builder: build with `FERN_KEEP_BUILD_TOOLS=1` or defer generation to runtime. +The base path is compiled into every URL of the static site, so a runtime base path that differs from the one the image was built with re-renders the site at startup. Re-rendering writes into the container filesystem, so it's not compatible with `readOnlyRootFilesystem: true` in Kubernetes, and it requires the image to retain the site builder: build with `FERN_KEEP_BUILD_TOOLS=1`. An image that retains the site builder can be re-rendered at startup for any base path, letting you reuse one image across environments that need different base paths. -### Runtime generation +### Per-deployment values -By default, `fern-generate` runs at Docker build time. Defer generation to runtime if you need to: -- Pass configuration (environment variables, secrets) at runtime -- Speed up Docker builds during development -- Share a single image across multiple documentation configurations +One image can serve several environments. A value that differs per deployment, such as an app server hostname, stays in the generated site as a `FERN_SELF_HOSTED_ENV_` placeholder and is resolved on each request, so a new environment costs no rebuild. -Use the `--only-deps` flag to defer generation to runtime: +Write the placeholder in `docs.yml` or in page content. Generation leaves it untouched: -```dockerfile -FROM fernenterprise/fern-self-hosted:latest +```mdx +App server: FERN_SELF_HOSTED_ENV_APP_SERVER -COPY fern/ /fern/ +Open the app +``` + +A placeholder in a link target holds a host rather than a full URL and needs a scheme in front of it, since a target that doesn't look absolute resolves against the docs site. When the deployment's value carries its own scheme (`APP_SERVER=https://app.plantstore.dev`), the container drops the one in the placeholder. + +Allowlist the names the container may resolve with `FERN_RUNTIME_ENV_VARS`, and supply the values at deploy time: -RUN fern-generate --only-deps +```dockerfile +ENV FERN_RUNTIME_ENV_VARS=APP_SERVER ``` -This starts required services (PostgreSQL, MinIO, FDR) at build time but skips documentation generation. When the container starts, it automatically runs `fern generate --docs`. +```bash +docker run -p 3000:3000 -e APP_SERVER=app.plantstore.dev self-hosted-docs +``` + +Substitution covers every text artifact the container serves: page text and attributes such as `href` and `src`, the [Markdown](/learn/docs/ai-features/markdown) and [`llms.txt`](/learn/docs/ai-features/llms-txt) versions of each page, the sitemap, manifests, and search results. Binary assets are served byte for byte, and the `etag` covers the substituted bytes, so changing a value invalidates caches. + +The allowlist is a security boundary. A name outside it stays in the page as the literal placeholder, so page content can't read arbitrary container environment variables. An allowlisted name with no value in the container is also left as-is, making a missing value visible rather than an empty string. - -Runtime generation requires network access at container startup. For air-gapped deployments, use the default build-time generation. - +#### Keeping `${VAR}` in your sources + +A docs repo that already writes these values as `${VAR}` with [`settings.substitute-env-vars`](/learn/docs/configuration/site-level-settings#settingssubstitute-env-vars) needs no edit. Generation reads the same `FERN_RUNTIME_ENV_VARS` list and rewrites a listed name to its placeholder instead of resolving it, prefixing `https://` where the value stands in a link target. Names outside the list keep resolving at build time, which is what the site's own identity requires: the instance `url` and `custom-domain` are baked into every absolute URL. + +```yaml docs.yml +settings: + substitute-env-vars: true + +instances: + # Resolved at build time. + - url: ${INSTANCE_NAME}.docs.buildwithfern.com + custom-domain: ${CUSTOM_DOMAIN} + +navbar-links: + # Resolved on each request, given FERN_RUNTIME_ENV_VARS=APP_SERVER. + - type: filled + text: Open the app + url: ${APP_SERVER}/login +``` ### Air-gapped deployments with gRPC @@ -326,46 +352,7 @@ RUN fern-generate This downloads BSR dependencies during the Docker build and bakes them into the image. No network access required at runtime. - - -Use this option when you don't have all the information at build time and need the docs to generate differently at runtime, such as injecting environment variables at runtime. - -To generate at runtime in an air-gapped environment, vendor buf dependencies locally: - -```dockerfile -FROM fernenterprise/fern-self-hosted:latest - -# Install buf CLI for dependency caching -RUN npm install -g @bufbuild/buf - -# Copy fern configuration -COPY fern/ fern/ -COPY protos/ protos/ - -# Pre-fetch buf dependencies at build time (caches googleapis, protovalidate) -RUN cd protos && buf dep update - -# Build fern dependencies -RUN fern-generate --only-deps -``` - -Update `buf.yaml` to reference vendored dependencies: - -```yaml -# Before -deps: - - buf.build/googleapis/googleapis - -# After -deps: - - ./vendor/googleapis -``` - - -See the [Buf documentation on dependency management](https://buf.build/docs/bsr/module/dependency-management) for more details. - - - + If you don't have a `buf.yaml` file, you can specify proto dependencies directly in your `generators.yml`. The self-hosted container automatically creates a temporary `buf.yaml` from these dependencies during the build process. @@ -379,18 +366,12 @@ api: - buf.build/bufbuild/protovalidate ``` -This approach works with both build-time and runtime generation: - ```dockerfile FROM fernenterprise/fern-self-hosted:latest COPY fern/ /fern/ -# For build-time generation (recommended for air-gapped deployments) RUN fern-generate - -# Or for runtime generation (requires network at startup) -# RUN fern-generate --only-deps ``` The container parses all `generators.yml` files in your fern directory, finds proto specs with dependencies but no `buf.yaml`, and creates the necessary configuration automatically. From 321553cb226e737362d9c67ad363b00be49cff63 Mon Sep 17 00:00:00 2001 From: "ryan.stephen" Date: Tue, 1 Sep 2026 00:24:23 +0000 Subject: [PATCH 2/7] docs(self-hosted): condense per-deployment values, use plant API example Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../pages/self-hosted/self-hosted-set-up.mdx | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx b/fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx index 4094480ca5..517a0e92f2 100644 --- a/fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx +++ b/fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx @@ -257,50 +257,45 @@ An image that retains the site builder can be re-rendered at startup for any bas ### Per-deployment values -One image can serve several environments. A value that differs per deployment, such as an app server hostname, stays in the generated site as a `FERN_SELF_HOSTED_ENV_` placeholder and is resolved on each request, so a new environment costs no rebuild. +A value that differs per deployment, such as an API hostname, can stay in the generated site as a `FERN_SELF_HOSTED_ENV_` placeholder that the container resolves on each request, so one image serves every environment. -Write the placeholder in `docs.yml` or in page content. Generation leaves it untouched: +Write the placeholder in `docs.yml` or page content, allowlist its name with `FERN_RUNTIME_ENV_VARS`, and set the value when running the container: ```mdx -App server: FERN_SELF_HOSTED_ENV_APP_SERVER +API host: FERN_SELF_HOSTED_ENV_PLANT_API -Open the app +Browse plants ``` -A placeholder in a link target holds a host rather than a full URL and needs a scheme in front of it, since a target that doesn't look absolute resolves against the docs site. When the deployment's value carries its own scheme (`APP_SERVER=https://app.plantstore.dev`), the container drops the one in the placeholder. - -Allowlist the names the container may resolve with `FERN_RUNTIME_ENV_VARS`, and supply the values at deploy time: - ```dockerfile -ENV FERN_RUNTIME_ENV_VARS=APP_SERVER +ENV FERN_RUNTIME_ENV_VARS=PLANT_API ``` ```bash -docker run -p 3000:3000 -e APP_SERVER=app.plantstore.dev self-hosted-docs +docker run -p 3000:3000 -e PLANT_API=api.plantstore.dev self-hosted-docs ``` -Substitution covers every text artifact the container serves: page text and attributes such as `href` and `src`, the [Markdown](/learn/docs/ai-features/markdown) and [`llms.txt`](/learn/docs/ai-features/llms-txt) versions of each page, the sitemap, manifests, and search results. Binary assets are served byte for byte, and the `etag` covers the substituted bytes, so changing a value invalidates caches. +A placeholder in a link target holds a host, so it needs a scheme in front of it; when the value carries its own scheme, the container drops the one in the placeholder. A name outside the allowlist, or an allowlisted name with no value, is served as the literal placeholder, so page content can't read arbitrary container environment variables. -The allowlist is a security boundary. A name outside it stays in the page as the literal placeholder, so page content can't read arbitrary container environment variables. An allowlisted name with no value in the container is also left as-is, making a missing value visible rather than an empty string. +Substitution covers every text artifact the container serves: page text and attributes such as `href` and `src`, the [Markdown](/learn/docs/ai-features/markdown) and [`llms.txt`](/learn/docs/ai-features/llms-txt) versions of each page, the sitemap, manifests, and search results. Binary assets are served byte for byte, and the `etag` covers the substituted bytes. #### Keeping `${VAR}` in your sources -A docs repo that already writes these values as `${VAR}` with [`settings.substitute-env-vars`](/learn/docs/configuration/site-level-settings#settingssubstitute-env-vars) needs no edit. Generation reads the same `FERN_RUNTIME_ENV_VARS` list and rewrites a listed name to its placeholder instead of resolving it, prefixing `https://` where the value stands in a link target. Names outside the list keep resolving at build time, which is what the site's own identity requires: the instance `url` and `custom-domain` are baked into every absolute URL. +Sources that already use `${VAR}` with [`settings.substitute-env-vars`](/learn/docs/configuration/site-level-settings#settingssubstitute-env-vars) need no edit: generation rewrites a name listed in `FERN_RUNTIME_ENV_VARS` to its placeholder instead of resolving it. Names outside the list keep resolving at build time, which the site's own identity requires, since the instance `url` and `custom-domain` are baked into every absolute URL. ```yaml docs.yml settings: substitute-env-vars: true instances: - # Resolved at build time. + # Build time. - url: ${INSTANCE_NAME}.docs.buildwithfern.com - custom-domain: ${CUSTOM_DOMAIN} navbar-links: - # Resolved on each request, given FERN_RUNTIME_ENV_VARS=APP_SERVER. + # Each request, given FERN_RUNTIME_ENV_VARS=PLANT_API. - type: filled - text: Open the app - url: ${APP_SERVER}/login + text: Browse plants + url: ${PLANT_API}/plants ``` ### Air-gapped deployments with gRPC From abf227a4255abcffae2997bff3dc34993fb9691c Mon Sep 17 00:00:00 2001 From: "ryan.stephen" Date: Tue, 1 Sep 2026 00:27:53 +0000 Subject: [PATCH 3/7] docs(self-hosted): lead per-deployment values with ${VAR} sources Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../docs/pages/changelog/2026-09-01.mdx | 2 +- .../pages/self-hosted/self-hosted-set-up.mdx | 42 ++++++++----------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/fern/products/docs/pages/changelog/2026-09-01.mdx b/fern/products/docs/pages/changelog/2026-09-01.mdx index 801d0b9424..31daeb687a 100644 --- a/fern/products/docs/pages/changelog/2026-09-01.mdx +++ b/fern/products/docs/pages/changelog/2026-09-01.mdx @@ -2,6 +2,6 @@ self-hosted, customization -A self-hosted container can now resolve environment variables in the pages it serves, on each request. Values that differ per deployment stay in the generated site as placeholders, so one image serves every environment instead of being rebuilt for each. Only names listed in `FERN_RUNTIME_ENV_VARS` are resolved. +A self-hosted container can now resolve environment variables in the pages it serves, on each request. A `${VAR}` whose name is listed in `FERN_RUNTIME_ENV_VARS` is deferred past generation instead of being substituted with its build-time value, so one image serves deployments that differ only in those values. diff --git a/fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx b/fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx index 517a0e92f2..8825e2109e 100644 --- a/fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx +++ b/fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx @@ -257,46 +257,38 @@ An image that retains the site builder can be re-rendered at startup for any bas ### Per-deployment values -A value that differs per deployment, such as an API hostname, can stay in the generated site as a `FERN_SELF_HOSTED_ENV_` placeholder that the container resolves on each request, so one image serves every environment. +A value that differs per deployment, such as an API hostname, can be resolved on each request instead of at build time, so one image serves every environment. -Write the placeholder in `docs.yml` or page content, allowlist its name with `FERN_RUNTIME_ENV_VARS`, and set the value when running the container: +Keep writing the value as `${VAR}` with [`settings.substitute-env-vars`](/learn/docs/configuration/site-level-settings#settingssubstitute-env-vars), and list its name in `FERN_RUNTIME_ENV_VARS` at build time. Generation rewrites a listed name to a `FERN_SELF_HOSTED_ENV_` placeholder instead of resolving it, and the container substitutes the placeholder from its own environment on every request: -```mdx -API host: FERN_SELF_HOSTED_ENV_PLANT_API +```yaml docs.yml +settings: + substitute-env-vars: true -Browse plants +instances: + # Not listed, so resolved at build time. + - url: ${INSTANCE_NAME}.docs.buildwithfern.com + +navbar-links: + - type: filled + text: Browse plants + url: ${PLANT_API}/plants ``` ```dockerfile ENV FERN_RUNTIME_ENV_VARS=PLANT_API +RUN fern-generate ``` ```bash docker run -p 3000:3000 -e PLANT_API=api.plantstore.dev self-hosted-docs ``` -A placeholder in a link target holds a host, so it needs a scheme in front of it; when the value carries its own scheme, the container drops the one in the placeholder. A name outside the allowlist, or an allowlisted name with no value, is served as the literal placeholder, so page content can't read arbitrary container environment variables. - -Substitution covers every text artifact the container serves: page text and attributes such as `href` and `src`, the [Markdown](/learn/docs/ai-features/markdown) and [`llms.txt`](/learn/docs/ai-features/llms-txt) versions of each page, the sitemap, manifests, and search results. Binary assets are served byte for byte, and the `etag` covers the substituted bytes. - -#### Keeping `${VAR}` in your sources +Site identity resolves at build time whether it's listed or not: the instance `url` and `custom-domain` are baked into every absolute URL. A `${VAR}` in a link target is rewritten with an `https://` prefix so the link stays absolute, and if the runtime value carries its own scheme, the container drops the one in the placeholder. -Sources that already use `${VAR}` with [`settings.substitute-env-vars`](/learn/docs/configuration/site-level-settings#settingssubstitute-env-vars) need no edit: generation rewrites a name listed in `FERN_RUNTIME_ENV_VARS` to its placeholder instead of resolving it. Names outside the list keep resolving at build time, which the site's own identity requires, since the instance `url` and `custom-domain` are baked into every absolute URL. +Substitution covers every text artifact the container serves: page text and attributes such as `href` and `src`, the [Markdown](/learn/docs/ai-features/markdown) and [`llms.txt`](/learn/docs/ai-features/llms-txt) versions of each page, the sitemap, manifests, and search results. Binary assets are served byte for byte, and the `etag` covers the substituted bytes. A name outside `FERN_RUNTIME_ENV_VARS`, or a listed name with no value in the container, is served as the literal placeholder, so page content can't read arbitrary container environment variables. -```yaml docs.yml -settings: - substitute-env-vars: true - -instances: - # Build time. - - url: ${INSTANCE_NAME}.docs.buildwithfern.com - -navbar-links: - # Each request, given FERN_RUNTIME_ENV_VARS=PLANT_API. - - type: filled - text: Browse plants - url: ${PLANT_API}/plants -``` +Writing `FERN_SELF_HOSTED_ENV_` directly in page content works too, for sources that don't use `${VAR}` substitution. ### Air-gapped deployments with gRPC From 115e4a38d78cedd11e3c33fb7816862b06db6145 Mon Sep 17 00:00:00 2001 From: "ryan.stephen" Date: Tue, 1 Sep 2026 14:17:22 +0000 Subject: [PATCH 4/7] docs(self-hosted): drop 'keep writing' framing Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx b/fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx index 8825e2109e..b522e8e1b1 100644 --- a/fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx +++ b/fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx @@ -259,7 +259,7 @@ An image that retains the site builder can be re-rendered at startup for any bas A value that differs per deployment, such as an API hostname, can be resolved on each request instead of at build time, so one image serves every environment. -Keep writing the value as `${VAR}` with [`settings.substitute-env-vars`](/learn/docs/configuration/site-level-settings#settingssubstitute-env-vars), and list its name in `FERN_RUNTIME_ENV_VARS` at build time. Generation rewrites a listed name to a `FERN_SELF_HOSTED_ENV_` placeholder instead of resolving it, and the container substitutes the placeholder from its own environment on every request: +Write the value as `${VAR}` with [`settings.substitute-env-vars`](/learn/docs/configuration/site-level-settings#settingssubstitute-env-vars), and list its name in `FERN_RUNTIME_ENV_VARS` at build time. Generation rewrites a listed name to a `FERN_SELF_HOSTED_ENV_` placeholder instead of resolving it, and the container substitutes the placeholder from its own environment on every request: ```yaml docs.yml settings: @@ -288,7 +288,7 @@ Site identity resolves at build time whether it's listed or not: the instance `u Substitution covers every text artifact the container serves: page text and attributes such as `href` and `src`, the [Markdown](/learn/docs/ai-features/markdown) and [`llms.txt`](/learn/docs/ai-features/llms-txt) versions of each page, the sitemap, manifests, and search results. Binary assets are served byte for byte, and the `etag` covers the substituted bytes. A name outside `FERN_RUNTIME_ENV_VARS`, or a listed name with no value in the container, is served as the literal placeholder, so page content can't read arbitrary container environment variables. -Writing `FERN_SELF_HOSTED_ENV_` directly in page content works too, for sources that don't use `${VAR}` substitution. +Page content can also spell out `FERN_SELF_HOSTED_ENV_` directly, for sources that don't use `${VAR}` substitution. ### Air-gapped deployments with gRPC From 476ed228a0a724c69c67947e76bc9d4c24542b91 Mon Sep 17 00:00:00 2001 From: "ryan.stephen" Date: Tue, 1 Sep 2026 14:32:30 +0000 Subject: [PATCH 5/7] docs(self-hosted): trim per-deployment values details Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx b/fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx index b522e8e1b1..ee282c95c2 100644 --- a/fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx +++ b/fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx @@ -284,9 +284,9 @@ RUN fern-generate docker run -p 3000:3000 -e PLANT_API=api.plantstore.dev self-hosted-docs ``` -Site identity resolves at build time whether it's listed or not: the instance `url` and `custom-domain` are baked into every absolute URL. A `${VAR}` in a link target is rewritten with an `https://` prefix so the link stays absolute, and if the runtime value carries its own scheme, the container drops the one in the placeholder. +The instance `url` and `custom-domain` always resolve at build time, since they're baked into every absolute URL. Every other text artifact the container serves is substituted, including the [Markdown](/learn/docs/ai-features/markdown) and [`llms.txt`](/learn/docs/ai-features/llms-txt) versions of each page and search results. The runtime value can be a bare hostname or carry its own scheme. -Substitution covers every text artifact the container serves: page text and attributes such as `href` and `src`, the [Markdown](/learn/docs/ai-features/markdown) and [`llms.txt`](/learn/docs/ai-features/llms-txt) versions of each page, the sitemap, manifests, and search results. Binary assets are served byte for byte, and the `etag` covers the substituted bytes. A name outside `FERN_RUNTIME_ENV_VARS`, or a listed name with no value in the container, is served as the literal placeholder, so page content can't read arbitrary container environment variables. +A name missing from `FERN_RUNTIME_ENV_VARS`, or listed with no value in the container, renders as the literal placeholder instead of an empty string. Page content can also spell out `FERN_SELF_HOSTED_ENV_` directly, for sources that don't use `${VAR}` substitution. From 64c6ecbc2e940ed6255719eb144795db232e5ca3 Mon Sep 17 00:00:00 2001 From: "ryan.stephen" Date: Tue, 1 Sep 2026 14:44:59 +0000 Subject: [PATCH 6/7] docs(self-hosted): clarify runtime value scheme Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx b/fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx index ee282c95c2..2efcdd1a53 100644 --- a/fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx +++ b/fern/products/docs/pages/self-hosted/self-hosted-set-up.mdx @@ -284,7 +284,7 @@ RUN fern-generate docker run -p 3000:3000 -e PLANT_API=api.plantstore.dev self-hosted-docs ``` -The instance `url` and `custom-domain` always resolve at build time, since they're baked into every absolute URL. Every other text artifact the container serves is substituted, including the [Markdown](/learn/docs/ai-features/markdown) and [`llms.txt`](/learn/docs/ai-features/llms-txt) versions of each page and search results. The runtime value can be a bare hostname or carry its own scheme. +The instance `url` and `custom-domain` always resolve at build time, since they're baked into every absolute URL. Every other text artifact the container serves is substituted, including the [Markdown](/learn/docs/ai-features/markdown) and [`llms.txt`](/learn/docs/ai-features/llms-txt) versions of each page and search results. The runtime value can include a scheme (`https://api.plantstore.dev`) or omit it (`api.plantstore.dev`). A name missing from `FERN_RUNTIME_ENV_VARS`, or listed with no value in the container, renders as the literal placeholder instead of an empty string. From 516591be956e75f385c96c8ea5fb54d905e56968 Mon Sep 17 00:00:00 2001 From: "ryan.stephen" Date: Tue, 1 Sep 2026 14:45:55 +0000 Subject: [PATCH 7/7] docs: drop self-hosted cross-reference from site-level settings Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- fern/products/docs/pages/navigation/site-level-settings.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/fern/products/docs/pages/navigation/site-level-settings.mdx b/fern/products/docs/pages/navigation/site-level-settings.mdx index 03039d3194..d58f9b40aa 100644 --- a/fern/products/docs/pages/navigation/site-level-settings.mdx +++ b/fern/products/docs/pages/navigation/site-level-settings.mdx @@ -830,8 +830,6 @@ settings: To output a literal `${VAR}`, escape it as `\$\{VAR\}`. - A [self-hosted](/learn/docs/self-hosted/set-up#per-deployment-values) container can resolve a variable on each request instead, so one image serves deployments that differ only in those values. - During local preview (`fern docs dev`), undefined variables resolve to empty strings. During publishing, undefined variables cause the build to fail.