DevOpsil

Backstage TechDocs Not Rendering: Fixing MkDocs Build Failures And Missing Plugin Errors

Majid Iqbal NayyarMajid Iqbal Nayyar15 min read

Backstage TechDocs Not Rendering: Fixing MkDocs Build Failures and Missing Plugin Errors

TechDocs is one of the most powerful features in Backstage — the idea of docs-as-code, living right next to your services in a unified developer portal, is genuinely compelling. And then you try to get it working in production, and suddenly you're staring at a blank page, a cryptic MkDocs error, or a plugin import that refuses to resolve.

I've spent more time than I'd like to admit debugging TechDocs pipelines across different organizations, and the failure modes are remarkably consistent. This guide is the definitive reference I wish had existed when I started — covering MkDocs build failures, missing plugin errors, storage backend issues, and the subtle configuration mistakes that silently break everything.

Let's get into it.


Understanding the TechDocs Architecture First

Before debugging anything, you need a clear mental model of what's actually happening when TechDocs renders documentation.

The pipeline looks like this:

Source Repository
  catalog-info.yaml (TechDocs annotation)
  TechDocs Builder (techdocs-cli or Backstage backend)
  MkDocs Build Process
  ├── mkdocs.yml (config)
  ├── docs/ directory
  └── plugins (mkdocs-material, etc.)
  Static Site Output (site/)
  Storage Backend (S3, GCS, Azure Blob, or local)
  TechDocs Reader (Backstage frontend)

There are two build modes:

  • local mode: Backstage builds docs on-demand when a user views them. Simple to set up, terrible for production.
  • external mode (recommended): A CI/CD pipeline or the techdocs-cli builds and publishes docs to object storage. Backstage just reads from storage.

Most production issues come from one of three places: the MkDocs build itself, the plugin environment, or the storage/serving layer. We'll tackle all three.


Setting Up Your Environment Correctly

The root cause of 80% of TechDocs issues I've seen is environment inconsistency. The docs build fine on your laptop, fail in CI, and nobody understands why.

The Container Approach (Use This)

Stop fighting environment drift. Use the official techdocs-container image for all builds:

# .github/workflows/techdocs.yml
name: Publish TechDocs

on:
  push:
    branches: [main]
    paths:
      - 'docs/**'
      - 'mkdocs.yml'

jobs:
  publish-techdocs:
    runs-on: ubuntu-latest
    
    env:
      TECHDOCS_S3_BUCKET_NAME: ${{ secrets.TECHDOCS_S3_BUCKET_NAME }}
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      AWS_REGION: us-east-1
      ENTITY_NAMESPACE: default
      ENTITY_KIND: Component
      ENTITY_NAME: my-service
    
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      
      - name: Publish TechDocs
        run: |
          docker run \
            -e AWS_ACCESS_KEY_ID \
            -e AWS_SECRET_ACCESS_KEY \
            -e AWS_REGION \
            -v $(pwd):/content \
            spotify/techdocs-container:latest \
            techdocs-cli publish \
              --publisher-type awsS3 \
              --storage-name $TECHDOCS_S3_BUCKET_NAME \
              --entity $ENTITY_NAMESPACE/$ENTITY_KIND/$ENTITY_NAME \
              --directory /content

When You Can't Use the Container

If you're stuck building locally or in an environment where Docker isn't available, pin your versions explicitly:

# requirements.txt for TechDocs
mkdocs==1.5.3
mkdocs-material==9.4.14
mkdocs-techdocs-core==1.3.3
pymdown-extensions==10.5

Then install and build:

pip install -r requirements.txt
techdocs-cli generate --no-docker --source-dir . --output-dir ./site

The --no-docker flag is critical here. Without it, techdocs-cli will try to spin up the container and fail if Docker isn't available.


Diagnosing MkDocs Build Failures

Let's go through the most common failure categories with specific error messages and their fixes.

Failure 1: Config file 'mkdocs.yml' does not exist

This one is deceptively simple but bites people constantly. The error looks like:

ERROR    -  Config file 'mkdocs.yml' does not exist.

Causes:

  • You're running the build command from the wrong directory
  • The file is named mkdocs.yaml (with an a) instead of mkdocs.yml
  • The catalog-info.yaml annotation points to the wrong path

Check your catalog-info.yaml:

