wasm-drydock
wasm-drydock is a single-command dev tool for fullstack Rust web applications using Actix-web and Yew.
Most fullstack Rust setups require you to manage two separate processes: one for the frontend WASM build and one for the backend server. wasm-drydock replaces both with a single command that owns the entire development loop.
What it does
wasm-drydock dev
This starts a dev server that:
- Builds the frontend with
wasm-packon startup - Spawns your Actix-web backend and waits for it to be ready
- Proxies
/api/*requests to the backend - Watches source files and rebuilds on changes
- Recompiles SCSS on the fly
- Pushes build errors directly to the browser
- Signals the browser to reload after successful builds
When you are ready to ship, wasm-drydock release produces a single self-contained binary with all frontend assets compiled in — no separate static file server required.
Prerequisites
- Rust (edition 2024)
- wasm-pack >= 0.13.0
- wasm32-unknown-unknown target:
rustup target add wasm32-unknown-unknown
Getting Started
This section walks you through everything you need to go from zero to a running wasm-drydock project.
- Installation — install the prerequisites and wasm-drydock itself
- Creating a Project — scaffold a new three-crate workspace with
wasm-drydock init - The Dev Server — start the dev server and understand what it does
- Release Build — produce a self-contained release binary
Installation
Prerequisites
Before installing wasm-drydock, ensure the following are available on your system.
Rust toolchain (edition 2024)
Install via rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
wasm-pack
wasm-drydock uses wasm-pack to compile your Yew frontend to WebAssembly.
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
Minimum required version: 0.13.0. Verify with:
wasm-pack --version
wasm32-unknown-unknown target
rustup target add wasm32-unknown-unknown
Installing wasm-drydock
cargo install wasm-drydock
Verify the installation:
wasm-drydock --help
Updating
cargo install wasm-drydock --force
Creating a Project
Scaffold a new project
wasm-drydock init my-app
cd my-app
wasm-drydock init validates your environment before scaffolding — it checks that wasm-pack is installed, meets the minimum version requirement, and that the wasm32-unknown-unknown target is present. If any check fails, it reports the problem and exits cleanly.
The generated project is a three-crate Cargo workspace with deployment files for Fly.io:
my-app/
├── .cargo/config.toml
├── .dockerignore
├── .gitignore
├── Cargo.toml
├── Dockerfile
├── drydock.toml
├── fly.toml
├── backend/
├── frontend/
└── shared/
See Project Structure for a detailed walkthrough of what each crate contains and why.
Skip deployment files
If you don't need Fly.io deployment configuration, use the --no-deploy flag:
wasm-drydock init my-app --no-deploy
This skips generating Dockerfile, .dockerignore, and fly.toml.
Project name rules
Project names follow Cargo conventions:
- 1–64 characters
- Alphanumeric characters, hyphens, and underscores only
- Cannot start with a hyphen or underscore
- Cannot be a Rust keyword or reserved Cargo command name
The Dev Server
Starting the server
From your project root:
wasm-drydock dev
On startup the dev server:
- Compiles
frontend/styles/screen.scssto CSS - Spawns the backend process and polls
/api/health_checkuntil it responds - Runs an initial
wasm-pack buildon the frontend - Starts the HTTP server on
http://localhost:8080
Opening the browser automatically
wasm-drydock dev --open
What the server handles
| Path | Behaviour |
|---|---|
/api/* | Proxied to the backend |
/pkg/* | Serves wasm-pack build output |
/styles/screen.css | Serves compiled SCSS |
/ws/reload | WebSocket live reload endpoint |
/* | SPA fallback — serves index.html |
File watchers
The dev server runs five core watchers simultaneously:
| Watcher | Path | Filters | On change |
|---|---|---|---|
| Frontend | frontend/src/ | .rs | wasm-pack build, then browser reload |
| Backend | backend/src/ | .rs | Kill backend, respawn, health check, then browser reload |
| Styles | frontend/styles/ | .scss | Recompile SCSS in memory, browser reload |
| Public assets | frontend/public/ | any file | Browser reload |
| HTML | frontend/ | .html | Browser reload |
See File Watchers for custom watch path configuration.
Error overlay
When a build fails, the error is pushed to the browser via WebSocket and displayed as a full-screen overlay. The overlay is dismissed automatically when the next successful build completes.
Release Build
wasm-drydock release
This command:
- Runs
wasm-pack build --releaseon the frontend - Compiles
frontend/styles/screen.scsstofrontend/styles/screen.css - Runs
cargo build --release --features embed-assetson the backend
The result is a single self-contained binary in target/release/ with all frontend assets — WASM, JavaScript, CSS, index.html, and public assets — compiled in. The binary has zero runtime filesystem dependencies.
Configuration at runtime
The release binary embeds configuration/base.yaml at compile time. To override settings at runtime without recompiling, place an environment-specific YAML file next to the binary:
my-app-backend ← the binary
configuration/
└── production.yaml ← optional override
APP_* environment variables are always applied last and override everything:
APP_APPLICATION__HOST=0.0.0.0 ./my-app-backend
Project Structure
A wasm-drydock project is a three-crate Cargo workspace. Each crate has a clearly defined responsibility:
| Crate | Purpose |
|---|---|
backend/ | Actix-web API server |
frontend/ | Yew WASM application |
shared/ | Serde-compatible API types |
The shared crate is the boundary between frontend and backend. Both crates depend on it, so serialization mismatches become compile errors rather than runtime surprises.
Workspace
A wasm-drydock project is a Cargo workspace containing three member crates: backend, frontend, and shared.
Cargo.toml
The workspace manifest at the project root lists all three members and pins shared dependency versions in [workspace.dependencies]:
[workspace]
members = ["backend", "frontend", "shared"]
resolver = "2"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
.cargo/config.toml
The workspace-level Cargo configuration sets the default build target for the frontend crate:
[build]
target = "wasm32-unknown-unknown"
Note: This applies only when building inside the
frontendcrate directory. wasm-drydock handles target selection automatically when invokingwasm-pack.
drydock.toml
The wasm-drydock configuration file at the workspace root. See Configuration Reference for all available fields.
.gitignore
The generated .gitignore excludes:
target/— Cargo build outputfrontend/pkg/— wasm-pack build outputfrontend/styles/screen.css— compiled SCSS (only written bywasm-drydock release)
Deployment Files
By default, wasm-drydock init generates deployment files for Fly.io:
Dockerfile
A multi-stage Docker build using cargo-chef for layer caching. Builds the frontend with wasm-pack, compiles SCSS, and produces a minimal production image with the backend binary.
.dockerignore
Excludes unnecessary files from Docker build context:
target/— Cargo build outputfrontend/pkg/— wasm-pack outputfrontend/styles/screen.css— compiled CSS.git/— version control*.md— documentationfly.toml— Fly.io config (not needed in image)
fly.toml
Fly.io deployment configuration with sensible defaults:
- Internal port 3001
- HTTPS forced
- Auto-scaling (stop/start machines)
shared-cpu-1xwith 256MB memory
See Fly.io deployment for details.
Skipping deployment files
Use --no-deploy to skip these files:
wasm-drydock init my-app --no-deploy
Backend
The scaffolded backend is an opinionated Actix-web starter. It is structured around a small set of focused modules:
configuration.rs— YAML-based configuration loading. Walks up the directory tree to findconfiguration/base.yaml. Respects theDRYDOCK_BACKEND_PORTenvironment variable set by the dev server.startup.rs— Wires up the Actix-web server, routes, and middleware.error.rs—ApiErrorenum implementingResponseError, mapping variants to HTTP status codes.response.rs— Re-exportsApiResponse<T>from the shared crate for use in handlers.telemetry.rs— Tracing subscriber setup with Bunyan-formatted JSON output.static_assets.rs— Serves embedded frontend assets in release builds. Compiled out in development.api/mod.rs— Route configuration with starter endpoints:/api/health_check,/api/hello,/api/status.
Integration tests
Integration test scaffolding lives in backend/tests/api/ with a TestApp helper that spawns the server on a random port, plus a sample health check test to build on.
Feature flags
| Flag | Effect |
|---|---|
embed-assets | Embeds all frontend assets and configuration/base.yaml into the binary at compile time |
Frontend
The frontend is a Yew CSR application compiled to WebAssembly via wasm-pack.
Entry point
frontend/src/lib.rs contains the root App component and the #[wasm_bindgen(start)] entry point that initialises the logger, panic hook, and Yew renderer.
Styles
SCSS lives in frontend/styles/screen.scss. The dev server compiles it to CSS in memory and serves it at /styles/screen.css. It is never written to disk during development — the compiled screen.css is only produced by wasm-drydock release.
A CSS reset based on Josh Comeau's modern reset is included by default.
Public assets
Static files placed in frontend/public/ are served as-is at their filename. For example, frontend/public/favicon.png is served at /favicon.png.
index.html
frontend/index.html bootstraps the application. The WASM module is loaded via an ES module script:
<script type="module">
import init from '/pkg/my_app_frontend.js';
init();
</script>
Note the absolute path — relative paths break on SPA subroutes.
Shared
The shared crate contains the API boundary types used by both backend and frontend.
Both crates depend on shared, so serialization mismatches between the API and the frontend are caught at compile time rather than at runtime.
Types
#![allow(unused)] fn main() { pub struct ApiResponse<T> { pub success: bool, pub data: Option<T>, pub error: Option<String>, } pub struct HelloResponse { pub message: String, } pub struct StatusResponse { pub version: String, pub uptime_seconds: u64, } }
Design notes
- Types do not use
#[serde(deny_unknown_fields)]— unknown fields are silently ignored, which ensures forward compatibility as the API evolves - Compile-time assertions verify that all types implement
SerializeandDeserialize
Dev Server
The wasm-drydock dev server owns the entire development loop — frontend builds, backend process management, file watching, live reload, and SCSS compilation — under a single command.
- File Watchers — the five core watchers and how to configure custom watch paths
- Hot Reload — how the browser is signalled to reload after a successful build
- Error Overlay — how build failures are surfaced in the browser
- SCSS Compilation — in-memory SCSS compilation with the
grasscompiler
File Watchers
Core watchers
Five core watchers run at all times:
| Watcher | Path | Filters | On change |
|---|---|---|---|
| Frontend | frontend/src/ | .rs | wasm-pack build, then browser reload |
| Backend | backend/src/ | .rs | Kill backend, respawn, health check, then browser reload |
| Styles | frontend/styles/ | .scss | Recompile SCSS in memory, browser reload |
| Public assets | frontend/public/ | any file | Browser reload |
| HTML | frontend/ | .html | Browser reload |
Custom watch paths
Additional watch paths can be configured in drydock.toml using [[watch]] entries:
[[watch]]
path = "frontend/content"
extensions = ["md", "mdx"]
action = "reload"
[[watch]]
path = "frontend/assets"
extensions = []
action = "reload"
[[watch]]
path = "shared/src"
extensions = ["rs"]
action = "rebuild"
Available actions
| Action | Effect |
|---|---|
reload | Trigger browser page reload |
rebuild | Trigger WASM rebuild then reload |
restart | Trigger backend restart |
Notes
- Paths are relative to the project root
- Empty
extensionsarray watches all files - Non-existent paths are logged as warnings and skipped
Hot Reload
The dev server injects a small JavaScript snippet into index.html before serving it in development mode. This snippet establishes a WebSocket connection to /ws/reload.
When a build or reload event occurs, the server sends a message over the WebSocket:
"reload"— the browser reloads the page"error:<message>"— the browser displays the error overlay
The snippet reconnects automatically if the WebSocket connection drops, with a one second delay. This means the browser reconnects seamlessly if the dev server restarts.
The reload script is never present in release builds.
Error Overlay
When a build fails during a watch cycle, the error message is sent to all connected browser tabs via WebSocket. The browser displays a full-screen overlay showing the error.
The overlay:
- Appears immediately when a build failure is detected
- Shows the full error message from the build tool
- Is dismissed automatically when the next successful build completes
- Does not require a page refresh to appear or disappear
This means you can keep your browser and editor side by side — build errors surface in the browser without needing to switch to the terminal.
SCSS compilation failures during a watch cycle are also surfaced via the overlay.
SCSS Compilation
The dev server compiles frontend/styles/screen.scss using the grass SCSS compiler and serves the result at /styles/screen.css.
Compilation happens:
- On startup
- Whenever a
.scssfile infrontend/styles/changes
The compiled CSS is held in memory and never written to disk during development. frontend/styles/screen.css is listed in .gitignore for this reason.
If SCSS compilation fails at startup, a warning is logged and the server continues with an empty stylesheet. The error overlay will display the compiler message on subsequent failures during the watch cycle.
Release builds
wasm-drydock release writes frontend/styles/screen.css to disk so it can be embedded into the release binary.
Configuration
wasm-drydock is configured via drydock.toml at the workspace root. The file is created automatically by wasm-drydock init and contains sensible defaults that work for most projects.
- Reference — complete field-by-field reference for
drydock.toml, including[[watch]]entries and environment variable overrides
Configuration Reference
drydock.toml lives at the workspace root. All [dev] fields are optional and fall back to the defaults shown.
[project]
name = "my-app"
[dev]
public_port = 8080 # browser-facing port
backend_port = 3001 # internal backend port
watch_debounce_ms = 300 # file change debounce interval in milliseconds
[project]
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | The project name. Used to identify the backend binary. |
[dev]
| Field | Type | Default | Description |
|---|---|---|---|
public_port | integer | 8080 | The port the dev server binds to |
backend_port | integer | 3001 | The port the backend process binds to |
watch_debounce_ms | integer | 300 | Milliseconds to wait after a file change before triggering a rebuild |
[[watch]]
Zero or more custom watch entries. Each entry has the following fields:
| Field | Type | Required | Description |
|---|---|---|---|
path | string | yes | Path to watch, relative to the project root |
extensions | array of strings | no | File extensions to filter. Empty array watches all files. |
action | string | yes | One of "reload", "rebuild", "restart" |
Environment variables
The backend respects the following environment variables at runtime:
| Variable | Description |
|---|---|
DRYDOCK_BACKEND_PORT | Overrides the backend port. Set automatically by the dev server. |
APP_ENVIRONMENT | Selects the environment config file (local or production). Defaults to local. |
APP_APPLICATION__HOST | Overrides the bind host. Use 0.0.0.0 for production deployments. |
APP_APPLICATION__PORT | Overrides the bind port. |
Building & Deployment
wasm-drydock produces a single self-contained binary that embeds all frontend assets. This makes deployment straightforward — there is no separate static file server to manage.
- Release Binary — build a release binary with
wasm-drydock release - Docker — build a minimal production image using the generated
Dockerfile - Fly.io — deploy to Fly.io with
fly deploy
Release Binary
wasm-drydock release
This command:
- Runs
wasm-pack build --releaseon the frontend - Compiles
frontend/styles/screen.scsstofrontend/styles/screen.css - Runs
cargo build --release --features embed-assetson the backend
The result is a single self-contained binary in target/release/ with all frontend assets — WASM, JavaScript, CSS, index.html, and public assets — compiled in via the embed-assets feature flag.
What is embedded
| Asset | Source | Mechanism |
|---|---|---|
| WASM and JS | frontend/pkg/ | include_dir! |
| HTML | frontend/index.html | include_bytes! |
| CSS | frontend/styles/screen.css | include_bytes! |
| Public files | frontend/public/ | include_dir! |
| Configuration | configuration/base.yaml | include_str! |
The binary has zero runtime filesystem dependencies.
Running the release binary
./target/release/my-app-backend
Configuration at runtime
The release binary embeds configuration/base.yaml at compile time. To override settings at runtime without recompiling, place an environment-specific YAML file next to the binary:
my-app-backend ← the binary
configuration/
└── production.yaml ← optional override
APP_* environment variables are always applied last and override everything:
APP_APPLICATION__HOST=0.0.0.0 ./my-app-backend
Docker
A Dockerfile and .dockerignore are included in every project scaffolded by wasm-drydock init. The Dockerfile uses a multi-stage build to produce a minimal production image.
Build stages
chef — base image with all build tools installed:
- Rust toolchain
- wasm-pack
- wasm32-unknown-unknown target
- cargo-chef for dependency caching
planner — analyses the dependency graph and produces recipe.json
builder — cooks dependencies from recipe.json, then builds the project:
wasm-pack build --releaseon the frontendgrasscompiles SCSS to CSScargo build --release --features embed-assetson the backend
Final stage — copies only the release binary into a minimal debian:bookworm-slim image.
Building
docker build -t my-app .
Running
docker run -p 3001:3001 \
-e APP_ENVIRONMENT=production \
-e APP_APPLICATION__HOST=0.0.0.0 \
my-app
Skipping deployment files
If you don't need deployment files, use the --no-deploy flag:
wasm-drydock init my-app --no-deploy
Fly.io
A fly.toml configuration file is included in every project scaffolded by wasm-drydock init. This provides zero-configuration deployment to Fly.io.
Prerequisites
- flyctl installed and authenticated
Quick Deploy
The scaffolded project includes everything needed for deployment:
# Register the app with Fly (one-time setup)
fly launch --no-deploy
# Deploy
fly deploy
The Docker build runs on your local machine. The resulting image is pushed to Fly's registry and deployed.
Generated fly.toml
The scaffolded fly.toml is pre-configured:
app = "your-app-name" # Uses your project name
primary_region = "yyz" # Toronto (change as needed)
[build]
[env]
APP_ENVIRONMENT = "production"
APP_APPLICATION__HOST = "0.0.0.0"
[http_service]
internal_port = 3001
force_https = true
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 0
[[vm]]
size = "shared-cpu-1x"
memory = "256mb"
Customization
Change the primary region
Edit fly.toml and change primary_region to your preferred region. Common options:
yyz— Torontosea— Seattlelax— Los Angelesfra— Frankfurtsin— Singapore
See all regions with fly regions list.
Scale the VM
Edit the [[vm]] section in fly.toml:
[[vm]]
size = "shared-cpu-2x" # Larger CPU
memory = "512mb" # More memory
Custom domain
Add your domain in the Fly dashboard under Certificates, then point your DNS at Fly's servers. Fly provisions the TLS certificate automatically.
Important: bind address
Fly's proxy routes external traffic to your app's internal port. Your app must bind to 0.0.0.0 rather than 127.0.0.1. The APP_APPLICATION__HOST = "0.0.0.0" environment variable in fly.toml handles this.
Skipping deployment files
If you don't need Fly.io deployment, use the --no-deploy flag when creating your project:
wasm-drydock init my-app --no-deploy
Architecture
This section covers the internal design of the wasm-drydock dev server.
- Build Coordinator — serializes rebuild requests and coalesces rapid file changes
- Process Manager — owns the backend child process and handles restarts
- Reload Channel — broadcasts reload and error messages to connected browser tabs
- Static Assets — how assets are served differently in development and release modes
Build Coordinator
src/build/build_coordinator.rs
The build coordinator serializes rebuild requests, ensuring builds never run concurrently and that rapid file changes don't trigger redundant builds.
Coalescing
The coordinator implements a one-pending-build policy:
- If a build is running and a new trigger arrives, the trigger is noted as pending
- When the running build finishes, the pending build starts immediately
- Additional triggers that arrive while a build is running and one is already pending are discarded
This means at most two builds are ever queued: one running and one pending.
Error handling
Build failures are logged at warn level but do not exit the loop. The server continues running and serving the last successful build. Only channel closure exits the loop.
Reload signaling
After a successful build, a DevServerMessage::Reload is sent on the broadcast channel. After a failed build, a DevServerMessage::BuildError is sent, which the browser displays as an error overlay.
Process Manager
src/dev/mod.rs
The ProcessManager struct owns the backend child process for the lifetime of the dev server.
Lifecycle
On new(), the backend is spawned via tokio::process::Command with kill_on_drop(true). This means the backend process is automatically killed if ProcessManager is dropped for any reason — clean exit, panic, or error propagation.
Restart
When a change is detected in backend/src/, ProcessManager::restart() is called:
- The existing child process is taken from the
Option, killed, and awaited - A new backend process is spawned
- The dev server polls
/api/health_checkuntil the new backend is ready - A reload signal is sent to connected browsers
Both kill() and wait() are async operations using tokio::process::Child, so they yield properly and do not block the Tokio runtime.
Graceful shutdown
kill_on_drop(true) on tokio::process::Child ensures the backend process is cleaned up on any exit path without requiring an explicit Drop implementation.
Reload Channel
src/build/reload.rs
The reload channel is a tokio::sync::broadcast channel that carries DevServerMessage values from the server to all connected browser tabs.
Message types
#![allow(unused)] fn main() { pub enum DevServerMessage { Reload, BuildError(String), } }
Flow
- A watcher detects a file change and sends a trigger to the build coordinator
- The build coordinator runs the build
- On success, it sends
DevServerMessage::Reloadon the broadcast channel - On failure, it sends
DevServerMessage::BuildError(message) - The WebSocket handler receives the message and forwards it to the browser
- The browser either reloads or displays the error overlay
WebSocket handler
ws_reload_handler upgrades the HTTP connection to WebSocket and subscribes to the broadcast channel. It handles:
Reload— sends"reload"text frameBuildError— sends"error:<message>"text frameRecvError::Lagged— subscriber fell behind, sends one reloadRecvError::Closed— broadcast channel closed, exits loop- Client disconnect — exits loop cleanly
Static Assets
Static asset serving behaves differently in development and release modes.
Development mode
Assets are served from the filesystem at runtime:
/pkg/*— files fromfrontend/pkg/(wasm-pack output)/styles/screen.css— CSS compiled from SCSS, held in memory/favicon.pngand other public files — files fromfrontend/public//*— SPA fallback servesfrontend/index.htmlwith the reload script injected
Release mode (embed-assets feature)
All assets are embedded into the binary at compile time:
frontend/pkg/— embedded viainclude_dir!frontend/index.html— embedded viainclude_bytes!frontend/styles/screen.css— embedded viainclude_bytes!frontend/public/— embedded viainclude_dir!configuration/base.yaml— embedded viainclude_str!
No filesystem access is required at runtime. The binary is fully self-contained.
Path traversal protection
All asset handlers extract only the final path component via Path::file_name() before serving. This prevents path traversal attacks such as /pkg/../../etc/passwd.
Resources
A curated list of resources for the technologies wasm-drydock is built on and around.
WebAssembly
- WebAssembly Official Site — specification, concepts, and use cases
- Rust and WebAssembly Book — the definitive guide to compiling Rust to WASM
- wasm-pack — the tool wasm-drydock uses to build your frontend
- wasm-bindgen — how Rust and JavaScript interoperate at the WASM boundary
Yew
- Yew Documentation — components, hooks, routing, and more
- Yew 0.22 Release Notes — what changed in the version wasm-drydock targets
- Yew GitHub
- gloo — toolkit for building WASM applications, used by Yew internally
Actix-web
- Actix-web Documentation — handlers, middleware, extractors
- Actix-web API Reference — full API docs on docs.rs
Trunk
- Trunk — the WASM bundler wasm-drydock does not use but which you may encounter in the Yew ecosystem. Understanding trunk helps clarify what wasm-drydock replaces.
Rust Async
- Tokio Documentation — the async runtime wasm-drydock is built on
- Tokio Tutorial — recommended reading for understanding the async foundations
Deployment
- Fly.io Documentation — the deployment platform covered in this guide
- cargo-chef — Docker layer caching for Rust projects, used in the wasm-drydock Dockerfile
- Caddy — a simple reverse proxy with automatic HTTPS, a good alternative to Fly for VPS deployments
Related Projects
- Leptos — a full-stack Rust framework with SSR support, worth knowing about as the ecosystem matures
- Dioxus — another Rust UI framework targeting multiple platforms including WASM
- cargo-generate — template-based project scaffolding, an alternative approach to what wasm-drydock's
initcommand does