Host a documentation site with Docusaurus

This tutorial walks you from a blank computer to a live documentation site that auto-deploys to Flexweg on every git push. The exact setup powers the site you're reading right now.
What you'll build
- A full Docusaurus site running on your machine with hot reload at
http://localhost:3000 - A GitHub repository with your source
- A GitHub Actions workflow that builds your site and pushes it to Flexweg on every commit to
main - A live URL of the form
https://your-subdomain.flexweg.com
Everything uses Docker so you don't need Node, npm or any JavaScript toolchain installed locally.
Before you start
This tutorial assumes you've completed the Prerequisites — Docker Desktop + Git installed, Git configured, and free GitHub + Flexweg accounts ready.
Once that's done, come back and start at Step 1 below.
Step 1 — Open a terminal and pick your workspace
Open a terminal (Terminal on macOS/Linux, PowerShell on Windows), then navigate to the folder where you want to keep your projects. Any existing folder works — Desktop, Documents, a dedicated ~/Projects folder…
- macOS / Linux
- Windows PowerShell
cd ~/Desktop
# or: cd ~/Projects, or any folder you prefer
cd $HOME\Desktop
# or: cd $HOME\Projects, or any folder you prefer
The next step will create a new my-docs subfolder inside the location you just chose. So if you cd ~/Desktop, your project will live at ~/Desktop/my-docs/.
Step 2 — Scaffold Docusaurus
Use Docker to run the official Docusaurus 3.10 installer, so you don't need Node.js on your machine. This command creates a new my-docs folder with a ready-to-use Docusaurus project (classic template, TypeScript).
- macOS / Linux
- Windows PowerShell
docker run --rm -it -v "$(pwd):/app" -w /app node:20-alpine \
npx -y create-docusaurus@latest my-docs classic --typescript
docker run --rm -it -v "${PWD}:/app" -w /app node:20-alpine `
npx -y create-docusaurus@latest my-docs classic --typescript
What happens:
- Docker pulls the
node:20-alpineimage (≈150 MB, one-time download). - The container mounts your current folder at
/appand runs the installer. - Docusaurus scaffolds the
classictemplate with TypeScript support (matches what this tutorial uses). - After a couple of minutes, a new
my-docs/folder appears next to where you ran the command.
On Linux, files created inside the container are owned by root on the host. Reclaim ownership once:
sudo chown -R $USER:$USER my-docs
Step 3 — Enter the project folder
All subsequent commands in this tutorial run from inside your my-docs folder:
cd my-docs
Your folder now looks like this:
my-docs/
├── blog/ ← example blog posts (optional, delete if unused)
├── docs/ ← your Markdown documentation pages
├── src/
│ ├── components/
│ ├── css/
│ │ └── custom.css ← site-wide styling
│ └── pages/
│ └── index.tsx ← homepage React component
├── static/
│ └── img/ ← static images served at /img/*
├── docusaurus.config.ts ← site metadata, navbar, footer
├── package.json
├── sidebars.ts ← docs sidebar layout
└── tsconfig.json
Step 4 — Add a docker-compose.yml for local dev
Create a file named docker-compose.yml at the root of your project with exactly this content:
services:
docs:
image: node:20-alpine
working_dir: /app
ports:
- "3000:3000"
volumes:
- .:/app
- node_modules:/app/node_modules
command: sh -c "npm install && npm start -- --host 0.0.0.0"
volumes:
node_modules:
What this does:
image: node:20-alpine— uses a stock Node.js 20 image, no custom Dockerfile needed.ports: ["3000:3000"]— exposes the dev server athttp://localhost:3000.volumes: [".:/app"]— bind-mounts your project folder so edits on your host trigger hot reload inside the container.volumes: ["node_modules:/app/node_modules"]— keepsnode_modules/inside a Docker-managed volume so it survives restarts and doesn't pollute your host folder.command: "npm install && npm start"— runs the dev server on everydocker compose up. First run takes 1–2 minutes (installs deps); subsequent runs start in seconds.
Step 5 — Start the dev server
From your project folder:
docker compose up
The first time, Docker downloads the image (~150 MB, cached after) and runs npm install. Watch for the line:
[SUCCESS] Docusaurus website is running at: http://localhost:3000/
Open http://localhost:3000 in your browser. You should see the default Docusaurus landing page.
Leave this terminal running — it keeps the server alive and shows logs. To stop, press Ctrl+C in the terminal (or run docker compose down from another terminal).
If something else is listening on port 3000, change the mapping to e.g. "3001:3000" in docker-compose.yml and visit http://localhost:3001.
Step 6 — Customize your site
Open the project folder in your editor of choice (VS Code, Sublime, anything). Key files:
| Path | Purpose |
|---|---|
docusaurus.config.ts | Site metadata (title, URL, navbar, footer). |
docs/ | Markdown pages for the docs sidebar. |
src/pages/index.tsx | The homepage React component. |
src/css/custom.css | Site-wide CSS tokens & overrides. |
static/ | Static assets copied verbatim to the build root. |
blog/ | Example blog posts (delete this folder if you don't need a blog). |
Open docusaurus.config.ts and change at least these three fields:
const config: Config = {
title: 'My Documentation',
tagline: 'Everything you need to know about my product',
favicon: 'img/favicon.ico',
url: 'https://your-subdomain.flexweg.com', // your future live URL
baseUrl: '/',
// ... rest unchanged
};
Save the file. Your browser reloads automatically and shows the new title.
Edit your first doc
Open docs/intro.md and replace its content with anything you like. Save — the page at http://localhost:3000/docs/intro updates instantly.
That's the entire dev loop: edit → save → see.
Step 7 — Note your Flexweg URL and API key
You already created a Flexweg account and generated an API key during the Prerequisites — grab them now:
- Log into flexweg.com. At the top of your site's interface you'll see its URL, e.g.
https://4dp6g1d6.flexweg.com. Note it down — it's where your Docusaurus site will go live. - Optionally customize the subdomain (see Publish your first page) to something readable like
docs-yourname→https://docs-yourname.flexweg.com. - Open Account → API and copy your API key. You'll paste it into a GitHub secret in a few steps.
Step 8 — Create the sync script
At the root of your project, create a file named sync-flexweg.js. Paste this exact content (it's self-contained, no dependencies):
// Upload every file in build/ to Flexweg via the REST API.
const fs = require('fs');
const path = require('path');
const API_KEY = process.env.FLEXWEG_API_KEY;
const BASE_URL = process.env.FLEXWEG_BASE_URL || 'https://www.flexweg.com';
const BUILD_DIR = 'build';
const BINARY = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'ico', 'pdf', 'woff', 'woff2', 'ttf', 'otf']);
const TEXT = new Set(['html', 'css', 'js', 'json', 'xml', 'txt', 'svg']);
const RATE_PER_MIN = 50;
const GAP_MS = Math.ceil(60000 / RATE_PER_MIN);
let lastAt = 0;
let errors = 0;
async function rateLimit() {
const wait = lastAt + GAP_MS - Date.now();
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
lastAt = Date.now();
}
async function upload(full, rel) {
const ext = path.extname(full).slice(1).toLowerCase();
if (!BINARY.has(ext) && !TEXT.has(ext)) return;
const isBinary = BINARY.has(ext);
const content = isBinary
? fs.readFileSync(full).toString('base64')
: fs.readFileSync(full, 'utf8');
const body = { path: rel, content };
if (isBinary) body.encoding = 'base64';
await rateLimit();
const res = await fetch(`${BASE_URL}/api/v1/files/upload`, {
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!res.ok) {
console.error(`✗ ${rel}: ${res.status} ${await res.text()}`);
errors++;
} else {
console.log(`✓ ${rel}`);
}
}
async function walk(dir, base = dir) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
const rel = path.relative(base, full).replace(/\\/g, '/');
if (entry.isDirectory()) await walk(full, base);
else await upload(full, rel);
}
}
async function main() {
if (!API_KEY) {
console.error('FLEXWEG_API_KEY environment variable is not set');
process.exit(1);
}
if (!fs.existsSync(BUILD_DIR)) {
console.error(`Build directory '${BUILD_DIR}' not found. Run 'npm run build' first.`);
process.exit(1);
}
console.log(`Uploading ${BUILD_DIR}/ to ${BASE_URL} ...\n`);
await walk(BUILD_DIR);
if (errors > 0) {
console.error(`\nFailed to sync ${errors} file(s).`);
process.exit(1);
}
console.log('\nAll files synced successfully.');
}
main();
What it does, in plain language:
- Walks the
build/folder produced bynpm run build. - For every supported file (HTML, CSS, JS, images, fonts, PDF, etc.), it POSTs the content to
https://www.flexweg.com/api/v1/files/uploadusing your API key. - It throttles itself to ~50 requests per minute to stay under Flexweg's rate limit.
- If any upload fails, it exits with status 1 (which makes GitHub Actions mark the deploy as failed).
You'll never need to edit this file — it just works.
Step 9 — Create the GitHub Actions workflow
GitHub Actions is GitHub's built-in CI/CD. We'll use it to rebuild and redeploy your site on every push to the main branch.
Create a file at .github/workflows/deploy.yml (you need to create the two folders first):
name: Deploy to Flexweg
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build site
run: npm run build
- name: Deploy to Flexweg
env:
FLEXWEG_API_KEY: ${{ secrets.FLEXWEG_API_KEY }}
run: node sync-flexweg.js
What this does on every push to main:
- Checks out your code.
- Installs Node.js 20 (cached between runs for speed).
- Runs
npm cito install dependencies frompackage-lock.json. - Runs
npm run build— this produces the static files inbuild/. - Runs
node sync-flexweg.js— which uploads everything inbuild/to your Flexweg site.
Step 10 — Create a .gitignore
Make sure Docusaurus's generated folders don't pollute your repository. Docusaurus scaffolded a .gitignore for you, but verify it contains at least:
# Dependencies
node_modules/
# Build output
build/
.docusaurus/
# Local env
.env
.env.local
Step 11 — Initialize git and push to GitHub
From your project folder:
git init -b main
git add .
git commit -m "Initial Docusaurus site"
Now create an empty repository on GitHub:
-
Go to github.com/new.
-
Name it e.g.
my-docs. -
Do not initialize it with a README, .gitignore or license (you already have files).
-
Click Create repository.
-
GitHub shows a "…push an existing repository from the command line" snippet. Copy and run the two commands — they look like:
git remote add origin https://github.com/YOUR_USERNAME/my-docs.gitgit push -u origin main
Your code is now on GitHub.
Step 12 — Add the Flexweg API key as a GitHub secret
Your API key must live as a GitHub secret (encrypted, never exposed in logs) so the workflow can use it without committing it to the repo.
- On GitHub, open your repository.
- Click Settings (repo-level, not account-level).
- In the left sidebar: Secrets and variables → Actions.
- Click New repository secret.
- Fill in:
- Name:
FLEXWEG_API_KEY - Secret: paste your Flexweg API key from Step 7
- Name:
- Click Add secret.
Step 13 — Trigger your first deployment
The workflow runs automatically on every push to main. You haven't pushed anything since adding the secret, so trigger it manually by making a trivial change:
# Edit docs/intro.md with any change, then:
git add docs/intro.md
git commit -m "Trigger first deployment"
git push
Go to your GitHub repo → Actions tab. You'll see a workflow run in progress. Click it to watch the steps stream in real-time. Expect it to take ~2 minutes (the upload step dominates due to rate limiting).
Once the run turns green, open your Flexweg URL — your Docusaurus site is live! 🎉
Step 14 — Your daily workflow
From now on, here's your loop:
- Edit a
.mdfile or a component on your machine. - Check it locally at
http://localhost:3000— hot reload, instant. - When happy:
git add -A && git commit -m "..." && git push. - GitHub Actions rebuilds and redeploys automatically (~2 minutes).
- Refresh your live URL.
Troubleshooting
Your FLEXWEG_API_KEY secret is missing or wrong. Re-copy the key from Account → API, update the GitHub secret, and re-run the workflow from the Actions tab.
You're on the free plan (2 MB / 20 files). A Docusaurus site exceeds this once it has a handful of pages. Upgrade your Flexweg plan from Account → Billing, or trim your site.
Another process owns port 3000. Either stop it (common culprits: Node apps, other Docusaurus sites), or change "3000:3000" to "3001:3000" in docker-compose.yml.
WSL2 sometimes misses file events on Windows-mounted folders. Move your project into the WSL filesystem (/home/<user>/my-docs) for instant hot reload.
Next steps
- Custom domain — point
docs.yourbrand.comat your Flexweg site. See your Flexweg site settings. - Add images — drop them into
static/img/and reference them as/img/filename.pngin markdown. - Organize your docs — create folders under
docs/(each with a_category_.json) for hierarchical navigation. The sidebar auto-generates. - Version your docs — Docusaurus supports multiple versions out of the box. Run
docker compose exec docs npm run docusaurus docs:version 1.0to snapshot. - Read Share styles across pages to understand how Docusaurus compiles your React/CSS into the static
build/that Flexweg serves.
You now have everything you need to run a production documentation site on Flexweg — the same setup that powers the docs you're reading.