Services
A service is a process loam keeps running for you. It starts at every boot of the sandbox, is restarted when it crashes (with a growing pause between tries), and survives both kinds of sleep: a pause keeps it running in the memory snapshot, and after a stop it starts again on the next boot. Use services for what a sandbox should always be running: its web server, a database, a worker. A command you start with exec is gone after a stop; a service isn't.
Service definitions are stored on the sandbox's disk (/var/lib/loam/services/<name>.json), so
they travel with checkpoints and forks. Output goes to rotating logs on the disk (8 MiB, two old
files kept).
| Method and path | |
|---|---|
GET /v1/sandboxes/{sandbox}/services |
→ {"services": [Service]} |
PUT /v1/sandboxes/{sandbox}/services/{name} |
create or replace, and start → Service |
GET /v1/sandboxes/{sandbox}/services/{name} |
→ Service |
DELETE /v1/sandboxes/{sandbox}/services/{name} |
stop it and remove it → 204 |
POST /v1/sandboxes/{sandbox}/services/{name}/{start|stop|restart} |
→ Service |
GET /v1/sandboxes/{sandbox}/services/{name}/logs?follow=false&tail=N&wake=true |
text/plain, stdout and stderr interleaved. Following (follow=true) is not activity: it wakes the sandbox but doesn't keep it from going idle, and a pause ends it. wake=false: a sandbox that isn't running is 409 conflict, not woken (for a log shipper) |
The definition
PUT .../services/{name}:
| Field | |
|---|---|
cmd |
argv; required |
cwd |
working directory |
env |
{"K": "V"} on top of the sandbox's env |
user |
default loam |
needs |
names of services to start first |
http_port |
the port this service serves; a request to that port's URL starts the service if it isn't running |
The answer, and GET, is the definition plus its state: state (running, starting,
stopped, crashed), pid, restarts and started_at. stopped is a service you stopped; it
stays stopped until you start it, a request reaches its http_port, or the next boot.
$ curl -sS -H "Authorization: Bearer $LOAM_TOKEN" -H 'Content-Type: application/json' -X PUT \
"$LOAM_API_URL/v1/sandboxes/svc-demo/services/ticker" \
-d '{"cmd":["sh","-c","while true; do date; sleep 5; done"],"env":{"TZ":"UTC"}}'
{"name":"ticker","cmd":["sh","-c","while true; do date; sleep 5; done"],"env":{"TZ":"UTC"},"state":"running","pid":296,"restarts":0,"started_at":"2026-10-12T09:18:51.448059501Z"}
$ curl -sS -H "Authorization: Bearer $LOAM_TOKEN" -H 'Content-Type: application/json' -X PUT \
"$LOAM_API_URL/v1/sandboxes/svc-demo/services/after" -d '{"cmd":["sleep","infinity"],"needs":["ticker"],"user":"root"}'
{"name":"after","cmd":["sleep","infinity"],"user":"root","needs":["ticker"],"state":"running","pid":299,"restarts":0,"started_at":"2026-10-12T09:18:51.476035191Z"}
$ curl -sS -H "Authorization: Bearer $LOAM_TOKEN" -H 'Content-Type: application/json' -X PUT \
"$LOAM_API_URL/v1/sandboxes/svc-demo/services/bad" -d '{"cmd":["true"],"needs":["nope"]}'
{"error":{"code":"invalid","message":"needs unknown service \"nope\""}}
With the CLI, flags come before -- and the command after it:
$ loam services put svc-demo web --http-port 8080 --cwd /home/loam/site -- python3 -m http.server 8080
name: web
state: running
cmd: python3 -m http.server 8080
pid: 295
restarts: 0
started: 2026-10-12T09:18:51Z
cwd: /home/loam/site
http_port: 8080
$ loam services ls svc-demo
NAME STATE PID RESTARTS HTTP_PORT CMD
after running 299 0 sleep infinity
ticker running 296 0 sh -c while true; do date; sleep 5; done
web running 295 0 8080 python3 -m http.server 8080
loam services put takes --cwd, --env K=V, --user, --needs (repeatable or
comma-separated) and --http-port.
A service as the sandbox's site
A service with http_port N is started by a request to port N's URL when it isn't running, and
the sandbox's own URL goes to the sandbox's http_port. Make the two the same and the sandbox's
URL always answers, even after you stop the service or the sandbox:
$ curl -sS -H "Authorization: Bearer $LOAM_TOKEN" -X POST "$LOAM_API_URL/v1/sandboxes/svc-demo/services/web/stop"
{"name":"web","cmd":["python3","-m","http.server","8080"],"cwd":"/home/loam/site","http_port":8080,"state":"stopped","restarts":0}
$ curl -sS -H "Authorization: Bearer $LOAM_TOKEN" https://svc-demo--docs.loam.example.com/
<h1>hello from loam</h1>
$ loam services get svc-demo web | grep state
state: running
Access follows the port's setting either way (Ports and URLs).
Start, stop, restart, delete
$ curl -sS -H "Authorization: Bearer $LOAM_TOKEN" -X POST "$LOAM_API_URL/v1/sandboxes/svc-demo/services/ticker/restart"
$ loam services stop svc-demo ticker
$ loam services start svc-demo ticker
$ curl -sS -w '%{http_code}\n' -H "Authorization: Bearer $LOAM_TOKEN" -X DELETE "$LOAM_API_URL/v1/sandboxes/svc-demo/services/after"
204
$ loam services rm svc-demo ticker
Services come back after a stop, with new PIDs:
$ loam stop svc-demo && loam wake svc-demo && loam services ls svc-demo
svc-demo stopped
svc-demo running
NAME STATE PID RESTARTS HTTP_PORT CMD
after running 292 0 sleep infinity
ticker running 291 0 sh -c while true; do date; sleep 5; done
web running 293 0 8080 python3 -m http.server 8080
A service that keeps failing ends up crashed:
$ curl -sS ... -X PUT "$LOAM_API_URL/v1/sandboxes/svc-demo/services/crashy" -d '{"cmd":["sh","-c","echo boom; exit 1"]}'
$ loam services get svc-demo crashy
name: crashy
state: crashed
cmd: sh -c echo boom; exit 1
restarts: 3
Logs
GET .../services/{name}/logs answers the service's output as text, stdout and stderr together.
tail=N gives the last N lines; follow=true keeps the answer open and streams new output.
$ curl -sS -H "Authorization: Bearer $LOAM_TOKEN" "$LOAM_API_URL/v1/sandboxes/svc-demo/services/ticker/logs?tail=2"
Mon Oct 12 09:18:51 UTC 2026
$ loam services logs svc-demo ticker -f
Programs that buffer their output when it isn't a terminal (Python, for one) show nothing until the
buffer fills; run them unbuffered (python3 -u, PYTHONUNBUFFERED=1).
Go and TypeScript
svc, err := c.PutService(ctx, "svc-demo", "web", api.ServiceSpec{
Cmd: []string{"python3", "-m", "http.server", "8080"}, Cwd: "/home/loam/site", HTTPPort: 8080,
})
logs, err := c.ServiceLogs(ctx, "svc-demo", "web", client.LogOptions{Tail: 100}) // an io.ReadCloser
const box = loam.sandbox('svc-demo')
await box.putService('web', { cmd: ['python3', '-m', 'http.server', '8080'], cwd: '/home/loam/site', http_port: 8080 })
await box.restartService('web') // also startService, stopService, deleteService, listServices, getService
console.log(await new Response(await box.serviceLogs('web', { tail: 100 })).text())