Tutorial 17 of 21 · for whoever has to put this on a server

Ship it

You'll turn an app into a directory you copy to a box. One binary, one tree, no runtime to install. First you'll run the checks that catch the things that only break in production.

Before you start

Straight from crates.io. No Node, no package manager.

cargo install snapfire_compiler
cargo install snapfire_fsr_cli
fsr --version

Every command and screenshot on this page was captured with fsr 0.x.

Run the checks

$ fsr doctor shop/app
doctor       13 checks, nothing to report

Thirteen checks for the failures that don't show up in development: a static root whose directory isn't there, a route a static prefix swallows so the page beneath it never runs, a mounted site pinned to nothing, a canonical link that's relative. Each one is fine on your machine and wrong on a server.

A finding exits non-zero, so this belongs in CI.

Write the tree

$ fsr bundle shop/app
serve/static/js/app              serves /static/js/app
serve/static/js/vendor           serves /static/js/vendor
serve/static/css                 serves /static/css
serve/static/js/fsr              serves /static/js/fsr
app/clients/shop.openapi.json    read by the host
app/generated/contracts          read by the host
app/generated/plan.sexp          read by the host
app/importmap.json               read by the host
config                           read by the host
config/bundle.toml               read by the host

fsr bundle runs doctor first and refuses on a finding, because the moment to notice is before the tree exists.

Two lists. Everything under serve/ is what a web server may hand out. Everything else is what the host process reads at boot. A .tsx file can't be served by accident, because it isn't in the tree.

Where files go in the tree

Not by where it sat in your project. Configuration goes to config/, what the host reads at boot goes to app/, a static root goes to serve/ at the route it answers.

That matters because your paths are a convenience and a tree's paths are a contract. A [[static]] pointing at dir = "../../shared/assets" is a sensible thing to write in a monorepo and meaningless on a server. The tree keeps the route and drops where it came from.

It's also what keeps the bundle inside its own output directory. Destinations are built from a route or a fixed name, never joined out of a configured path, so no setting can write above --out.

config/bundle.toml

Moving files means your config no longer describes where things are, so the bundle writes config/bundle.toml and the host loads it last, after every environment overlay.

toml
[app]
dir = "app"

[server]
contracts = "generated/contracts"
plan = "generated/plan.sexp"

[document]
entry = "/static/js/app/src/main.js"
import_map = "importmap.json"

[[static]]
dir = "../serve/static/js/app"
route = "/static/js/app"

config/ ships whole, not just the files this run loaded, because the tree may be deployed under a different RELEASE_ENV than the one you bundled under.

Put the binary beside it

$ cp target/release/shop dist/
$ cp fibre_logging.production.yaml dist/fibre_logging.yaml

That's the deploy. A directory and a binary. No Node on the server, no runtime to keep patched, no npm ci in your pipeline.

Point nginx at serve/

text
location ~ ^/static {
  root /srv/shop/dist/serve;
}

location / {
  proxy_pass http://127.0.0.1:3000;
  proxy_set_header Host $host;
}

nginx answers the assets straight off disk. Everything else goes to the host. That proxy_set_header line is what makes ctx.host mean anything, which 300 covers.

Release again

Bundle into a fresh directory, copy it up, move a symlink. The old tree is still there if you need to move it back.

Because destinations are derived rather than copied, bundling the same input twice gives you the same tree byte for byte. That's what lets a mounted site be verified against the hash of the bundle that produced it, which 310 uses for pinning.

Not in the tree

No node_modules, no build step on the production box, no runtime to install. Source files are not in the tree, so a misconfigured path cannot serve one.

Next up: 120. Debug a page in production.

Built with SnapFire FSR. Pure Rust runtime, zero Node.js on the server.

Proudly Created by Excerion Sun LLC