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.
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. Runmkdocs build --strictto turn warnings into errors and catch these during CI. - Search returns no results: The
searchplugin must be listed underplugins:. If you define aplugins: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/activatebefore running anymkdocscommands. - Caddy 404 on sub-pages: MkDocs Material generates
page/index.htmlstyle URLs by default. Caddy'sfile_serverhandles this correctly; no rewrite rules are needed.
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
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.