Deploys and releases
A push is source. The server compiles it, writes a new release directory, swaps a symlink, and stops the old process. Nothing is atomic-er than a
rename.
What happens
flowchart TD
A["POST /v1/apps/{name}/deploys<br/>a tar.gz of the app directory"] --> B["unpack into releases/<id>/source"]
B --> C["write spin.toml pointing at this server's stdlib<br/>(the pushed one is ignored)"]
C --> D["melee-build: compile templates, check the Ruby,<br/>run each migration against a scratch database"]
D --> E["spin build: Spinel compiles app + stdlib + SQLite"]
E -- ok --> F["releases/<id>/bin/<name>"]
F --> G["rename() the 'current' symlink"]
G --> H["stop the old warm process and worker"]
E -- error --> X["422 with diagnostics; nothing changes"]
The release id is <millis>-<counter>, created exclusively so two deploys cannot collide on one. The
current symlink is swapped with rename, which is atomic: a request either sees the old release or the new
one, never half of either.
The old warm process is stopped rather than drained. An in-flight request finishes; the next one starts the new release, which costs the usual couple of milliseconds.
What the server does not trust
The tarball is tenant input, and the server treats it that way:
- a
build/directory in the tarball is refused, so an uploaded executable can never become the app; - a symlink entry is refused;
- file modes are masked, so a planted setuid file comes out non-setuid;
spin.tomlis rewritten to point at this server’s standard library, whatever the pushed one said.
What it does not do is sandbox the build itself. melee-build runs as the server’s user with the toolchain
on its PATH, compiling Ruby that somebody pushed. That is fine when the only person who can push is you, and
it is the reason melee is not ready for untrusted app authors. It is open work (SEC-04, and P-01 for signing
what comes out).
One deploy at a time per app. A second concurrent deploy of the same app is refused rather than queued — two builds in one source tree race to activate and the loser’s release ends up live with the winner’s binary. Different apps deploy concurrently.
A failed build changes nothing
app.rb:12: unsupported eval of a runtime string is not supported by AOT compilation
The response is a 422 carrying {diagnostics: [{file, line, message}], output}, and the CLI prints one
line each. No release is activated; the previous one keeps serving. A broken push is never an outage.
Diagnostics are any path.rb:LINE: or path.erb:LINE: found in the build output, made relative to the
pushed source, with template lines mapped back from the generated Ruby to the .erb.
The build is killed at --build-timeout-secs (300 by default) and the deploy fails.
When a release won’t start
A build can succeed and the binary still fail to run: code that raises while the app loads, or
MELEE_SESSION_SECRET missing at boot. When the warm process dies before it has answered a single request,
the server retries once immediately, on the chance it was a one-off. A second failure in a row is taken as
proof the release itself is broken rather than bad luck: the server stops respawning it for 10 seconds and
answers every request in that window with a 503 and Retry-After set to whatever is left of it, instead of
trying to start the process again for each one. The log gets one line per attempt:
[melee] warm process exited before answering its first request; trying again
[melee] warm process exited before answering its first request, 2 times in a row; cooling down for 10s before trying again
A slow first request that simply times out is not held against the release — a cold start under load looks
identical to a dead process from the server’s side, and backing off after one slow request would turn a busy
moment into a ten-second outage. The most common real cause is either your own code raising before any route
can run, or MELEE_SESSION_SECRET being unset: the server generates one and stores it for you on an app’s
first deploy, but an env file that ends up without a usable value some other way (hand-edited outside melee env, say) leaves the runtime nothing to sign sessions with, and it refuses to start rather than run with no
key. Its refusal line shows up verbatim in melee logs:
melee: MELEE_SESSION_SECRET is not set; refusing to start in warm mode (melee-server sets this on deploy; melee-drive needs --env MELEE_SESSION_SECRET=...)
The cooldown is not something to wait out: a new melee push, melee restart, or melee env all clear it
immediately, so once the release is fixed the very next request tries again rather than waiting out the 10
seconds.
Rolling back
There is no rollback command. The releases are all still on disk, but nothing exposes them.
What you can do:
melee push # from the previous source
Re-pushing the previous source is the rollback, and it costs a rebuild. If you need better than that, keep the app in git and tag what you deployed — there is no provenance recorded on the server today (that is P-01: a release manifest and a signature).
Old release directories are not pruned. They accumulate, one per successful deploy, each holding the
source plus a two-megabyte binary. Removing old ones is a manual job; leave current and the one before it.
Environment values
melee env NOTES_SECRET letmein
PUT /v1/apps/{name}/env/{key} writes <home>/apps/<name>/env, mode 0600, replaced by rename. The app is
restarted on its next request, so the change takes effect without a deploy. MELEE_SESSION_SECRET is the one
key with a rule of its own: it has to be at least 32 bytes, and the server refuses anything shorter with a
400.
Two things to know:
- The value crosses the control API in the clear unless you have TLS in front of it.
- Setting an environment value on an app name that has never been deployed creates the app directory and permanently spends a uid from the range. A typo costs an id, and ids are never retired (SEC-30).
Restarting
melee restart
Stops the warm process and the worker. The next request starts them again. Useful after changing something on disk, and harmless — no state lives in those processes that is not also in the database.
Watching a deploy
melee logs -f
[melee] warm process started (pid 91129, 4 slots, sandbox off, cgroup off), release 1789257411211-0000
[melee] killing warm process group 91129 (environment changed)
[melee] warm process exited: signal: 9 (SIGKILL)
Server events are tagged [melee] and are mixed in with the app’s own log lines.