# catalog-info.yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: my-service
  annotations:
    backstage.io/techdocs-ref: dir:.  # This means mkdocs.yml is in the same directory

If your docs are in a subdirectory:

annotations:
  backstage.io/techdocs-ref: dir:./docs-site  # mkdocs.yml lives in docs-site/

The dir: prefix means "the path to the directory containing mkdocs.yml." Not the path to mkdocs.yml itself. This trips people up constantly.

Failure 2: Missing Plugin Errors

This is the most common production failure I encounter. The error looks like:

ERROR    -  Config value 'plugins': The "search" plugin is not installed
ERROR    -  Config value 'plugins': The "techdocs-core" plugin is not installed

Or the more specific import error:

ModuleNotFoundError: No module named 'mkdocs_material'

The fix depends on your build mode.

If you're using local build mode, you need to install plugins in the Backstage backend environment:

# In your Backstage backend Dockerfile
FROM node:18-bookworm-slim

# Install Python and MkDocs dependencies
RUN apt-get update && apt-get install -y python3 python3-pip && \
    pip3 install mkdocs mkdocs-material mkdocs-techdocs-core --break-system-packages

# ... rest of your Backstage setup

If you're using external build mode (you should be), install them in your CI environment or use the container.

Failure 3: Plugin Version Conflicts

This one is subtle. Your mkdocs.yml specifies plugins, they're installed, but the build still fails:

ERROR    -  Plugin 'awesome-pages' is already installed but version 2.8.0 
            is required, found 2.7.0.

Or worse, no error at all but the output is broken because of incompatible plugin versions.

Here's a mkdocs.yml configuration that I've validated works reliably with the TechDocs core plugin:

# mkdocs.yml
site_name: My Service Documentation
site_description: Documentation for My Service

docs_dir: docs
site_dir: site

theme:
  name: material
  palette:
    - scheme: default
      primary: indigo
      accent: indigo
  features:
    - navigation.tabs
    - navigation.sections
    - navigation.top
    - search.highlight
    - content.code.copy

plugins:
  - techdocs-core  # This MUST be first
  - search
  # Add additional plugins after techdocs-core

markdown_extensions:
  - admonition
  - pymdownx.details
  - pymdownx.superfences:
      custom_fences:
        - name: mermaid
          class: mermaid
          format: !!python/name:pymdownx.superfences.fence_code_format
  - pymdownx.tabbed:
      alternate_style: true
  - pymdownx.highlight:
      anchor_linenums: true
  - attr_list
  - md_in_html
  - tables
  - footnotes

nav:
  - Home: index.md
  - Getting Started: getting-started.md
  - Architecture: architecture.md
  - API Reference: api-reference.md

Critical note: techdocs-core must be the first plugin listed. It sets up the base theme and configuration that other plugins depend on. Putting it second or third causes bizarre rendering issues that are nearly impossible to trace.

Failure 4: Docs Directory Structure Problems

WARNING  -  Documentation file 'api-reference.md' is not found at docs/api-reference.md

Your nav section in mkdocs.yml references files that don't exist. This is a warning, not an error, which means the build succeeds but pages are missing. TechDocs will serve whatever built successfully, leaving you with silent missing content.

# Quick validation script - run this before committing
#!/bin/bash

DOCS_DIR="docs"
MKDOCS_YML="mkdocs.yml"

echo "Validating nav entries against docs directory..."

# Extract nav file references and check they exist
python3 - <<EOF
import yaml
import os
import sys

with open('$MKDOCS_YML', 'r') as f:
    config = yaml.safe_load(f)

nav = config.get('nav', [])
missing = []

def check_nav(nav_items, prefix=""):
    for item in nav_items:
        if isinstance(item, dict):
            for key, value in item.items():
                if isinstance(value, str):
                    filepath = os.path.join('$DOCS_DIR', value)
                    if not os.path.exists(filepath):
                        missing.append(filepath)
                elif isinstance(value, list):
                    check_nav(value)

check_nav(nav)

if missing:
    print("MISSING FILES:")
    for f in missing:
        print(f"  ✗ {f}")
    sys.exit(1)
else:
    print("✓ All nav files exist")
EOF

The techdocs-core Plugin Deep Dive

The mkdocs-techdocs-core plugin is a meta-plugin that bundles several MkDocs plugins and extensions required for Backstage integration. Understanding what it does helps you debug when it fails.

