Set up self-hosted documentation

View as Markdown
Enterprise feature

This feature is available only for the Enterprise plan. To get started, reach out to support@buildwithfern.com.

Prerequisites

Before setting up self-hosted documentation, ensure you have:

  • Docker installed on your system
  • Access to your Fern project’s fern/ directory
  • A Docker Hub organization access token (OAT) from Fern (for pulling the private image)

Setup instructions

1

Authenticate with Docker Hub

The self-hosted documentation runs on a private Docker image: fernenterprise/fern-self-hosted

Contact Fern Support to receive a Docker Hub organization access token (OAT).

Log in to Docker Hub using the token provided by Fern:

$docker login --username fernenterprise

When prompted for a password, enter the OAT provided by the Fern team.

In CI, pass the token via the DOCKERHUB_OAT environment variable:

$echo "$DOCKERHUB_OAT" | docker login --username fernenterprise --password-stdin
2

Download the Docker image

Pull the image:

$docker pull fernenterprise/fern-self-hosted:latest

Verify the image is available in your Docker daemon:

$docker images | grep fernenterprise/fern-self-hosted
3

Create a Dockerfile

In the same directory that contains your fern/ folder, create a file named Dockerfile:

your-project/
├── Dockerfile
└── fern/
├── fern.config.json
├── docs.yml
└── ...

Add the following content to the Dockerfile:

Dockerfile
1FROM fernenterprise/fern-self-hosted:latest
2
3COPY fern/ /fern/
4
5RUN fern-generate

fern-generate is a command available inside the Docker image that processes your documentation 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.

4

Build your Docker image

From the directory containing your Dockerfile and fern/ folder, build the image:

$docker build -t self-hosted-docs .
5

Run the documentation

Start your self-hosted documentation:

$docker run -p 3000:3000 self-hosted-docs

The documentation will be available at localhost:3000.

6

Deploy the documentation

You can now deploy the image to your own infrastructure, allowing you to host the documentation on your own domain.

Once deployed, you can set up preview environments to preview documentation changes on every pull request.

Custom domain

Your documentation uses the domain specified in your docs.yml file. For example:

1instances:
2 - url: example-org.docs.buildwithfern.com
3 custom-domain: docs.plantstore.dev

To override the domain at runtime (for example, when the actual hostname differs from the custom-domain in docs.yml), set the CUSTOM_DOMAIN environment variable:

$docker run -p 3000:3000 -e CUSTOM_DOMAIN=docs.plantstore.dev self-hosted-docs

See Environment variables for details.

Environment variables

Configure the self-hosted container’s behavior by setting environment variables in your Dockerfile or Kubernetes deployment.

General

VariableDescriptionDefault
CUSTOM_DOMAINOverride 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_LOG_LEVELLog level for the Fern CLI during docs generation. Options: debug, info, warn, error.debug
NODE_MEMORY_LIMITNode.js heap size in MB for the Next.js server. Increase for large documentation sites with many API versions.4096
NEXT_PUBLIC_BASE_PATHServe the documentation from a sub-path instead of root. The value must start with / and have no trailing slash (e.g., /docs). See Base path for details.none (serves from /)

Cache warmup

The container can pre-fetch all pages on startup to ensure the first real user request is fast.

VariableDescriptionDefault
WARMUPSet to true to enable cache warmup on startup. Runs in the background and doesn’t block container readiness.false
WARMUP_TIMEOUTTimeout in seconds for each page request during warmup.5

Cache proxy

The container includes a caching proxy that sits in front of the Next.js server.

VariableDescriptionDefault
CACHE_MAX_ENTRIESMaximum number of pages to cache.1000
CACHE_MAX_ENTRY_SIZEMaximum size per cached entry in bytes.5242880 (5 MB)
CACHE_DEFAULT_TTLDefault cache time to live (TTL) in seconds.2592000 (30 days)
CACHE_CDN_TTLCache TTL in seconds for downstream CDN caches (e.g., CloudFront).3600 (1 hour)
CACHE_DISABLEDSet to true or 1 to disable caching entirely.false
CACHE_PROXY_DEBUGSet to 1 for verbose cache proxy logging.0

Cross-origin resource sharing (CORS) proxy

The container includes a CORS proxy that allows the documentation frontend to make cross-origin requests (e.g., to your API for the API Explorer’s Try it feature). By default, only the docs domain itself is allowed. Use CORS_PROXY_ALLOWED_DOMAINS to allowlist additional domains.

VariableDescriptionDefault
CORS_PROXY_ALLOWED_DOMAINSComma-separated list of root domains to allow through the CORS proxy. Subdomains are matched automatically.none

For example, to allow requests to api.plantstore.dev and auth.plantstore.dev:

1ENV CORS_PROXY_ALLOWED_DOMAINS="plantstore.dev"

To allow multiple domains:

1ENV CORS_PROXY_ALLOWED_DOMAINS="plantstore.dev,partner-api.example.com"

Debugging

VariableDescriptionDefault
ENABLE_JAEGERSet to true to start Jaeger for distributed tracing. The Jaeger UI is available on port 16686.false

On-page feedback

In self-hosted mode, on-page feedback events are emitted as structured JSON logs to the container’s stdout, prefixed with [fern-docs-feedback]. You can filter for these in your logging infrastructure:

$docker logs <container-id> 2>&1 | grep "\[fern-docs-feedback\]"

Each log line contains an event name, a timestamp, and a set of properties:

1[fern-docs-feedback] {"event":"feedback_submitted","timestamp":"2026-01-15T12:34:56.789Z","properties":{"satisfied":true,"message":"Great docs!","email":"user@example.com","type":"on-page-feedback"}}

Tracked events

