Deploying a Rust/Axum App to Cloudflare Pages

Mizushi is a knowledge base engine built with Rust + Axum + Askama — it renders Markdown articles into HTML server-side. The goal was to host it on Cloudflare Pages (free tier).

The Problem

Cloudflare Pages' build environment does not have Rust or Cargo installed. Running cargo run in the build command fails immediately:

/bin/sh: 1: cargo: not found

Pages supports Node.js, Python, Go, Ruby, PHP — but not Rust.

Approach: Static Export + GitHub Actions

Instead of running the server dynamically, the solution is to pre-render all pages to static HTML during CI, then deploy the static output to Pages.

Step 1: export CLI Subcommand

Added an Export variant to the CLI enum:

pub enum Command {
    Serve { port, content, static_dir },
    Export { content, static_dir, output },
}

It loads all Markdown content via the existing load_content(), renders every page (index, articles, tags, search index) using Askama templates, and writes them to ./dist/. Static assets (CSS, JS) are copied as-is.

The output structure:

dist/
  index.html
  article/{slug}.html
  tag/{tag}.html
  search-index.json
  css/style.css
  js/app.js

Cloudflare Pages automatically serves article/slug for article/slug.html (extensionless HTML support).

Step 2: GitHub Actions Workflow

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions-rust-lang/setup-rust-toolchain@v1
      - run: cargo run -- export --output dist
      - uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: pages deploy dist --project-name=mizushi

Step 3: API Token Pitfalls

The wrangler action authenticates via CLOUDFLARE_API_TOKEN. Two issues encountered:

  1. Token scope: The token needs Account-level permissions (not Zone-level). Required:
    • Account > Cloudflare Pages > Edit
    • Account > Workers Scripts > Edit
  2. Account ID: Even with a valid token, the /memberships endpoint may fail. Pass accountId explicitly to skip membership discovery. Get it from the Dashboard URL: https://dash.cloudflare.com/<32-hex-id>.

Both values go into GitHub Secrets (not Variables).

Step 4: Creating the Pages Project

The CLI creates it:

npx wrangler pages project create mizushi --production-branch main

The GUI doesn't allow creating an empty project — it requires connecting to GitHub or uploading files. The API/CLI route avoids this chicken-and-egg problem.

Once the project exists, GitHub Actions can deploy to it directly without Pages needing its own GitHub connection.

Final Pipeline

flowchart LR
    A[git push] --> B[GitHub Actions]
    B --> C[checkout]
    C --> D[setup Rust]
    D --> E["cargo run -- export"]
    E --> F["wrangler pages deploy"]
    F --> G[Cloudflare Pages]

Total CI time: ~2-3 minutes. Cost: $0 (Cloudflare free tier + GitHub Actions free tier).