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

Running melee-server

One process, two ports, one directory. Every flag, what it defaults to, and the two it refuses to start without.

melee-server \
  --home /var/lib/melee \
  --listen 0.0.0.0:8080 \
  --api 127.0.0.1:7070 \
  --api-token-file /etc/melee/token \
  --domain apps.example.com \
  --app-uid-range 60000-60999 \
  --stdlib /opt/melee/stdlib \
  --spin /opt/melee/vendor/spinel/bin/spin \
  --ruby /usr/bin/ruby \
  --tz Europe/London

The two ports

--listen (default 127.0.0.1:8080) is where visitors arrive. The app is chosen by the Host header against --domain: with --domain apps.example.com, kitchen.apps.example.com is the app named kitchen. Plain HTTP only — put a TLS terminator in front.

--api (default 127.0.0.1:7070) is the control plane: deploys, logs, env, restarts, objects. It is what melee push talks to. Keep it on loopback and reach it over SSH, or put TLS in front of it; the token and every environment value you set cross it in the clear.

RouteWhat
GET /v1/healthok — and it needs the token too; there is no unauthenticated corner
GET /v1/appswhat is deployed
POST /v1/apps/{name}/deploysa tarball; builds and activates
GET /v1/apps/{name}/logsthe log, with follow
PUT /v1/apps/{name}/env/{key}set an environment value
POST /v1/apps/{name}/restartstop the warm process
GET /v1/apps/{name}/objects, POST .../objects/destroydurable objects

The two it refuses to start without

A token. --api-token-file is required — there is no unauthenticated mode, even on loopback. Every control request must carry Authorization: Bearer <the file's contents>.

install -m 600 -o root -g root /dev/null /etc/melee/token
head -c 32 /dev/urandom | base64 | tr -d '\n=' > /etc/melee/token

A uid range, when running as root. --app-uid-range LOW-HIGH gives every app a Unix user and group of its own out of that range, which is what keeps one app out of another’s files, processes and signals. Running as root without it is refused outright, because every app would then run as root. Pick a range no other user on the machine uses:

--app-uid-range 60000-60999

Running as a non-root user is allowed and is what development does. You still get Landlock and seccomp; you do not get per-app uids or cgroup limits, and the server says so at startup:

WARN not root, so every app runs as uid 501: one app can read another's session secret out of /proc and
signal its processes. Single-tenant and development only; multi-tenant needs root and --app-uid-range

Every flag

FlagDefaultWhat
--home./melee-homeApps, releases, data, logs. Everything melee knows lives here.
--listen127.0.0.1:8080Where app traffic arrives.
--api127.0.0.1:7070The control API.
--api-token-filerequiredFile holding the control-API bearer token.
--domainlocalhostSuffix that maps Host to an app name.
--trusted-proxynoneIP or CIDR (repeatable, comma-separated) of an edge allowed to set X-Forwarded-Proto, X-Forwarded-For and X-Melee-App. Unset: http, the socket peer, and Host-only routing.
--edge-secret-filenoneFile holding a shared secret (mode 600, 32+ bytes) a --trusted-proxy peer must also send as X-Melee-Edge to be believed. Without it, any local process at a trusted address can pass as the edge.
--app-uid-rangenoneLOW-HIGH; required as root, refused as anyone else.
--slots4Requests at once per app. Also the number of socket pairs handed to each warm process.
--idle-secs300Idle before a warm process is stopped.
--request-timeout-secs30Then the process group is killed and the caller gets a 504.
--build-timeout-secs300Then the build is killed and the deploy fails.
--max-body8388608Largest accepted request body, in bytes.
--memory-max128Mcgroup memory.max per app (root only).
--cpu-weight100cgroup cpu.weight per app, 1–10000 (root only).
--no-sandboxoffRun apps with no kernel sandbox. Development, and macOS where there is none.
--stdlib../stdlibThe melee standard library checkout, used by deploys.
--spin../vendor/spinel/bin/spinThe Spinel compiler, used by deploys.
--rubyrubyRuby used to run the build step.
--tzUTCTZ handed to app processes.

Never use --no-sandbox on a machine serving anything real. It is there so the server can run on macOS, where the sandbox does not exist.

Choosing the timeouts and slots

They are per-server, not per-app, which is a real limitation if you host apps with different shapes.

  • --slots is both the concurrency limit and the number of file descriptors per warm app. Raising it lets one app use more of the machine and costs descriptors; the server logs the descriptor limit at startup.
  • --idle-secs is the whole economics of melee. Low means apps are stopped sooner and cold starts are more frequent (about 2 ms). High means memory is held for apps nobody is using.
  • --request-timeout-secs bounds the damage from a stuck request. Do not raise it to accommodate a slow route; move the slow work to a timer.

The home directory

<home>/
  apps/<name>/
    releases/<id>/source/      the pushed source, plus the spin.toml the server wrote
    releases/<id>/bin/<name>   the compiled binary
    current -> releases/<id>   swapped atomically on deploy
    data/                      the app's SQLite databases; the only place it can write
    env                        KEY=VALUE, 0600, delivered as environment
  logs/<name>.ndjson           one JSON object per line
  uids/<id>                    which app owns which uid

There is no database in the server; the filesystem is the registry. Two things follow:

  • The home directory is the whole state of the system. Back it up, and understand that doing so means copying live SQLite files (see Logs, backups and upgrades).
  • Its permissions matter. App processes must be able to traverse it to reach their own release. A --home inside a directory the app uids cannot traverse, or a server started with a restrictive umask, produces an app that looks broken rather than a clear error.

Keeping it running

There is no service unit shipped with melee, and writing one is open work (P-06 in the project’s TASKS.md). What a unit has to get right, if you write one:

  • Run as root and pass --app-uid-range, or it is single-tenant.
  • TimeoutStopSec long enough for an orderly stop. SIGTERM makes the server stop the app processes it owns; killing it instead leaves them behind.
  • Restart on failure, and expect app processes to be restarted on demand afterwards — nothing is lost by a server restart except warm processes.
  • A umask that leaves release trees readable by the app uids (see above).

For development, scripts/server.sh in the repository runs it in the foreground on macOS and backgrounded as root on Linux. It is a development helper and assumes the checkout; do not deploy with it.

Next