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-pack on 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

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:

  1. Compiles frontend/styles/screen.scss to CSS
  2. Spawns the backend process and polls /api/health_check until it responds
  3. Runs an initial wasm-pack build on the frontend
  4. Starts the HTTP server on http://localhost:8080

Opening the browser automatically

wasm-drydock dev --open

What the server handles

PathBehaviour
/api/*Proxied to the backend
/pkg/*Serves wasm-pack build output
/styles/screen.cssServes compiled SCSS
/ws/reloadWebSocket live reload endpoint
/*SPA fallback — serves index.html

File watchers

The dev server runs five core watchers simultaneously:

WatcherPathFiltersOn change
Frontendfrontend/src/.rswasm-pack build, then browser reload
Backendbackend/src/.rsKill backend, respawn, health check, then browser reload
Stylesfrontend/styles/.scssRecompile SCSS in memory, browser reload
Public assetsfrontend/public/any fileBrowser reload
HTMLfrontend/.htmlBrowser 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:

  1. Runs wasm-pack build --release on the frontend
  2. Compiles frontend/styles/screen.scss to frontend/styles/screen.css
  3. Runs cargo build --release --features embed-assets on 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:

CratePurpose
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 frontend crate directory. wasm-drydock handles target selection automatically when invoking wasm-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 output
  • frontend/pkg/ — wasm-pack build output
  • frontend/styles/screen.css — compiled SCSS (only written by wasm-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 output
  • frontend/pkg/ — wasm-pack output
  • frontend/styles/screen.css — compiled CSS
  • .git/ — version control
  • *.md — documentation
  • fly.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-1x with 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 find configuration/base.yaml. Respects the DRYDOCK_BACKEND_PORT environment variable set by the dev server.
  • startup.rs — Wires up the Actix-web server, routes, and middleware.
  • error.rsApiError enum implementing ResponseError, mapping variants to HTTP status codes.
  • response.rs — Re-exports ApiResponse<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

FlagEffect
embed-assetsEmbeds 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 Serialize and Deserialize

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 grass compiler

File Watchers

Core watchers

Five core watchers run at all times:

WatcherPathFiltersOn change
Frontendfrontend/src/.rswasm-pack build, then browser reload
Backendbackend/src/.rsKill backend, respawn, health check, then browser reload
Stylesfrontend/styles/.scssRecompile SCSS in memory, browser reload
Public assetsfrontend/public/any fileBrowser reload
HTMLfrontend/.htmlBrowser 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

ActionEffect
reloadTrigger browser page reload
rebuildTrigger WASM rebuild then reload
restartTrigger backend restart

Notes

  • Paths are relative to the project root
  • Empty extensions array 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 .scss file in frontend/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]

FieldTypeRequiredDescription
namestringyesThe project name. Used to identify the backend binary.

[dev]

FieldTypeDefaultDescription
public_portinteger8080The port the dev server binds to
backend_portinteger3001The port the backend process binds to
watch_debounce_msinteger300Milliseconds to wait after a file change before triggering a rebuild

[[watch]]

Zero or more custom watch entries. Each entry has the following fields:

FieldTypeRequiredDescription
pathstringyesPath to watch, relative to the project root
extensionsarray of stringsnoFile extensions to filter. Empty array watches all files.
actionstringyesOne of "reload", "rebuild", "restart"

Environment variables

The backend respects the following environment variables at runtime:

VariableDescription
DRYDOCK_BACKEND_PORTOverrides the backend port. Set automatically by the dev server.
APP_ENVIRONMENTSelects the environment config file (local or production). Defaults to local.
APP_APPLICATION__HOSTOverrides the bind host. Use 0.0.0.0 for production deployments.
APP_APPLICATION__PORTOverrides 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:

  1. Runs wasm-pack build --release on the frontend
  2. Compiles frontend/styles/screen.scss to frontend/styles/screen.css
  3. Runs cargo build --release --features embed-assets on 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

AssetSourceMechanism
WASM and JSfrontend/pkg/include_dir!
HTMLfrontend/index.htmlinclude_bytes!
CSSfrontend/styles/screen.cssinclude_bytes!
Public filesfrontend/public/include_dir!
Configurationconfiguration/base.yamlinclude_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:

  1. wasm-pack build --release on the frontend
  2. grass compiles SCSS to CSS
  3. cargo build --release --features embed-assets on 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 — Toronto
  • sea — Seattle
  • lax — Los Angeles
  • fra — Frankfurt
  • sin — 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:

  1. The existing child process is taken from the Option, killed, and awaited
  2. A new backend process is spawned
  3. The dev server polls /api/health_check until the new backend is ready
  4. 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

  1. A watcher detects a file change and sends a trigger to the build coordinator
  2. The build coordinator runs the build
  3. On success, it sends DevServerMessage::Reload on the broadcast channel
  4. On failure, it sends DevServerMessage::BuildError(message)
  5. The WebSocket handler receives the message and forwards it to the browser
  6. 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 frame
  • BuildError — sends "error:<message>" text frame
  • RecvError::Lagged — subscriber fell behind, sends one reload
  • RecvError::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 from frontend/pkg/ (wasm-pack output)
  • /styles/screen.css — CSS compiled from SCSS, held in memory
  • /favicon.png and other public files — files from frontend/public/
  • /* — SPA fallback serves frontend/index.html with the reload script injected

Release mode (embed-assets feature)

All assets are embedded into the binary at compile time:

  • frontend/pkg/ — embedded via include_dir!
  • frontend/index.html — embedded via include_bytes!
  • frontend/styles/screen.css — embedded via include_bytes!
  • frontend/public/ — embedded via include_dir!
  • configuration/base.yaml — embedded via include_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

Yew

Actix-web

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

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
  • 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 init command does