Install it:

pip install mkdocs-techdocs-core

What it includes under the hood:

  • mkdocs-material theme
  • mkdocs-monorepo-plugin (for monorepo support)
  • pymdown-extensions
  • mkdocs-redirects
  • Various markdown extensions pre-configured

The most common issue: you install mkdocs-techdocs-core but it pulls in an incompatible version of mkdocs-material. Pin everything:

# Generate a working lockfile
pip install mkdocs-techdocs-core==1.3.3
pip freeze > requirements.txt

# Inspect what got installed
cat requirements.txt | grep -E "mkdocs|material|pymdown"

Monorepo Documentation: The Setup Nobody Documents Well

This deserves its own section because it's genuinely complex and the documentation is scattered.

If you have a monorepo with multiple services, each with their own docs, you have two approaches:

Approach 1: One catalog-info.yaml Per Service

Each service has its own catalog-info.yaml pointing to its own mkdocs.yml. This is the simplest approach and the one I recommend starting with.

monorepo/
├── services/
│   ├── auth-service/
│   │   ├── catalog-info.yaml
│   │   ├── mkdocs.yml
│   │   └── docs/
│   │       └── index.md
│   └── payment-service/
│       ├── catalog-info.yaml
│       ├── mkdocs.yml
│       └── docs/
│           └── index.md

Approach 2: Monorepo Plugin for Cross-Service Docs

When you need to aggregate docs from multiple services into one unified documentation site:

# mkdocs.yml at repo root
site_name: Platform Documentation

plugins:
  - techdocs-core
  - monorepo

nav:
  - Home: index.md
  - Auth Service: '!include ./services/auth-service/mkdocs.yml'
  - Payment Service: '!include ./services/payment-service/mkdocs.yml'

The !include syntax is provided by the mkdocs-monorepo-plugin. Common failure here:

ERROR - The !include tag requires the mkdocs-monorepo-plugin to be installed.

Make sure monorepo appears in your plugins list AND that the plugin is installed. The techdocs-core plugin includes it, but if you're running an older version, install it explicitly:

pip install mkdocs-monorepo-plugin

Storage Backend Configuration

The docs built fine. Now they're not showing up in Backstage. This is a storage and serving problem, not a build problem.

S3 Configuration

# app-config.yaml
techdocs:
  builder: 'external'
  generator:
    runIn: 'local'  # or 'docker'
  publisher:
    type: 'awsS3'
    awsS3:
      bucketName: my-techdocs-bucket
      region: us-east-1
      # For non-default AWS auth, specify credentials
      credentials:
        accessKeyId: ${AWS_ACCESS_KEY_ID}
        secretAccessKey: ${AWS_SECRET_ACCESS_KEY}

S3 bucket policy that actually works (the minimal version you need):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "TechDocsReadWrite",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::ACCOUNT_ID:role/backstage-techdocs-role"
      },
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::my-techdocs-bucket",
        "arn:aws:s3:::my-techdocs-bucket/*"
      ]
    }
  ]
}

The Entity Path Mismatch Problem

This is the most insidious TechDocs issue and affects every storage backend. The docs are published to storage, but Backstage can't find them.

TechDocs stores docs at a path derived from the entity triple: namespace/kind/name. This must match exactly what's in your catalog.

Published path:

my-techdocs-bucket/default/component/my-service/

If your catalog-info.yaml says:

metadata:
  name: My-Service  # Capital letters!
  namespace: default

TechDocs will look for default/component/my-service (lowercased) but your CI published to default/component/My-Service. Silent failure, blank page.

Always use lowercase, kebab-case names in your catalog entities.

Verification script:

#!/bin/bash
# verify-techdocs-path.sh
# Usage: ./verify-techdocs-path.sh <bucket-name> <namespace> <kind> <name>

BUCKET=$1
NAMESPACE=$(echo $2 | tr '[:upper:]' '[:lower:]')
KIND=$(echo $3 | tr '[:upper:]' '[:lower:]')
NAME=$(echo $4 | tr '[:upper:]' '[:lower:]')

echo "Checking for TechDocs at: $NAMESPACE/$KIND/$NAME"

aws s3 ls "s3://$BUCKET/$NAMESPACE/$KIND/$NAME/" && \
  echo "✓ Found TechDocs content" || \
  echo "✗ No content found at expected path"

