Remote management

cardinal-wings — a REST API for your panel.

cardinal-wings is a REST API daemon that lets panels and frontends manage cardinal containers, images, blueprints, services and a multi-node cluster over HTTP — no SSH, no CLI.

Experimental. The API works and everything below — docs, terminal, webhooks, cluster support — is real, but wings is not stable yet: endpoints and payloads can change between versions. Not recommended for production panels yet.
What it is

One control surface over cardinal.

wings sits on top of cardinal's Docker-compatible HTTP API and gives it a panel-oriented REST surface plus a cluster view. It reuses what cardinal already does — it does not reinvent the container runtime or the orchestrator.

http

REST API for panels

Full container lifecycle: list with filters and pagination, create, inspect, start/stop/restart/kill, remove, stats, and logs with SSE streaming.

scope

Images · blueprints · services

Image list/pull/tag/push and search, the blueprint catalog with async install, plus service and function management through cardinal's orchestrator.

cluster

Multi-node by design

`?node=` routes any request to a specific server; background health checks feed `/v1/nodes`, and `/v1/system/info` aggregates the whole fleet in one call.

tasks

Async by default

Long operations — blueprint install, image pull — return a task id and run in the background, pollable at `/v1/tasks`.

auth

Bearer-token auth + roles

Multiple keys, each with a role (read-only or admin), rate limiting, TLS support and loopback-by-default — plus `/v1/self` so the UI knows what it can do.

ops

Metrics and uniform errors

Multi-node Prometheus metrics (admin-only) and a stable error envelope — `{"error":{"code":"…","message":"…"}}` — across every endpoint.

terminal

Interactive terminal

Open a shell in any container via websocket (`/terminal/ws`) or SSE — a real terminal for the panel, no extra dependencies.

live

Live stats and events

CPU/memory snapshots stream as SSE (`/stats?stream=1`), and container events push to the panel over `/v1/events`.

hooks

Webhooks

Task completion and container events POST to panel URLs with a shared secret — notifications without polling.

Reference

The whole wings API, on one page.

Base URL http://127.0.0.1:8080 (default). Every request except /v1/ping and /healthz requires Authorization: Bearer <key>. Every resource endpoint accepts ?node=<name> to target a specific cardinal node — see the cluster guide below for wiring a hub over many nodes.

Errors are always {"error":{"code":"…","message":"…"}} with one of bad_request, unauthorized, forbidden, not_found, conflict, method_not_allowed, not_implemented, upstream_error, internal.

System & discovery

MethodPathDescription
GET/v1/pingLiveness — returns pong, no auth
GET/healthzHealth — 200 ok or 503 degraded when local cardinal is down, no auth
GET/v1/versionWings version
GET/v1/selfCurrent key role (admin / readonly) for UI gating
GET/v1/nodesCluster nodes with live health from the background checker
GET/v1/system/infoDashboard aggregate — per-node containers/images/cpu/mem + totals
GET/v1/eventsContainer events streamed as SSE for live UI updates
GET/v1/metricsMulti-node Prometheus metrics (admin only)

Containers

MethodPathDescription
GET/v1/containersList — filters ?all=1, ?state=, ?image=, ?search=, ?sort=, ?limit=, ?offset=
POST/v1/containersCreate (admin) — {"image","name","ports","restart",…}
GET/v1/containers/{id}Inspect — full container state
POST/v1/containers/{id}/startStart (admin)
POST/v1/containers/{id}/stopStop (admin)
POST/v1/containers/{id}/restartRestart (admin)
POST/v1/containers/{id}/killForce-kill (admin)
DELETE/v1/containers/{id}Remove — ?force=1 (admin)
GET/v1/containers/{id}/statsCPU/mem snapshot — ?stream=1 streams as SSE
GET/v1/containers/{id}/logsLogs — ?tail=N, ?follow=1 streams as SSE
POST/v1/containers/{id}/execRun a command, returns an exec id (admin)
POST/v1/containers/{id}/exec/streamRun a command and stream output as SSE (admin)
POST/v1/containers/{id}/terminalOpen an interactive shell session (admin)
POST/v1/containers/{id}/terminal/inputWrite to the session's stdin (admin)
GET/v1/containers/{id}/terminal/streamSession output as SSE (ring buffer, last 200 lines)
GET/v1/containers/{id}/terminal/wsFull terminal over websocket — text frames in, output back (admin)
GET/v1/containers/{id}/fs/lsFile browser — list ?path=
GET/v1/containers/{id}/fs/catFile browser — read a file ?path=
GET/v1/containers/{id}/fs/treeFile browser — directory tree ?path=
POST/v1/containers/{id}/cpCopy files in/out — body {"src","dst"} (admin)

Images

MethodPathDescription
GET/v1/imagesList local images
GET/v1/images/{ref}Inspect an image
GET/v1/images/searchDocker Hub search — ?q=postgres
POST/v1/images/{ref}/pullPull — async, returns a task id (admin)
POST/v1/images/{ref}/tagTag — ?repo=…&tag=… (admin)
POST/v1/images/{ref}/pushPush to a registry (admin)
DELETE/v1/images/{ref}Remove an image (admin)

Blueprints

