Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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/&lt;id&gt;/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/&lt;id&gt;/bin/&lt;name&gt;"]
  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.toml is 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.

Next