Run RadVel as a service
Added in version 1.6.
RadVel 1.6 ships an HTTP service that wraps the entire CLI workflow
(fit → mcmc/ns → derive → ic → tables → plots → report) behind a
JSON API plus an optional browser UI. It exists for the cases where the
Python REPL or CLI is awkward — web frontends, language-agnostic
clients, batch pipelines, multi-tenant deployments — and runs as a
single self-contained Docker image.
When to use the service
Stick with the CLI for one-off interactive analyses on your laptop. Reach for the service when:
You want to fit RVs from a frontend that isn’t Python (JS, Go, R…).
You need a long-running MCMC to survive a notebook restart.
Multiple users (or a CI pipeline) need to drive the same pipeline.
You want a containerised, reproducible deployment.
Two-line quickstart
$ docker run --rm -p 8000:8000 \
-v rv-data:/data \
ghcr.io/california-planet-search/radvel-api:latest
$ open http://localhost:8000/ui
The release workflow publishes :latest, :{major}.{minor}, and
:{version} tags; pin to a specific version (e.g. :1.6.0) for
reproducible deployments.
Visit /docs for live Swagger UI, /redoc for ReDoc.
Note
The image runs as a non-root operator (uid 10001). The example
above uses a named docker volume (rv-data) which the daemon
creates with the right ownership. If you prefer a host bind-mount
(-v "$PWD/.runs:/data") so the run outputs land on the host
filesystem, make the directory writable by uid 10001 first — either
chmod 0777 ./.runs or run with --user $(id -u):$(id -g).
Otherwise the lifespan crashes on the first write with
PermissionError.
Run from a source checkout
To build the image from your working tree — useful when you’re
contributing to radvel/api/ or testing an unreleased branch — use
the bundled docker-compose.yml:
$ git clone https://github.com/California-Planet-Search/radvel
$ cd radvel
$ docker compose up --build # rebuilds on every edit to the source tree
$ open http://localhost:8000/ui
The compose file mounts ./.runs into /data so runs survive
container restarts, pins RADVEL_API_WORKERS=1, and disables
.py setup-file upload (RADVEL_API_ALLOW_PY_UPLOAD=false). Edit
docker-compose.yml to flip those for local experimentation.
Rebuild after a code change with docker compose up --build -d;
docker compose logs -f radvel-api tails the uvicorn output.
JSON setup file
The service replaces the Python setup module with a JSON document. Every
attribute the legacy epic203771098.py setup exposes has a JSON
equivalent. A pared-down example:
{
"starname": "epic203771098",
"nplanets": 2,
"instnames": ["j"],
"fitting_basis": "per tc secosw sesinw k",
"bjd0": 2454833.0,
"planet_letters": {"1": "b", "2": "c"},
"params": {
"per1": {"value": 20.885258, "vary": false},
"tc1": {"value": 2072.79438, "vary": false},
"secosw1": {"value": 0.0, "vary": false},
"sesinw1": {"value": 0.1, "vary": false},
"k1": {"value": 10.0},
"per2": {"value": 42.363011, "vary": false},
"tc2": {"value": 2082.62516, "vary": false},
"secosw2": {"value": 0.0, "vary": false},
"sesinw2": {"value": 0.1, "vary": false},
"k2": {"value": 10.0},
"dvdt": {"value": 0.0},
"curv": {"value": 0.0},
"gamma_j": {"value": 1.0, "vary": false, "linear": true},
"jit_j": {"value": 2.6}
},
"data": {"kind": "dataset_ref", "dataset": "epic203771098.csv"},
"priors": [
{"type": "eccentricity", "num_planets": 2},
{"type": "positivek", "num_planets": 2},
{"type": "jeffreys", "param": "k1", "minval": 0.01, "maxval": 1000},
{"type": "jeffreys", "param": "k2", "minval": 0.01, "maxval": 1000},
{"type": "hardbounds", "param": "jit_j", "minval": 0.0, "maxval": 15.0},
{"type": "gaussian", "param": "dvdt", "mu": 0.0, "sigma": 1.0},
{"type": "gaussian", "param": "curv", "mu": 0.0, "sigma": 0.1}
],
"stellar": {"mstar": 1.12, "mstar_err": 0.05}
}
The four data shapes are:
{"kind": "inline", "rows": [{"time": ..., "mnvel": ..., "errvel": ..., "tel": ...}]}{"kind": "csv_base64", "csv_base64": "...", "separator": ","}{"kind": "server_path", "path": "/some/csv"}(disabled by default; requiresRADVEL_API_DATA_ALLOWLISTto be set to one or more absolute directories, and rejected with422if the resolved path is outside every configured root){"kind": "dataset_ref", "dataset": "epic203771098.csv"}(any fixture inradvel.DATADIR)
Pydantic v2 validates every payload and returns 422 with a
field-by-field error report on bad input. Hit /docs for the full
machine-readable schema.
End-to-end pipeline (curl)
# 1. Create the run
$ RID=$(curl -s -X POST http://localhost:8000/runs \
-H 'content-type: application/json' \
-d @epic.json | jq -r .run_id)
# 2. MAP fit (synchronous, ~1 s)
$ curl -s -X POST "http://localhost:8000/runs/$RID/fit" -d '{}' \
-H 'content-type: application/json' | jq
# 3. MCMC (asynchronous — returns 202 with a job_id)
$ JID=$(curl -s -X POST "http://localhost:8000/runs/$RID/mcmc" \
-H 'content-type: application/json' \
-d '{"nsteps":1000, "nwalkers":40, "ensembles":4}' | jq -r .job_id)
# 4. Poll the job until terminal
$ while :; do
state=$(curl -s "http://localhost:8000/jobs/$JID" | jq -r .state)
echo "$state"; [[ $state =~ succeeded|failed|cancelled ]] && break
sleep 2
done
# 5. Derive, tables, report
$ curl -s -X POST "http://localhost:8000/runs/$RID/derive" -d '{}' \
-H 'content-type: application/json' | jq
$ curl -s -X POST "http://localhost:8000/runs/$RID/tables" \
-H 'content-type: application/json' \
-d '{"types":["params","priors","rv"]}' | jq
$ curl -s -X POST "http://localhost:8000/runs/$RID/report" -d '{}' \
-H 'content-type: application/json' | jq
# 6. Download the PDF
$ curl -OJ "http://localhost:8000/runs/$RID/files/${RID}_results.pdf"
The same flow in Python with httpx ships in
tutorials/api_quickstart.
Async job semantics
mcmc and ns run as background jobs because they take minutes to
hours. State machine:
queued → running → succeeded | failed | cancelled
Job state lives in SQLite at ${RADVEL_API_DB_PATH} (default
/data/jobs.db) so the container can restart without losing
records. GET /jobs/{id} exposes live MCMC convergence telemetry
(Introduction to Autocorrelation Times covers the convergence quantities). DELETE
/jobs/{id} issues SIGTERM to the worker; the job reaches
cancelled within a few seconds.
If the host crashes mid-job, startup runs a reconciliation pass that
marks any stranded running rows as failed: process gone (likely
container restart) so polling clients see a clean terminal state.
Environment variables
Variable |
Default |
Meaning |
|---|---|---|
|
|
Per-run output dir. |
|
|
SQLite jobs table. |
|
|
uvicorn bind host. |
|
|
uvicorn port. |
|
|
ProcessPool size for MCMC/NS. |
|
|
Permit |
|
|
Mount |
|
(empty) |
Colon-separated absolute roots permitted for |
|
install-relative |
Where |
|
|
Matplotlib backend. |
Security notes
RadVel 1.6 ships no first-party authentication. The service trusts every reachable client. For shared deployments:
Put a reverse proxy in front (nginx or Caddy) and terminate auth there. Both basic auth and mTLS work fine.
Keep
RADVEL_API_ALLOW_PY_UPLOAD=false. The.pysetup-file upload endpoint and theuserdefinedprior bothexecuser Python and are only appropriate for trusted local users.Set
RADVEL_API_ENABLE_UI=falseon headless / multi-tenant nodes.Mount
/dataon a local volume; the SQLite jobs table is the only shared state.
Multi-arch image
Releases publish ghcr.io/california-planet-search/radvel-api for
both linux/amd64 and linux/arm64. Docker picks the right
architecture automatically; force it with --platform if you need
to.
Known limitations (1.6)
No built-in auth (mitigation: reverse proxy).
Single-host job runner — no clustering.
Pickle files are tied to the radvel version that wrote them; the service refuses to read pickles from a different version with a
409 version_mismatch.RADVEL_API_WORKERS=N ×
ensemblesshould not exceed the host CPU count.