Deploying
Run wisp build, copy the binary, run it. Pick the build for your host:
| You have | Use |
|---|---|
| VPS or server | wisp build; wisp service install keeps it running (VPS) |
| Container host (Fly.io, Railway, Render, Cloud Run, Azure) | wisp build --docker (Docker) |
| Static host (GitHub Pages, GitLab Pages, S3) | wisp build --static (or --spa) |
| Edge or serverless (Cloudflare, Deno Deploy, Vercel, Netlify, Amplify, Firebase, Azure Static Web Apps) | wisp build --target <host> (targets) |
| AWS Lambda / Bun / Node | --target lambda / --target bun / --target node |
An app that signs cookies needs WISP_SECRET (32+ random characters) on every host. Logs, metrics and traces: Observe. Step by step for each host: Hosting.
A plain wisp build inside a host's CI picks the target from its variables and says so: WORKERS_CI or CF_PAGES (cloudflare), VERCEL (output in .vercel/output), NETLIFY, DENO_DEPLOYMENT_ID (deno), AWS_APP_ID (node). --target native forces the plain binary.
wisp deploy init <host> writes a config:
| Host | Writes |
|---|---|
cloudflare, deno, vercel, netlify, lambda, pages (GitHub Pages) | .github/workflows/deploy.yml: build and deploy on push to main; line 1 names the secrets; --force replaces it |
fly, render, railway | that host's config, and a Dockerfile if none |
Binary
wisp build makes one release binary with static files and styles inside. It listens on $HOST:$PORT (0.0.0.0:3000 in release).
HTTP/2 without a proxy: the h2 feature serves h2c with prior knowledge on the same port (for a proxy that speaks h2c, or curl --http2-prior-knowledge). HTTP/1 is unchanged and pays nothing. There is no TLS in the server, so browsers still want the proxy.
# Cargo.toml
wisp = { version = "..", features = ["h2"] }
Static and SPA
wisp build --static [--out site] writes dist/: every parameterless page as about/index.html, plus static/ and the /_app files (.maps with --sourcemap). Forms need a server: the export warns for each page with actions, and for each exported page whose HTML holds a form that posts (method="post", action="?/name"), whichever page its action is on. A [params] route lists its pages:
<!-- src/routes/blog/[slug]/+page.wisp (or its +page.rs) -->
---
fn entries() -> Vec<&'static str> {
vec!["hello", "second-post"]
}
---
entriesreturns aStringor&strper param, or a tuple in path order. For[[optional]]and[...rest]an empty string leaves it out.- A route with actions or a
+server.rsneeds a server (the export warns). --spais--staticplus anindex.htmlfallback (Netlify:/* /index.html 200in_redirects; Cloudflare Pages with no404.html).- A
const SSR: bool = false;page whose[params]have noentriesis written once (params0) to_app/spa/N.html.index.htmllists them, and wisp.js draws the one whose route fits, with that address's params. It gets its data from+page.js.
Prerender
---
const PRERENDER: bool = true;
// with [params]
fn entries() -> Vec<&'static str> {
vec!["hello", "second-post"]
}
let post = db::post(&slug).await?;
---
wisp buildbuilds the binary, runs it once (initruns) to render those pages, builds again with the bytes inside, and serves them as they are (ETag, 304).- Before that (
cargo run, other targets) each worker keeps the first render. - One render serves all, so
cxin statements, markup orloadis a build error. Its layouts render once, as for a request without cookies. --staticprerenders every page.- Under the hood the build calls
wisp::prerenderon the app; you never call it yourself.
Service
wisp build, then wisp service install (as root or administrator) runs the release binary from the app folder as an OS service that starts at boot. Then start, stop, status, uninstall.
| Option | What it does |
|---|---|
--user <name> | run as that user |
--port <n> | listen port |
--name <service> | service name (default: the package name) |
--dry-run | print what would be written and run, change nothing |
- Linux:
/etc/systemd/system/<name>.servicewithRestart=on-failure,EnvironmentFile=-/etc/<name>.env(made 0600 if missing: putWISP_SECRETthere),WorkingDirectory,LimitNOFILE=1048576,User=when given, andAmbientCapabilities=CAP_NET_BIND_SERVICEfor--portbelow 1024. Thendaemon-reload,enable,start. The--usermust read the app folder. - macOS:
/Library/LaunchDaemons/wisp.<name>.plist, loaded withlaunchctl. - Windows: a scheduled task at startup (
schtasks, as SYSTEM). A true Windows service must answer the Service Control Manager, which needsunsafeFFI that Wisp does not have, sostopends the process without draining.
SIGTERM (systemd stop, launchd) and Ctrl+C make the runtime stop accepting and drain for up to 10 seconds.
Docker
wisp build --docker # --force replaces existing files
docker build -t my-app .
docker run -p 3000:3000 -e WISP_SECRET=... my-app
- Writes a two-stage
Dockerfile(rust:slimthendebian:stable-slim,HOST=0.0.0.0,WISP_DATA=/data) and.dockerignore. - Saved tables live in
/data: mount a volume (-v my-app-data:/data). - Docker's default seccomp refuses io_uring, so the server uses an epoll per worker. A profile allowing
io_uring_setup,io_uring_enter,io_uring_registerbrings it back.
Is This Page Useful?