MethodPathDescription
GET/v1/blueprintsBlueprint catalog
GET/v1/blueprints/{name}Blueprint detail
POST/v1/blueprints/{name}/installInstall — async, poll the returned task id (admin)
POST/v1/blueprints/{name}/uninstallUninstall — async (admin)

Tasks (async jobs)

MethodPathDescription
GET/v1/tasksList tasks — including finished ones persisted across restarts
GET/v1/tasks/{id}Task status, output and live pull/install progress

Services & functions

MethodPathDescription
GET/v1/servicesList services
POST/v1/servicesCreate — {"name","image","replicas","ports"} (admin)
POST/v1/services/{name}/scaleScale — {"replicas":3} (admin)
DELETE/v1/services/{name}Remove a service (admin)
GET/v1/functionsList functions
POST/v1/functionsCreate — {"name","image"} (admin)
POST/v1/functions/{name}/invokeInvoke — {"data"} payload
DELETE/v1/functions/{name}Remove a function (admin)

Cluster

MethodPathDescription
GET/v1/cluster/healthFleet health across all configured nodes
GET/v1/cluster/replicasService replicas across the cluster
GET/v1/cluster/containersContainers across all nodes
Examples

Every call, with curl.

auth & system
# liveness — no token needed
curl localhost:8080/v1/ping                    # pong
curl localhost:8080/healthz                    # 200 ok / 503 degraded

# everything else requires a Bearer key
curl -H "Authorization: Bearer KEY" localhost:8080/v1/version
curl -H "Authorization: Bearer KEY" localhost:8080/v1/self
curl -H "Authorization: Bearer KEY" localhost:8080/v1/system/info
containers & live streams
# list running containers (filters: ?all=1, ?state=, ?image=, ?search=, ?sort=, ?limit=)
curl -H "Authorization: Bearer KEY" "localhost:8080/v1/containers?state=running&limit=50"

# create (admin)
curl -X POST -H "Authorization: Bearer KEY" -H "Content-Type: application/json" \
  -d '{"image":"nginx:latest","name":"web","ports":["8080:80"],"restart":"always"}' \
  localhost:8080/v1/containers

# lifecycle, live logs, live stats
curl -X POST -H "Authorization: Bearer KEY" localhost:8080/v1/containers/web/start
curl -N -H "Authorization: Bearer KEY" "localhost:8080/v1/containers/web/logs?follow=1"
curl -H "Authorization: Bearer KEY" "localhost:8080/v1/containers/web/stats?stream=1"
terminal & exec
# exec: run a command and stream output as SSE (admin)
curl -N -X POST -H "Authorization: Bearer KEY" -H "Content-Type: application/json" \
  -d '{"Cmd":["tail","-f","/var/log/app.log"]}' \
  localhost:8080/v1/containers/web/exec/stream

# interactive terminal: open a session, then bridge a websocket to it
curl -X POST -H "Authorization: Bearer KEY" localhost:8080/v1/containers/web/terminal
# ws://localhost:8080/v1/containers/web/terminal/ws  (text frames → stdin, output ← frames)
images, blueprints & tasks
# pull an image — async, returns a task id to poll
curl -X POST -H "Authorization: Bearer KEY" -H "Content-Type: application/json" \
  -d '{"image":"postgres:16"}' \
  localhost:8080/v1/images/postgres:16/pull
# → {"task_id":"task-3","action":"pull","image":"postgres:16"}

# poll the task — status + live pull progress
curl -H "Authorization: Bearer KEY" localhost:8080/v1/tasks/task-3

# install a blueprint — also async
curl -X POST -H "Authorization: Bearer KEY" -H "Content-Type: application/json" \
  -d '{"memory":"2g","cpus":"2","env":["EULA=TRUE"]}' \
  localhost:8080/v1/blueprints/minecraft-server/install
cluster & metrics
# target any node with ?node= — or aggregate the whole fleet
curl -H "Authorization: Bearer KEY" "localhost:8080/v1/containers?node=node-2"
curl -H "Authorization: Bearer KEY" localhost:8080/v1/nodes
curl -H "Authorization: Bearer KEY" localhost:8080/v1/cluster/health
curl -H "Authorization: Bearer KEY" localhost:8080/v1/system/info
curl -H "Authorization: Bearer KEY" localhost:8080/v1/metrics   # Prometheus (admin)
Releases

Built and published automatically.

Pushing a v* tag runs the build pipeline — lint, race-tested tests, linux/amd64 + arm64 binaries with the version embedded — and publishes a GitHub Release.

Install the latest release.

One command installs the daemon and its systemd unit. Point a panel at it with a Bearer token from the config.

install
curl -fsSL https://github.com/animesao/cardinal-wings/releases/latest/download/install.sh | bash
cardinal-wings on GitHub API reference
Docs

Built for panel developers.

The full reference above with curl examples, an OpenAPI schema for generating a client, and a step-by-step guide to wiring wings as a hub over many cardinal nodes. Wings is MIT licensed — the contributing guide and security policy are right next to the docs.

Roadmap

What is next.

The wings backend has everything a panel needs; the next step is the panel itself. One remaining upstream item: true PTY allocation in cardinal for full terminal emulation (`vim`, `htop`). The API is still experimental and details can change between versions.