Local Development: The techdocs-cli serve Workflow

Don't push to CI to test your docs. Use techdocs-cli serve for rapid iteration:

# Install the CLI
npm install -g @techdocs/cli

# Serve docs locally with live reload
techdocs-cli serve --no-docker --port 3000

# Or with Docker (matches CI environment exactly)
techdocs-cli serve --port 3000

This spins up a local preview at http://localhost:3000 with live reload. Use this constantly.

For debugging build issues specifically:

# Verbose MkDocs output
techdocs-cli generate --no-docker --verbose

# Check what MkDocs version you're running
python3 -m mkdocs --version

# Validate mkdocs.yml without building
python3 -m mkdocs build --strict 2>&1 | head -50

The --strict flag treats warnings as errors, which is what Backstage effectively does in production. Run it locally first.


Mermaid Diagrams Not Rendering

This comes up in every TechDocs implementation. Mermaid diagrams either show as raw text or throw JavaScript errors in the browser.

The issue: Mermaid in TechDocs requires the superfences extension configured correctly, and the Backstage frontend loads Mermaid separately.

Backend: mkdocs.yml configuration

markdown_extensions:
  - pymdownx.superfences:
      custom_fences:
        - name: mermaid
          class: mermaid
          format: !!python/name:pymdownx.superfences.fence_code_format

In your docs:

```mermaid
graph TD
    A[Client] --> B[Load Balancer]
    B --> C[Service 1]
    B --> D[Service 2]
```

If diagrams still don't render, check whether your Backstage instance has the Mermaid plugin installed in the frontend:

// packages/app/src/App.tsx
import { techdocsPlugin } from '@backstage/plugin-techdocs';

// In your App component, ensure TechDocs is properly configured

And in your Backstage backend package:

# Check installed packages
yarn workspace backend list | grep mermaid
# Should include @backstage/plugin-techdocs-module-addons-contrib

Common app-config.yaml Mistakes

Here's a complete, working TechDocs configuration with annotations explaining each decision:

# app-config.yaml
techdocs:
  # Use 'external' in production, never 'local'
  builder: 'external'
  
  generator:
    # 'docker' ensures consistent builds; 'local' is fine if you control the environment
    runIn: 'docker'
    # Pin the Docker image to avoid surprise breakages
    dockerImage: 'spotify/techdocs-container:v1.2.4'
    pullImage: true  # Always pull to get security patches
  
  publisher:
    type: 'awsS3'
    awsS3:
      bucketName: ${TECHDOCS_S3_BUCKET_NAME}
      region: ${AWS_REGION}
      # This is crucial - set to true if you want Backstage to serve files 
      # directly rather than through the backend proxy
      legacyUseCaseSensitiveTripletPaths: false

# Also ensure your backend CORS is configured for TechDocs assets
backend:
  csp:
    # Required for Mermaid and other dynamic content
    script-src:
      - "'self'"
      - "'unsafe-eval'"  # Needed for Mermaid
    img-src:
      - "'self'"
      - 'data:'  # Required for some diagram types

Building a Validation Pipeline

Don't let broken docs reach production. Here's a complete validation pipeline:

# .github/workflows/validate-techdocs.yml
name: Validate TechDocs

on:
  pull_request:
    paths:
      - 'docs/**'
      - 'mkdocs.yml'
      - 'catalog-info.yaml'

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Validate mkdocs.yml syntax
        run: |
          docker run --rm \
            -v $(pwd):/content \
            -w /content \
            spotify/techdocs-container:latest \
            python3 -c "
          import yaml
          import sys
          try:
              with open('mkdocs.yml') as f:
                  config = yaml.safe_load(f)
              print('✓ mkdocs.yml is valid YAML')
          except yaml.YAMLError as e:
              print(f'✗ Invalid YAML: {e}')
              sys.exit(1)
          "
      
      - name: Build TechDocs (strict mode)
        run: |
          docker run --rm \
            -v $(pwd):/content \
            -w /content \
            spotify/techdocs-container:latest \
            mkdocs build --strict
      
      - name: Check for broken internal links
        run: |
          docker run --rm \
            -v $(pwd):/content \
            -w /content \
            spotify/techdocs-container:latest \
            bash -c "pip install linkchecker && linkchecker --check-extern site/"
      
      - name: Verify catalog-info.yaml annotation
        run: |
          python3 - <<'EOF'
          import yaml
          import sys
          
          with open('catalog-info.yaml') as f:
              catalog = yaml.safe_load(f)
          
          annotations = catalog.get('metadata', {}).get('annotations', {})
          techdocs_ref = annotations.get('backstage.io/techdocs-ref')
          
          if not techdocs_ref:
              print('✗ Missing backstage.io/techdocs-ref annotation')
              sys.exit(1)
          
          print(f'✓ TechDocs annotation found: {techdocs_ref}')
          
          # Validate entity name is lowercase
          name = catalog.get('metadata', {}).get('name', '')
          if name != name.lower():
              print(f'✗ Entity name "{name}" contains uppercase - will cause path mismatch')
              sys.exit(1)
          
          print(f'✓ Entity name "{name}" is lowercase')
          EOF

