Skip to content

API Overview

The dockmesh API is a plain REST + JSON interface served at /api/v1/. The same API powers the SvelteKit UI — everything you can do in the browser, you can do over HTTP.

https://dockmesh.example.com/api/v1/

Behind the scenes, Caddy (or your own reverse proxy) terminates TLS and proxies to the Go server. In dev, http://localhost:8080/api/v1/ works.

Three authentication options:

Exchange username+password (or an SSO callback) for a short-lived access token (15-minute JWT) and a refresh token. Refresh-token lifetime comes from the server’s session settings (defaults: 24h absolute, 60-min idle, 14-day remember-me — configurable under Authentication → Sessions & sign-in flow). Send the access token as Authorization: Bearer <token> on every request.

Terminal window
# Login
curl -X POST https://dockmesh.example.com/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"••••"}'
# Response
{
"access_token": "eyJhbGci...",
"refresh_token": "eyJhbGci...",
"user": { "id": "", "username": "admin", "role": "admin" }
}
# Use it
curl https://dockmesh.example.com/api/v1/stacks \
-H 'Authorization: Bearer eyJhbGci...'

Refresh with POST /api/v1/auth/refresh { "refresh_token": "..." } before the access token expires. The refresh path rotates the refresh token (token-family rotation): capture and store the new pair every time.

User Profile → API tokens → New token creates a token tied to your account. Choose an expiry at creation; revoke from the same UI when no longer needed. Send it like any JWT: Authorization: Bearer <token>. API tokens inherit your role + scope tags — issue tokens from a scoped service-account user so the token can’t reach more than the job needs.

Agents authenticate with their client certificate on the dedicated mTLS listener (:8443). Not used for general REST traffic — REST goes through the HTTP listener on :8080 with JWT or API-token auth as above.

All endpoints accept and return JSON. Set Content-Type: application/json on POST/PUT/PATCH.

dockmesh uses bare arrays / objects, not a wrapping envelope. A list endpoint returns the array directly:

[
{ "name": "monitoring", ... },
{ "name": "gitea", ... }
]

Multi-host list endpoints (containers, images, etc.) return a FanOutResponse wrapper when called with ?host=all:

{
"items": [ ... ],
"unreachable_hosts": [
{ "host_id": "prod-eu-2", "error": "agent offline" }
]
}

Use ?host=<id> for single-host (bare-array) responses. List endpoints don’t implement page/per_page/sort/filter today — fetches are unpaginated up to a backend-side cap of 200–1000 entries depending on the resource.

Errors use a flat shape and an appropriate HTTP status:

{ "error": "pull access denied for private/image" }
StatusMeaning
400Validation error (bad input)
401No / invalid auth
403Authenticated but forbidden by RBAC
404Resource not found
409Conflict (e.g. stack already exists)
422Unprocessable (e.g. compose.yaml failed to parse)
423Locked — used when an account is in cooldown after failed logins
500Server error — see req_id from journalctl -u dockmesh for correlation

Every response carries an X-Request-Id header (the chi middleware’s id). journalctl -u dockmesh | grep <id> lines up the server-side log entry with the client-side error.

There is no general per-route rate limiter today. The only rate limit in place is brute-force protection on /auth/login (configurable via the auth.lockout_max_attempts and auth.lockout_duration_minutes policy settings — defaults 5 attempts / 15-minute lockout per account). Other endpoints have no limiter and don’t return 429.

If you need a global rate limit (untrusted clients, public-internet exposure), put dockmesh behind a reverse proxy with rate-limiting (Caddy, Traefik, nginx) and tune there.

dockmesh streams several live feeds over WebSocket. Browser clients can’t set custom headers on a WebSocket upgrade, so dockmesh uses a ticket flow: POST /api/v1/ws/ticket with your Bearer token, receive a single-use ticket, append it to the WebSocket URL as ?ticket=<ticket>.

EndpointStreams
/api/v1/ws/logs/{container-id}Live container logs
/api/v1/ws/stats/{container-id}Live CPU / memory / network / I/O
/api/v1/ws/exec/{container-id}Interactive shell
/api/v1/ws/eventsDocker daemon events + dockmesh lifecycle events
Terminal window
# Get a ticket
TICKET=$(curl -s -X POST https://dockmesh.example.com/api/v1/ws/ticket \
-H "Authorization: Bearer $TOKEN" | jq -r .ticket)
# Dial the WebSocket
websocat "wss://dockmesh.example.com/api/v1/ws/events?ticket=$TICKET"

CLI / server-side clients can skip the ticket and set Authorization: Bearer … on the upgrade if their HTTP client supports it (dmctl uses the ticket path for parity with the browser).

The machine-readable OpenAPI 3.1 spec is served at three paths — all public (no Bearer token needed) so consumers can integrate without first getting credentials:

  • GET /api/v1/openapi.json — JSON, 5-minute cache, ready for openapi-typescript, oapi-codegen, Swagger Codegen, openapi-generator-cli, etc.
  • GET /api/v1/openapi.yaml — raw YAML — what spectral / redocly linters prefer
  • GET /api/v1/docs — rendered Swagger UI with “Try it out” wired up against the live server. Log in separately in Swagger UI’s Authorize button (paste any Bearer token you already have).

Point any generator at openapi.json:

Terminal window
# Generate a TypeScript client
npx openapi-typescript https://dockmesh.example.com/api/v1/openapi.json \
-o ./dockmesh-api.d.ts
# Or a Go client
oapi-codegen -generate client \
-o dockmesh_client.go -package dockmesh \
https://dockmesh.example.com/api/v1/openapi.json

The spec is hand-maintained at internal/api/openapi/openapi.yaml in the dockmesh repo. A drift test (TestOpenAPIDriftAgainstRoutes) fails CI if a route is registered without a matching spec entry (or vice versa), so the documentation and the running server never diverge — that’s the mechanical guarantee, not a process promise.