Skip to main content

Host a documentation site with Docusaurus

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

Required setup

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…

cd ~/Desktop
# or: cd ~/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).

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-alpine image (≈150 MB, one-time download).
  • The container mounts your current folder at /app and runs the installer.
  • Docusaurus scaffolds the classic template 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.
Linux permission note

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:

docker-compose.yml
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 at http://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"] — keeps node_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 every docker 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).

Port 3000 already taken?

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:

PathPurpose
docusaurus.config.tsSite metadata (title, URL, navbar, footer).
docs/Markdown pages for the docs sidebar.
src/pages/index.tsxThe homepage React component.
src/css/custom.cssSite-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:

docusaurus.config.ts
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:

  1. 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.
  2. Optionally customize the subdomain (see Publish your first page) to something readable like docs-yournamehttps://docs-yourname.flexweg.com.
  3. 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):

sync-flexweg.js
// 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:

  1. Walks the build/ folder produced by npm run build.
  2. For every supported file (HTML, CSS, JS, images, fonts, PDF, etc.), it POSTs the content to https://www.flexweg.com/api/v1/files/upload using your API key.
  3. It throttles itself to ~50 requests per minute to stay under Flexweg's rate limit.
  4. 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):

.github/workflows/deploy.yml
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:

  1. Checks out your code.
  2. Installs Node.js 20 (cached between runs for speed).
  3. Runs npm ci to install dependencies from package-lock.json.
  4. Runs npm run build — this produces the static files in build/.
  5. Runs node sync-flexweg.js — which uploads everything in build/ 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:

.gitignore
# 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:

  1. Go to github.com/new.

  2. Name it e.g. my-docs.

  3. Do not initialize it with a README, .gitignore or license (you already have files).

  4. Click Create repository.

  5. 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.git
    git 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.

  1. On GitHub, open your repository.
  2. Click Settings (repo-level, not account-level).
  3. In the left sidebar: Secrets and variables → Actions.
  4. Click New repository secret.
  5. Fill in:
    • Name: FLEXWEG_API_KEY
    • Secret: paste your Flexweg API key from Step 7
  6. 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:

  1. Edit a .md file or a component on your machine.
  2. Check it locally at http://localhost:3000 — hot reload, instant.
  3. When happy: git add -A && git commit -m "..." && git push.
  4. GitHub Actions rebuilds and redeploys automatically (~2 minutes).
  5. Refresh your live URL.

Troubleshooting

Workflow fails at "Deploy to Flexweg" with 401

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.

Workflow fails with "Storage limit exceeded"

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.

Docker compose says "port is already allocated"

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.

Hot reload doesn't trigger on Windows

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.com at your Flexweg site. See your Flexweg site settings.
  • Add images — drop them into static/img/ and reference them as /img/filename.png in 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.0 to 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.