Deploying Quartz v5 on Docker: A Complete Guide for Self-Hosted Knowledge Bases
Introduction
If you want to run a self-hosted knowledge base, deploying Quartz v5 on Docker provides a fast, batteries-included static site generator that transforms Markdown files into a website. Version 5 introduced a community plugin system, YAML-based configuration, and a completely redesigned architecture. If you run an Obsidian vault as a published knowledge base—as I do with my Inner Range technical reference wiki—hosting it yourself on a self-hosted web server on VPS with Docker gives you full control.
This guide walks through deploying Quartz v5 on Docker, from repository setup to production deployment, including the pitfalls I hit and how you can avoid them.

Why Deploy Quartz v5 on Docker?
Quartz is a Node.js application. You could run npx quartz build directly on a server, but Docker offers several advantages:
- Reproducible builds — The same
Dockerfileworks locally and on any VPS - No Node.js installation on the host — The container has everything it needs
- Easy rollbacks — Rebuild from any Git commit
- Nginx in the same stack — Serve static files with proper caching and URL handling
- Plugin isolation — Community plugins install inside the container, never pollute the host
Architecture Overview
Here’s the architecture we’re building:
| Component | Role |
|---|---|
| GitHub repository | Source of truth — contains the Quartz source, your content, config, and Dockerfile |
| Multi-stage Dockerfile | Stage 1: build the site (Node.js). Stage 2: serve it (Nginx Alpine) |
| Docker Compose | Manage the container lifecycle with restart policies |
| VPS | Hostinger VPS running Docker, ports exposed via Nginx Proxy Manager |
The key insight: your repository is the Quartz v5 source. You fork the upstream branch, add your content and Docker config, and that single repo becomes both your content vault and your build system.
your-repo/
content/ ← Your Markdown pages
quartz/ ← Quartz v5 source (forked from upstream)
quartz.config.yaml ← Your site configuration
Dockerfile ← Multi-stage build
docker-compose.yml
nginx.conf
Step 1: Fork Quartz v5 Into Your Repository
Rather than cloning Quartz as a dependency at build time, make your repository the Quartz source itself:
# Add the official Quartz repository as upstream
git remote add upstream https://github.com/jackyzha0/quartz.git
# Fetch and branch from upstream's v5
git fetch upstream v5
git checkout -b v5 upstream/v5
# Push to your own repository
git push -u origin v5
Now your repository has the complete Quartz codebase under quartz/. Install dependencies:
npm install
Run the setup wizard to configure your site:
npx quartz create
This prompts for a template (choose obsidian if you use an Obsidian vault), a content folder, and generates quartz.config.yaml.
Step 2: Move Your Content In
Quartz v5 expects content in the content/ folder:
# Copy your Markdown files into content/
cp -r /path/to/your/vault/* content/
If migrating from Quartz v4, note that the content folder was previously named wiki/. The migration guide at quartz.jzhao.xyz/getting-started/migrating covers the full process.
Step 3: Install Community Plugins
Quartz v5 plugins are standalone Git repositories maintained under the quartz-community organization. Your quartz.config.yaml lists which plugins to enable, and the CLI installs them:
npx quartz plugin install --from-config
This reads your config, clones each plugin into .quartz/plugins/, and records the exact commits in quartz.lock.json for reproducible builds.
Step 4: The Dockerfile
Here’s the multi-stage Dockerfile I use in production:
FROM node:24-slim AS builder
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
WORKDIR /quartz
# Install dependencies first (cached layer)
COPY package.json package-lock.json ./
RUN npm ci
# Copy everything else
COPY . .
# Install plugins and build the site
RUN npx quartz plugin install --from-config
RUN npx quartz build
FROM nginx:alpine
COPY --from=builder /quartz/public /usr/share/nginx/html
# Symlink for image asset paths (e.g., /wiki/assets/...)
RUN ln -s . /usr/share/nginx/html/wiki
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
Key details:
- Stage 1 (
builder): Usesnode:24-slimto runnpm ci, install plugins, and build the static site into/quartz/public/ - Stage 2 (runtime): Uses the tiny
nginx:alpineimage — only the built HTML/CSS/JS, no Node.js, no build tools - The
--from-configflag tells Quartz to install exactly the plugins and versions pinned inquartz.lock.json - The symlink (
ln -s . /usr/share/nginx/html/wiki) preserves image paths that reference/wiki/assets/...from the v4 URL structure
⚠️ Important: Always use
--from-config, not--latest, for production builds.--latestpulls the newest commits of all plugins, which can introduce breaking changes.--from-configuses your tested, locked versions.
Step 5: Docker Compose
A minimal docker-compose.yml:
services:
irwiki:
build: .
container_name: irwiki
restart: unless-stopped
ports:
- "8085:80"
The restart: unless-stopped policy ensures the container comes back up after a server reboot.
Step 6: Nginx Configuration
A simple Nginx config to serve the static site with proper caching and a custom 404 page:
server {
listen 80;
server_name irwiki.kamath.cloud;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri.html $uri/ =404;
}
# Cache static assets aggressively
location /static/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
Step 7: Deploy
With your Dockerfile, docker-compose.yml, and nginx.conf committed and pushed to your repository, deploy on the VPS:
ssh root@your-vps
cd /docker/irwiki
git pull
docker compose build --no-cache
docker compose up -d
⚠️ Always use
--no-cacheon the first deploy and after any plugin change. Docker caches intermediate layers. If a plugin install failed once, the cached layer will serve the broken install forever.--no-cacheforces a clean rebuild.
Pitfalls I Hit (So You Don’t Have To)
1. npm Fails on Google Drive Mounts
If your repository lives on a Google Drive folder (common with Obsidian vaults), npm install and npx will fail with EBADF or EPERM errors. Cloud sync tools interfere with Node.js file operations.
Fix: Clone your repository to a local disk (e.g., C:\temp) for all npm/npx operations, then copy only the config files back to the Google Drive folder.
# Work on local disk
cd /c/temp && git clone --branch v5 /path/to/google-drive/repo wiki-build
cd wiki-build && npm ci
npx quartz plugin install --from-config
npx quartz build
# Copy changed configs back
cp quartz.config.yaml quartz.lock.json /path/to/google-drive/repo/
2. Plugin Loader Errors Are Misleading
If a community plugin fails with "declares components but failed to load them", the real error is almost certainly different from what the message says. The v5 component loader swallows the actual exception and prints this generic warning.
Fix: Read the loader source code at quartz/plugins/loader/componentLoader.ts. In my case, the loader imports the ./components subpath of the plugin, but the plugin only exposed its main . entry. Adding a ./components export to the plugin’s package.json fixed it instantly.
3. Private Plugin Repos Won’t Work in Docker
Your Docker build has no GitHub authentication. Any plugin referenced in quartz.config.yaml with a github: source must be in a public repository. For private custom plugins, use a local folder inside your repository:
- source: ./plugins/my-custom-plugin
enabled: true
4. Docker Cache Masks Plugin Failures
If npx quartz plugin install --from-config fails during a Docker build, the error might scroll past. On the next build, Docker reuses the cached layer from before the plugin step — so even after fixing the plugin, the build appears to “work” but still serves the broken version.
Fix: Always use --no-cache after any plugin change. For routine content-only deploys, a normal cached build is fine.
Daily Workflow
For content updates (no plugin changes):
# Edit your Markdown files in content/
git add content/
git commit -m "Update hardware pinout reference"
git push
# Deploy (cached build is fine for content-only changes)
ssh root@vps "cd /docker/irwiki && git pull && docker compose up -d --build"
For plugin changes or Quartz version upgrades:
# Always use --no-cache
ssh root@vps "cd /docker/irwiki && git pull && docker compose build --no-cache && docker compose up -d"
Rollback
Your old v4 (or previous v5 commit) is preserved in Git. To roll back instantly:
ssh root@vps
cd /docker/irwiki
git checkout main # Or any previous commit
docker compose up -d --build
The old build runs and the site is back. No database migrations, no state to unwind.
Verification
After deploying, confirm everything is healthy:
# Check the Quartz version
curl -s https://irwiki.kamath.cloud | grep -o "Quartz v[0-9.]*"
# Check a deep page
curl -s -o /dev/null -w "%{http_code}\n" https://irwiki.kamath.cloud/hardware/some-page
# Check old v4 URLs still redirect
curl -s -o /dev/null -w "%{http_code}\n" \
"https://irwiki.kamath.cloud/Hardware/Old-Uppercase-URL"
Conclusion
Deploying Quartz v5 on Docker gives you a reproducible, self-contained knowledge base that builds identically on any machine. The multi-stage Dockerfile keeps the runtime image tiny (just Nginx and static files), while Git provides instant rollbacks and a complete history of every change.
The setup I’ve described has been running my Inner Range technical reference wiki at irwiki.kamath.cloud — 150 pages, 45 plugins, full-text search, dark mode, and site-wide disclaimers — all from a single repository and a handful of Docker commands.
Related Reading: If you’re managing files across different environments, see my guide on Using Git Safely on Cloud-Synced Folders.




