$linuxjunkies
>

Build a Documentation Site with MkDocs Material

Install MkDocs Material, configure navigation and theme features, preview locally, then deploy to GitHub Pages or serve with Caddy on your own server.

BeginnerUbuntuDebianFedoraArch8 min readUpdated June 7, 2026

Before you start

  • Python 3.9 or newer installed (python3 --version to check)
  • pip available (python3 -m pip --version)
  • A GitHub account if deploying to GitHub Pages
  • Caddy installed and running if using self-hosted deployment

MkDocs with the Material theme turns a folder of Markdown files into a polished, searchable documentation site in minutes. This guide walks through installation, configuration, adding navigation, enabling Material's extra features, and deploying—either to GitHub Pages for free hosting or behind a Caddy reverse proxy on your own server.

Install MkDocs and Material

Material for MkDocs ships as a Python package. Use a virtual environment so the install stays isolated from your system Python.

Create and activate a virtual environment

python3 -m venv ~/mkdocs-env
source ~/mkdocs-env/bin/activate

Install the packages

pip install mkdocs-material

This pulls in mkdocs, mkdocs-material, and all required dependencies including pymdown-extensions. Confirm the versions:

mkdocs --version
pip show mkdocs-material | grep Version

Output will vary, but you want MkDocs 1.5+ and mkdocs-material 9.x for all features covered here.

Scaffold a New Project

mkdocs new my-docs
cd my-docs

The generated structure looks like this:

my-docs/
├── docs/
│   └── index.md
└── mkdocs.yml

docs/ holds your Markdown source files. mkdocs.yml is the single configuration file for everything.

Configure the Material Theme

Open mkdocs.yml and replace its contents with a solid starting configuration:

cat > mkdocs.yml <<'EOF'
site_name: My Project Docs
site_url: https://docs.example.com
site_description: Documentation for My Project

theme:
  name: material
  palette:
    - scheme: default
      primary: indigo
      accent: indigo
      toggle:
        icon: material/brightness-7
        name: Switch to dark mode
    - scheme: slate
      primary: indigo
      accent: indigo
      toggle:
        icon: material/brightness-4
        name: Switch to light mode
  features:
    - navigation.tabs
    - navigation.sections
    - navigation.top
    - search.suggest
    - search.highlight
    - content.code.copy

plugins:
  - search

markdown_extensions:
  - admonition
  - pymdownx.details
  - pymdownx.superfences
  - pymdownx.highlight:
      anchor_linenums: true
  - pymdownx.inlinehilite
  - pymdownx.tabbed:
      alternate_style: true
  - attr_list
  - md_in_html
EOF

Key theme features explained

  • navigation.tabs — top-level nav items appear as tabs rather than a sidebar list; good for multi-section sites.
  • navigation.sections — groups sub-pages under bold headings in the sidebar.
  • navigation.top — shows a "back to top" button when scrolling.
  • search.suggest / search.highlight — autocomplete and in-page term highlighting.
  • content.code.copy — adds a copy button to every code block automatically.

Build Out Your Navigation

Create some pages to populate the site:

mkdir -p docs/getting-started docs/reference
echo '# Getting Started\n\nWelcome to the quick-start guide.' > docs/getting-started/index.md
echo '# Installation\n\nInstall steps go here.' > docs/getting-started/installation.md
echo '# API Reference\n\nFunction reference goes here.' > docs/reference/api.md

Then define the navigation tree explicitly in mkdocs.yml. Explicit nav is always preferable to relying on auto-discovery for anything beyond a tiny site:

cat >> mkdocs.yml <<'EOF'

nav:
  - Home: index.md
  - Getting Started:
    - Overview: getting-started/index.md
    - Installation: getting-started/installation.md
  - Reference:
    - API: reference/api.md
EOF

Preview Locally

The built-in dev server watches for file changes and reloads instantly:

mkdocs serve

Open http://127.0.0.1:8000 in a browser. On a headless server, bind to a specific address instead:

mkdocs serve --dev-addr 0.0.0.0:8000

Note: never expose the dev server directly to the public internet; it is not hardened for production use.

Deploy to GitHub Pages

MkDocs has first-class GitHub Pages support. Push your project to a GitHub repo, then run:

mkdocs gh-deploy

This builds the site, commits the output to the gh-pages branch, and pushes it. GitHub automatically serves it at https://<username>.github.io/<repo>/.

Automate with GitHub Actions

Create .github/workflows/docs.yml in your repo to rebuild on every push to main:

mkdir -p .github/workflows
cat > .github/workflows/docs.yml <<'EOF'
name: Deploy Docs
on:
  push:
    branches: [main]

permissions:
  contents: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-python@v5
        with:
          python-version: 3.x
      - run: pip install mkdocs-material
      - run: mkdocs gh-deploy --force
EOF

Deploy Behind Caddy (Self-Hosted)

If you prefer your own server, build the static site and serve it with Caddy.

Build the static output

mkdocs build
# Output lands in ./site/

Copy files to the server

rsync -av --delete site/ user@your-server:/var/www/docs/

Configure Caddy

Add a block to /etc/caddy/Caddyfile:

docs.example.com {
    root * /var/www/docs
    file_server
    encode gzip
}

Reload Caddy:

sudo systemctl reload caddy

Caddy handles HTTPS automatically via Let's Encrypt. No certbot, no renewal cron jobs needed.

Verify the Deployment

Check the site loads and search works:

# HTTP status check
curl -o /dev/null -s -w "%{http_code}\n" https://docs.example.com

# Verify the search index was generated
curl -s https://docs.example.com/search/search_index.json | python3 -m json.tool | head -20

A 200 and a valid JSON response confirm the build and serving are both healthy.

Troubleshooting

  • Navigation entries missing: If a file listed in nav: does not exist, MkDocs will warn and skip it. Run mkdocs build --strict to turn warnings into errors and catch these during CI.
  • Search returns no results: The search plugin must be listed under plugins:. If you define a plugins: block at all, MkDocs removes the default search plugin unless you re-add it explicitly.
  • GitHub Actions deploy fails with permission denied: Ensure the workflow has permissions: contents: write (shown above). Older repos may also need the Actions setting Read and write permissions enabled under Settings → Actions → General.
  • pip command not found after deactivating venv: Re-activate with source ~/mkdocs-env/bin/activate before running any mkdocs commands.
  • Caddy 404 on sub-pages: MkDocs Material generates page/index.html style URLs by default. Caddy's file_server handles this correctly; no rewrite rules are needed.
tested on:Ubuntu 24.04Fedora 40Arch rollingDebian 12

Frequently asked questions

Do I need a paid MkDocs Material license?
The community edition is fully open source and free. The Insiders tier adds extra features like social cards and tags, but everything in this guide uses the free version.
Can I use MkDocs without GitHub?
Yes. mkdocs build produces a plain static site in the site/ directory that you can host anywhere—Caddy, Nginx, S3, Netlify, or any web server.
How do I add versioned documentation?
Install the mike plugin (pip install mike) alongside mkdocs-material. It manages multiple doc versions on the gh-pages branch and integrates with the Material version selector.
Why does MkDocs warn that my nav entries don't match files?
Every path in nav: must exactly match an existing file under docs/, including case. Run mkdocs build --strict to surface these as errors so they don't silently break links.
How do I add a custom domain to the GitHub Pages site?
Create a docs/CNAME file containing just your domain (e.g. docs.example.com). mkdocs gh-deploy copies it to the gh-pages branch, and GitHub picks it up automatically.

Related guides