Debugging Checklist

When TechDocs isn't rendering, work through this list in order:

1. Verify the build is actually running

# Check CI logs for your last build
# Look for: "Successfully published techdocs for entity"

2. Verify the content exists in storage

aws s3 ls s3://my-bucket/default/component/my-service/ --recursive | head -20
# You should see index.html and other static assets

3. Verify Backstage can reach the storage backend

# Check Backstage backend logs for storage errors
kubectl logs -n backstage deployment/backstage | grep -i techdocs

4. Verify the entity name matches the storage path

# Compare catalog entity triple with storage path
# Namespace/Kind/Name must be lowercase and match exactly

5. Force a cache refresh

# TechDocs caches aggressively. In the UI, append ?refresh=true to the URL
# Or invalidate via the TechDocs API:
curl -X POST "https://backstage.example.com/api/techdocs/sync/default/component/my-service" \
  -H "Authorization: Bearer $BACKSTAGE_TOKEN"

6. Check browser console for asset loading errors

TechDocs loads CSS, JS, and images dynamically. If your Backstage instance has strict CSP headers, assets may be blocked silently.


Production Recommendations

After deploying TechDocs across several platforms, here's my opinionated take on what actually works:

Use external builder always. Local builder is a load balancer killer when you have hundreds of services.

Pin your container image version in CI pipelines. Auto-updating the latest tag has broken docs deployments more times than I can count.

Set up a dedicated S3 bucket with versioning enabled. Bad docs deploys happen. Being able to roll back to a previous version without re-running CI is worth the storage cost.

Add TechDocs validation to your organization's service template in Backstage. If every new service scaffolds with a working mkdocs.yml and catalog-info.yaml, you avoid half the issues covered in this article.

Monitor storage costs. Every docs build overwrites the previous version completely. For repositories with large binary assets in docs (screenshots, diagrams), this adds up. Set up lifecycle policies.

# S3 lifecycle policy to clean up old versions
aws s3api put-bucket-lifecycle-configuration \
  --bucket my-techdocs-bucket \
  --lifecycle-configuration '{
    "Rules": [{
      "ID": "CleanupOldVersions",
      "Status": "Enabled",
      "NoncurrentVersionExpiration": {
        "NoncurrentDays": 30
      }
    }]
  }'

Wrapping Up

TechDocs is worth the setup pain. Having documentation live in the same developer portal as your service catalog, runbooks, and APIs genuinely changes how engineers interact with docs. The failure modes are frustrating but predictable once you understand them.

The tl;dr:

  • Use the official container image for all builds
  • Pin your plugin versions explicitly
  • techdocs-core plugin must be first in your plugin list
  • Entity names in catalog-info.yaml must be lowercase to match storage paths
  • Validate your docs build with --strict mode before pushing
  • External builder mode is not optional for production

If you're still stuck after working through this guide, the most useful thing you can do is run techdocs-cli generate --verbose --no-docker locally and paste the full output somewhere. The verbose output from MkDocs almost always contains the exact error — it just gets swallowed in non-verbose mode.

Share:

Was this article helpful?

Majid Iqbal Nayyar
Majid Iqbal Nayyar

Data Infrastructure Engineer

Your data is only as good as the infrastructure it sits on. I specialize in PostgreSQL, Redis, database migrations, backup strategies, and making sure your data survives whatever chaos your application throws at it.

Related Articles

More in Platform Engineering

View all →

Discussion