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.
| Route | What |
|---|---|
GET /v1/health | ok — and it needs the token too; there is no unauthenticated corner |
GET /v1/apps | what is deployed |
POST /v1/apps/{name}/deploys | a tarball; builds and activates |
GET /v1/apps/{name}/logs | the log, with follow |
PUT /v1/apps/{name}/env/{key} | set an environment value |
POST /v1/apps/{name}/restart | stop the warm process |
GET /v1/apps/{name}/objects, POST .../objects/destroy | durable 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
| Flag | Default | What |
|---|---|---|
--home | ./melee-home | Apps, releases, data, logs. Everything melee knows lives here. |
--listen | 127.0.0.1:8080 | Where app traffic arrives. |
--api | 127.0.0.1:7070 | The control API. |
--api-token-file | required | File holding the control-API bearer token. |
--domain | localhost | Suffix that maps Host to an app name. |
--trusted-proxy | none | IP 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-file | none | File 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-range | none | LOW-HIGH; required as root, refused as anyone else. |
--slots | 4 | Requests at once per app. Also the number of socket pairs handed to each warm process. |
--idle-secs | 300 | Idle before a warm process is stopped. |
--request-timeout-secs | 30 | Then the process group is killed and the caller gets a 504. |
--build-timeout-secs | 300 | Then the build is killed and the deploy fails. |
--max-body | 8388608 | Largest accepted request body, in bytes. |
--memory-max | 128M | cgroup memory.max per app (root only). |
--cpu-weight | 100 | cgroup cpu.weight per app, 1–10000 (root only). |
--no-sandbox | off | Run apps with no kernel sandbox. Development, and macOS where there is none. |
--stdlib | ../stdlib | The melee standard library checkout, used by deploys. |
--spin | ../vendor/spinel/bin/spin | The Spinel compiler, used by deploys. |
--ruby | ruby | Ruby used to run the build step. |
--tz | UTC | TZ 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.
--slotsis 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-secsis 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-secsbounds 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
--homeinside 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. TimeoutStopSeclong enough for an orderly stop.SIGTERMmakes 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.