EventDescription
feedback_votedA user clicked the thumbs up or thumbs down button.
feedback_submittedA user submitted written feedback via the feedback form.
code_block_feedback_submittedA user reported an issue with a code example.

Properties

PropertyDescription
satisfiedtrue for thumbs up, false for thumbs down.
messageThe user’s written feedback message (present in feedback_submitted and code_block_feedback_submitted events).
emailThe user’s email address, if provided.
typeThe feedback source, such as on-page-feedback.

Additional configuration

The following sections cover optional configurations for specific deployment scenarios.

Base path

By default, the self-hosted container serves documentation from root (/). Set NEXT_PUBLIC_BASE_PATH to serve from a sub-path instead, such as /docs. This is useful when your documentation shares a domain with other applications behind a reverse proxy.

The value must start with / and must not end with a trailing slash.

With NEXT_PUBLIC_BASE_PATH=/docs, the documentation is accessible at http://localhost:3000/docs instead of http://localhost:3000/.

A single Docker image built without NEXT_PUBLIC_BASE_PATH can be configured at runtime to serve from any path. This lets you reuse one image across environments that may need different base paths.

Runtime generation

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

Use the --only-deps flag to defer generation to runtime:

1FROM fernenterprise/fern-self-hosted:latest
2
3COPY fern/ /fern/
4
5RUN fern-generate --only-deps

This starts required services (PostgreSQL, MinIO, FDR) at build time but skips documentation generation. When the container starts, it automatically runs fern generate --docs.

Runtime generation requires network access at container startup. For air-gapped deployments, use the default build-time generation.

Air-gapped deployments with gRPC

If your API uses gRPC with dependencies from the Buf Schema Registry (BSR), the buf CLI fetches modules from buf.build during generation. This fails in air-gapped environments without network access.

1

Check for BSR dependencies

Check if your project has BSR dependencies in either location:

In buf.yaml:

1version: v2
2deps:
3 - buf.build/googleapis/googleapis
4 - buf.build/grpc-ecosystem/grpc-gateway

In generators.yml:

1api:
2 specs:
3 - proto:
4 root: ./protos/
5 dependencies:
6 - buf.build/googleapis/googleapis

If there’s no deps or dependencies section (or only local paths), you can skip the rest of this section.

2

Choose a solution

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:

1FROM fernenterprise/fern-self-hosted:latest
2
3# Install buf CLI for dependency caching
4RUN npm install -g @bufbuild/buf
5
6# Copy fern configuration
7COPY fern/ fern/
8COPY protos/ protos/
9
10# Pre-fetch buf dependencies at build time (caches googleapis, protovalidate)
11RUN cd protos && buf dep update
12
13# Build fern dependencies
14RUN fern-generate --only-deps

Update buf.yaml to reference vendored dependencies:

1# Before
2deps:
3 - buf.build/googleapis/googleapis
4
5# After
6deps:
7 - ./vendor/googleapis

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.

generators.yml
1api:
2 specs:
3 - proto:
4 root: ./protos/
5 dependencies:
6 - buf.build/googleapis/googleapis
7 - buf.build/bufbuild/protovalidate

This approach works with both build-time and runtime generation:

1FROM fernenterprise/fern-self-hosted:latest
2
3COPY fern/ /fern/
4
5# For build-time generation (recommended for air-gapped deployments)
6RUN fern-generate
7
8# Or for runtime generation (requires network at startup)
9# 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.

Kubernetes deployment

Here is a sample Deployment and Service configuration. Replace your-registry/fern-docs:latest with your image name.

Apply the configuration:

$kubectl apply -f deployment.yaml
$kubectl apply -f service.yaml

deployment.yaml:

deployment.yaml
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: fern-docs
5 labels:
6 app: fern-docs
7spec:
8 replicas: 1
9 selector:
10 matchLabels:
11 app: fern-docs
12 template:
13 metadata:
14 labels:
15 app: fern-docs
16 spec:
17 securityContext:
18 runAsNonRoot: true
19 runAsUser: 65532
20 runAsGroup: 65532
21 fsGroup: 65532
22 fsGroupChangePolicy: OnRootMismatch
23 containers:
24 - name: fern-docs
25 image: your-registry/fern-docs:latest
26 imagePullPolicy: IfNotPresent
27 ports:
28 - name: docs
29 containerPort: 3000
30 protocol: TCP
31 - name: health
32 containerPort: 8081
33 protocol: TCP
34 resources:
35 requests:
36 memory: "2Gi"
37 cpu: "1000m"
38 limits:
39 memory: "4Gi"
40 cpu: "2000m"
41 livenessProbe:
42 httpGet:
43 path: /liveness
44 port: 8081
45 initialDelaySeconds: 120
46 periodSeconds: 10
47 timeoutSeconds: 5
48 failureThreshold: 3
49 readinessProbe:
50 httpGet:
51 path: /readiness
52 port: 8081
53 initialDelaySeconds: 60
54 periodSeconds: 5
55 timeoutSeconds: 5
56 failureThreshold: 6
57 securityContext:
58 allowPrivilegeEscalation: false
59 runAsNonRoot: true
60 runAsUser: 65532
61 runAsGroup: 65532
62 privileged: false
63 readOnlyRootFilesystem: false
64 capabilities:
65 drop:
66 - ALL
67 terminationGracePeriodSeconds: 30

service.yaml:

service.yaml
1apiVersion: v1
2kind: Service
3metadata:
4 name: fern-docs
5 labels:
6 app: fern-docs
7spec:
8 type: NodePort
9 ports:
10 - name: http
11 port: 80
12 targetPort: 3000
13 protocol: TCP
14 nodePort: 30080
15 selector:
16 app: fern-docs

For health check endpoint details, see Health check endpoints.