polyserve deploy docs: Go, Python, and Bun quickstarts
Per-language deploy quickstarts for polyserve spaces plus the full host control protocol: deploy, usage, health, routing, scale-to-zero, env vars, addons, and the autoscaler contract.
polyserve is extreme-lightweight, in-process serverless hosting: one host process per language serves many apps ("spaces") with per-second usage metering, hot-swap deploys, and scale-to-zero. It runs the app.nz hosting platform and is cheap enough to run yourself on a $4 VPS. Every language host implements the same HTTP control surface, so the control plane treats hosts uniformly.
| Host | Default port | Space model |
|---|---|---|
| Go | 8440 | Compiled binary behind a unix socket, or a .so plugin fully in-process |
| Python | 8441 | ASGI app / handle() imported in-process by an asyncio host |
| Bun | 8442 | fetch handler imported in-process by Bun.serve |
Background reading: the announcement, the Go host deep dive, and in-process ONNX serving.
Running the hosts
git clone https://github.com/lee101/polyserve
cd polyserve
make build # builds all hosts
make e2e # deploys every example against real hosts and asserts
./hosts/go/polyserve-go # serve Go spaces on :8440Go quickstart
A Go space is a normal net/http binary with zero polyserve dependencies (see examples/go-hello). Build it for linux and POST it to the host:
GOOS=linux go build -o hello ./examples/go-hello
curl -X POST localhost:8440/_polyserve/deploy -F space=lee101/hello -F binary=@hello
curl localhost:8440/s/lee101/hello/The binary is saved to slots/<owner>/<name>/bin-<N>, started on a fresh unix socket, traffic is atomically switched, and the old process is drained for 5s then killed — zero dropped requests. Cold start budget: under 100ms.
Python quickstart
A Python space exposes an ASGI app or a handle() function; the asyncio host imports it in-process (see examples/python-hello and examples/python-onnx-tiny). Deploy the module as a source file or tar:
curl -X POST localhost:8441/_polyserve/deploy -F space=lee101/hello-py -F [email protected]
curl localhost:8441/s/lee101/hello-py/
# multi-file spaces: upload a tar
tar -cf onnx-tiny.tar -C examples/python-onnx-tiny .
curl -X POST localhost:8441/_polyserve/deploy -F space=lee101/onnx-tiny -F [email protected]New deploys load in a fresh module namespace; the old namespace is dropped after the switch. Cold start budget: import under 500ms.
Bun quickstart
A Bun space default-exports a fetch handler (see examples/bun-hello):
export default {
fetch(req: Request): Response {
const url = new URL(req.url);
return Response.json({ hello: "world", space: "bun-hello", path: url.pathname });
},
};curl -X POST localhost:8442/_polyserve/deploy -F space=lee101/hello-bun -F [email protected]
curl localhost:8442/s/lee101/hello-bun/Cold start budget: import under 100ms.
Control protocol
If POLYSERVE_ADMIN_TOKEN is set, control endpoints require Authorization: Bearer <token>.
POST /_polyserve/deploy
Multipart hot-swap deploy.
| Field | Description |
|---|---|
space | Required. owner/name. |
binary | Go: compiled executable. New process on a fresh unix socket, atomic traffic switch, old process drained (5s) then killed. |
module | Python/Bun: source file or tar. Loaded in a fresh module namespace; old namespace dropped after switch. |
env | Repeatable KEY=VALUE pairs injected into the space (addon credentials like DATABASE_URL). |
Response: {"ok":true,"space":"owner/name","generation":N}.
GET /_polyserve/usage
Per-second billing source of truth. Counters are monotonic; the control plane diffs between scrapes. Usage is also appended once per minute to usage.jsonl for crash-safe billing.
{"spaces":{"owner/name":{"requests":123,"busy_ms":4567,"warm_s":890,"loaded":true}}}busy_ms— cumulative wall time with >=1 in-flight request (billable compute)warm_s— cumulative seconds loaded in memory (billable residency)
GET /_polyserve/health
Returns {"ok":true,"spaces_loaded":N,"uptime_s":N}.
DELETE /_polyserve/spaces/:owner/:name
Unload and remove the space.
Routing
GET/POST/... /s/<owner>/<name>/<rest>routes to spaceowner/namewith path/<rest>.- Or by Host header when a custom domain is mapped (the host consults its
spaces.jsonstate file). - Unknown space: 404
{"error":"unknown space"}. - Space present but scaled to zero: the host loads it, then serves.
Scale-to-zero
A space with no requests for POLYSERVE_IDLE_TTL (default 900s) is unloaded — Go: process stopped; Python/Bun: module released. State stays in spaces.json so the next request cold-starts it transparently.
Environment variables
| Variable | Description |
|---|---|
POLYSERVE_PORT | Host listen port (defaults: Go 8440, Python 8441, Bun 8442) |
POLYSERVE_DATA_DIR | State directory (default ./polyserve-data) |
POLYSERVE_IDLE_TTL | Scale-to-zero idle timeout (default 900s) |
POLYSERVE_ADMIN_TOKEN | If set, control endpoints require Authorization: Bearer <token> |
Addons
Spaces get env injected per-space: the app.nz control plane provisions addons (Postgres, MongoDB, ...) and passes DATABASE_URL etc. at deploy time. Locally:
curl -X POST localhost:8440/_polyserve/deploy \
-F space=lee101/pgapp \
-F binary=@pgapp \
-F env=DATABASE_URL=postgres://user:pass@host:5432/dbThe examples/go-postgres example shows the app.nz Postgres addon via injected DATABASE_URL.
Autoscaler
The autoscaler/ control loop scrapes /_polyserve/usage from every worker, computes aggregate busy-ratio, and scales a provider (Hetzner implementation, RunPod GPU implementation) between MIN_WORKERS and MAX_WORKERS. Workers self-register by running hosts under systemd; the scaler only needs each worker's address list.
polyserve is MIT licensed: github.com/lee101/polyserve, mirrored at app.nz/lee101/polyserve.