Previews

View as Markdown
Enterprise feature

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

There are two ways to set up preview environments for your self-hosted docs:

  • Container-based — deploys the same self-hosted Docker container you use in production, giving you a full-fidelity preview with search, API Explorer, and authentication.
  • Static export — renders your docs to static HTML and assets that can be served from any object store (S3, GCS, R2), with no running containers required.

Both approaches render content exactly as it would appear in production.

Choosing an approach

For most teams, static export is the best starting point because of its minimal infrastructure and setup. Choose container-based if you need search, API Explorer, or authentication in your previews.

Container-basedStatic export Beta
Content renderingIdentical to productionIdentical to production
SearchYesNo
API Explorer (Try it)YesNo
AuthenticationYesNo
InfrastructureRequires container hosting, load balancer, DNSServes from any object store (S3, GCS, R2)
ScalingOne container per previewThousands of previews with no servers
CostHigherLow
CleanupMust tear down containers and resourcesSimple — delete static files

GitHub Actions workflows

The following workflows can be added to your repository to automatically build and deploy preview environments on every pull request.

This workflow builds the Docker image on each pull request and deploys it to your container hosting platform.

.github/workflows/preview-docs-container.yml
1name: preview-docs-container
2
3on:
4 pull_request:
5
6jobs:
7 preview-docs:
8 runs-on: ubuntu-latest
9 permissions: write-all
10 steps:
11 - name: Checkout repository
12 uses: actions/checkout@v4
13
14 - name: Log in to Docker Hub
15 uses: docker/login-action@v3
16 with:
17 username: ${{ secrets.DOCKERHUB_USERNAME }}
18 password: ${{ secrets.DOCKERHUB_TOKEN }}
19
20 - name: Build docs container
21 run: docker build -t self-hosted-docs:${{ github.sha }} .
22
23 - name: Push container to registry
24 run: |
25 docker tag self-hosted-docs:${{ github.sha }} ${{ secrets.REGISTRY }}/self-hosted-docs:${{ github.sha }}
26 docker push ${{ secrets.REGISTRY }}/self-hosted-docs:${{ github.sha }}
27
28 - name: Deploy preview
29 run: |
30 # Deploy the container to your hosting platform
31 # (e.g. ECS, Cloud Run, Kubernetes, etc.)
32 # and retrieve the preview URL
33 echo "Deploy self-hosted-docs:${{ github.sha }} to your platform"

This workflow builds the container, runs the static export, and uploads the output to S3. Adapt the upload step for your object store.

.github/workflows/preview-docs-static.yml
1name: preview-docs-static
2
3on:
4 pull_request:
5
6jobs:
7 preview-docs:
8 runs-on: ubuntu-latest
9 permissions: write-all
10 steps:
11 - name: Checkout repository
12 uses: actions/checkout@v4
13
14 - name: Log in to Docker Hub
15 uses: docker/login-action@v3
16 with:
17 username: ${{ secrets.DOCKERHUB_USERNAME }}
18 password: ${{ secrets.DOCKERHUB_TOKEN }}
19
20 - name: Build docs container
21 run: docker build -t self-hosted-docs .
22
23 - name: Start container
24 run: |
25 docker run -d \
26 --name docs-container \
27 -e WARMUP=true \
28 self-hosted-docs
29
30 - name: Wait for container to be healthy
31 run: |
32 echo "Waiting for container to be healthy..."
33 MAX_ATTEMPTS=60
34 ATTEMPT=0
35 while [ "$ATTEMPT" -lt "$MAX_ATTEMPTS" ]; do
36 ATTEMPT=$((ATTEMPT + 1))
37 if docker exec docs-container sh -c \
38 'TOKEN=$(cat /tmp/.cache-admin-token 2>/dev/null); \
39 curl -f -s --max-time 5 \
40 -H "Authorization: Bearer $TOKEN" \
41 http://localhost:3000/__cache/stats' > /dev/null 2>&1; then
42 echo "Container is healthy."
43 break
44 fi
45 echo " Not ready yet... ($ATTEMPT/$MAX_ATTEMPTS)"
46 sleep 5
47 done
48 if [ "$ATTEMPT" -ge "$MAX_ATTEMPTS" ]; then
49 echo "Error: container did not become healthy."
50 exit 1
51 fi
52
53 - name: Export static site
54 run: |
55 docker exec docs-container /scripts/export.sh
56 docker cp docs-container:/tmp/fern-static-export.tar.gz ./export.tar.gz
57
58 - name: Extract static files
59 run: |
60 mkdir -p ./site
61 tar -xzf export.tar.gz -C ./site
62
63 - name: Upload to S3
64 uses: jakejarvis/s3-sync-action@v0.5.1
65 with:
66 args: --delete
67 env:
68 SOURCE_DIR: ./site
69 AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
70 AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
71 AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
72 AWS_REGION: ${{ secrets.AWS_REGION }}
73 DEST_DIR: pr-${{ github.event.pull_request.number }}
74
75 - name: Comment preview URL on PR
76 if: github.event_name == 'pull_request'
77 uses: thollander/actions-comment-pull-request@v3
78 with:
79 message: |
80 Preview: http://${{ secrets.AWS_S3_BUCKET }}.s3-website.${{ secrets.AWS_REGION }}.amazonaws.com/pr-${{ github.event.pull_request.number }}
81 pr-number: ${{ github.event.pull_request.number }}
82
83 - name: Stop container
84 if: always()
85 run: docker rm -f docs-container