cardinal serve exposes a Docker-compatible REST API
(Docker API version 1.44) so Portainer, VS Code Dev Containers, CI
runners and custom scripts talk to cardinal without changes — with a
Bearer token, TLS and rate limiting built in.
# loopback only — safe without a token
cardinal serve
# external bind requires a token
cardinal serve -H 0.0.0.0 -p 2375 --token "$CARDINAL_TOKEN" -d
# or as a systemd service (auto-start on boot)
sudo cardinal serve on --port 2375
sudo cardinal serve status
sudo cardinal serve off curl -s http://localhost:2375/_ping # OK
curl -s http://localhost:2375/version # Docker API 1.44
curl -s http://localhost:2375/containers/json # running containers
curl -s http://localhost:2375/images/json # local images -H "Authorization: Bearer $CARDINAL_TOKEN" when you
started the server with a token. Container IDs work as full ID,
ID prefix, or container name.
| Method | Path | Description |
|---|---|---|
GET | /_ping | Health check — returns OK |
GET | /version | Server + API version (1.44, min 1.24) |
GET | /info | Host, storage, CPU, memory, container/image counts |
GET | / | API banner — version and data directory |
| Method | Path | Description |
|---|---|---|
GET | /containers/json | List containers (?all=1 includes stopped) |
POST | /containers/create | Create a container (JSON body; pulls the image) |
GET | /containers/{id}/json | Inspect — full container state |
POST | /containers/{id}/start | Start a created/stopped container |
POST | /containers/{id}/stop | Stop (?t=SECONDS grace period) |
POST | /containers/{id}/restart | Restart |
POST | /containers/{id}/kill | Force-kill the main process |
DELETE | /containers/{id} | Remove (?v=1 remove volumes, ?force=1) |
GET | /containers/{id}/logs | Logs (?tail=50, ?stdout=1, ?stderr=1) |
GET | /containers/{id}/top | Processes running inside |
GET | /containers/{id}/stats | CPU / memory / I/O usage |
POST | /containers/{id}/rename | Rename (?name=web2) |
POST | /containers/{id}/update | Change limits (memory, CPUs, restart policy) |
POST | /containers/{id}/exec | Run a command inside (JSON body with Cmd) |
POST | /containers/{id}/wait | Block until exit — returns status code |
GET | /containers/{id}/changes | Filesystem changes vs image |
Images
Method Path Description GET/images/jsonList local images GET/images/{name}/jsonInspect an image DELETE/images/{name}Remove an image POST/images/{name}/pushPush to a registry (auth via X-Registry-Auth) POST/images/{name}/tagTag (?repo=…, ?tag=…) GET/images/{name}/historyBuild history GET/images/{name}/getExport an image archive
System, cluster & metrics
Method Path Description POST/system/pruneRemove unused containers/images (?containers=false, ?images=false) POST/cluster/replicasCreate a service replica (JSON body) DELETE/cluster/replicas/{id}Remove a replica GET/cluster/containersList containers on this node GET/cluster/healthCluster health — {"status":"ok"} GET/metricsPrometheus metrics (Bearer token required by default)
Examples Create, run, inspect — with curl.
curl -s -X POST http://localhost:2375/containers/create \
-H "Content-Type: application/json" \
-d '{
"Image": "nginx:alpine",
"Hostname": "web",
"HostConfig": {
"PortBindings": { "80/tcp": [{ "HostPort": "8080" }] },
"RestartPolicy": { "Name": "always" },
"Memory": 268435456
}
}'
# → { "Id": "a1b2c3…", "Warnings": [] }
curl -s -X POST http://localhost:2375/containers/web/start
curl -s http://localhost:2375/containers/web/json | head -20
curl -s "http://localhost:2375/containers/json?all=1"
curl -s "http://localhost:2375/containers/web/logs?tail=50"
curl -s http://localhost:2375/containers/web/top
curl -s http://localhost:2375/containers/web/stats
curl -s -X POST http://localhost:2375/containers/web/exec \
-H "Content-Type: application/json" \
-d '{"Cmd": ["echo", "hello from cardinal"]}'
curl -s -X POST "http://localhost:2375/containers/web/rename?name=web2"
curl -s -X DELETE "http://localhost:2375/containers/web2?v=1&force=1"
curl -s http://localhost:2375/images/json
curl -s http://localhost:2375/images/nginx/json
curl -s -X POST "http://localhost:2375/images/nginx/tag?repo=myrepo/nginx&tag=v1"
curl -s -X DELETE http://localhost:2375/images/nginx:latest
curl -s -X POST http://localhost:2375/system/prune
curl -s http://localhost:2375/metrics | head -10
Tooling Point your tools at it.
portainer Portainer
Environments → Add → Docker Standalone → API. Use
http://host:2375 (or https:// with TLS)
and paste the Bearer token when prompted.
dev containers VS Code Dev Containers
Set the Docker context to the API:
docker context create cardinal --docker host=tcp://host:2375 —
Dev Containers and the Docker extension talk to cardinal directly.
scripts Anything that speaks Docker
The API tracks the Docker contract — list, inspect, create, start,
stop, exec, logs, stats, images — so CI runners and orchestration
scripts keep working unchanged.
Hardening How the API stays safe.
token Bearer token, constant-time
External binds are refused entirely without --token or
CARDINAL_TOKEN; comparisons are constant-time. TLS is
optional but does not replace the token — external access always
needs both.
limits Rate limit & body cap
A per-IP token bucket (25 req/s, burst 50) gates non-loopback
callers, and request bodies are capped at 8 MB. Loopback traffic is
exempt so cardinal exec and friends stay snappy.
cors No accidental web exposure
CORS is limited to loopback by default; add origins with
CARDINAL_CORS_ORIGINS deliberately. Server timeouts,
header caps and a hard cap on tracked clients are configured out of
the box.
metrics Metrics stay private
/metrics (Prometheus) requires the Bearer token by
default — set CARDINAL_METRICS_REQUIRES_AUTH=0 only on
loopback-bound instances.
Compatibility notes: GET /containers/{id}/export returns
501 (use cardinal export),
/containers/{id}/changes returns an empty list, and
container IDs accept prefixes and names — not just full IDs.
One binary, every endpoint.
The CLI reference covers the same operations from the terminal —
run, stop, exec, logs, images, volumes, networks and more.
Read the CLI reference cardinal-wings (remote agent) →