Skip to content

Standalone Mode

Run Portwing as a hardened remote Docker API without a Drydock platform connection, using the generic REST adapter.

Portwing normally operates in one of two connection modes — edge (WebSocket to Drydock) or standard (Drydock-compatible HTTP server). Standalone mode is a third option: set ADAPTER=generic and Portwing becomes a self-contained, security-hardened remote Docker API with no external controller required.

The generic adapter exposes a clean REST and SSE surface under /api/v1/*, backed directly by the local Docker daemon. Everything else — auth enforcement, rate limiting, TLS, the Docker API passthrough, the MCP server, health endpoints — works identically to the other modes.


Enabling the generic adapter

Pass ADAPTER=generic when starting the container. You must also configure at least one auth mechanism unless you are on an isolated private network.

docker run -d \
  --name portwing \
  --group-add $(stat -c '%g' /var/run/docker.sock) \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -e ADAPTER=generic \
  -e TOKEN=my-secret \
  -p 3000:3000 \
  ghcr.io/codeswhat/portwing:latest

Never expose Portwing directly on a public interface without TLS. Run it behind a reverse proxy that terminates TLS, or supply TLS_CERT and TLS_KEY to enable the built-in TLS listener. See Configuration.

The ADAPTER environment variable defaults to drydock. The only change needed to switch modes is setting it to generic. All other configuration — token auth, Ed25519 keys, TLS, bind address, port — is unchanged.


REST endpoints

The generic adapter registers four authenticated routes. All require a valid token or Ed25519 signature; see Authentication for how to supply credentials.

EndpointMethodDescription
/api/v1/versionGETAgent version, protocol name/version, active adapter
/api/v1/containersGETCached container inventory
/api/v1/containers/{id}/logsGETDemuxed container logs (tail, time-filter, live follow)
/api/v1/eventsGETSSE stream of Docker container lifecycle events

GET /api/v1/version

Returns the running agent version, the protocol name and version, and the adapter name (generic).

curl -s -H "Authorization: Bearer $TOKEN" \
  http://localhost:3000/api/v1/version | jq .
{
  "agentVersion": "0.9.1",
  "protocolName": "portwing",
  "protocolVersion": "1.0",
  "adapter": "generic"
}

GET /api/v1/containers

Returns the cached container inventory built from the Docker daemon. The response is an array of container objects with id, name, image, state, and label metadata.

curl -s -H "Authorization: Bearer $TOKEN" \
  http://localhost:3000/api/v1/containers | jq .

GET /api/v1/containers/{id}/logs

Returns demuxed container logs. Docker's 8-byte stream-multiplexing frame header is stripped before the response is written. The id parameter accepts a container ID or name.

Query parameters:

ParameterTypeDescription
tailintegerNumber of lines from the end to return
sincestringShow logs since timestamp (RFC3339) or relative duration (e.g. 1h)
untilstringShow logs before timestamp or duration
follow1 or trueStream logs in real time with chunked transfer encoding
# Last 50 lines
curl -s -H "Authorization: Bearer $TOKEN" \
  "http://localhost:3000/api/v1/containers/my-container/logs?tail=50"

# Live stream
curl -sN -H "Authorization: Bearer $TOKEN" \
  "http://localhost:3000/api/v1/containers/my-container/logs?follow=1"

# Logs from the last hour
curl -s -H "Authorization: Bearer $TOKEN" \
  "http://localhost:3000/api/v1/containers/my-container/logs?since=1h"

GET /api/v1/events

Opens a Server-Sent Events stream of Docker container lifecycle events. Each data: frame carries a JSON envelope. The connection stays open until the client disconnects; a comment heartbeat line is written every 30 seconds to keep the connection alive through proxies.

curl -sN -H "Authorization: Bearer $TOKEN" \
  http://localhost:3000/api/v1/events

Each event has this shape:

{
  "ts": "2026-06-11T10:00:00Z",
  "type": "container",
  "action": "start",
  "containerId": "abc123def456",
  "name": "my-container",
  "image": "nginx:latest",
  "labels": { "app": "web" }
}

The labels field contains only user-defined container labels; synthetic Docker attributes (name, image, exitCode, etc.) are stripped before the event is written.

Event actions: create, start, stop, die, kill, restart, pause, unpause, destroy, rename, update, oom, health_status.

A comment heartbeat (": heartbeat") is written every 30 seconds. Standard SSE clients ignore comment lines automatically. They are present solely to prevent idle-connection timeouts in load balancers and proxies.


Health endpoints

These two endpoints are unauthenticated and are safe for load-balancer probes and Docker HEALTHCHECK instructions. They behave identically regardless of which adapter is active.

EndpointDescription
GET /healthReturns {"status":"ok"} — HTTP 200 always
GET /_portwing/healthReturns Docker connectivity status — HTTP 503 if daemon is unreachable
# Simple liveness probe
curl -sf http://localhost:3000/health

# Readiness probe (includes Docker connectivity)
curl -sf http://localhost:3000/_portwing/health
{
  "status": "healthy",
  "docker": "connected"
}

Agent info endpoint

GET /_portwing/info returns agent version, Docker version, connection mode, uptime, hostname, and the capability list. Auth required.

curl -s -H "Authorization: Bearer $TOKEN" \
  http://localhost:3000/_portwing/info | jq .

The base capabilities compose, exec, metrics, and events are always present regardless of adapter, followed by the generic adapter's own containers, logs, events, and version. This means the full array is ["compose", "exec", "metrics", "events", "containers", "logs", "events", "version"] — note events appears twice, since it is both a base capability and one the generic adapter reports.


Docker API passthrough

Any path not matched by a named route is transparently proxied to the local Docker daemon over the Unix socket. This covers every endpoint in the Docker Engine API — including streaming endpoints (/logs, /attach, /exec/*/start, /events, /build), binary responses, and interactive exec sessions using Upgrade: tcp.

The agent negotiates the API version on startup; v1.44 is the fallback. You can address the proxy with or without the version prefix — the agent prepends it automatically when omitted.

# List running containers via the Docker proxy
curl -s -H "Authorization: Bearer $TOKEN" \
  "http://localhost:3000/v1.44/containers/json?filters=%7B%22status%22%3A%5B%22running%22%5D%7D" | jq .

# Inspect a container
curl -s -H "Authorization: Bearer $TOKEN" \
  http://localhost:3000/v1.44/containers/my-container/json | jq .

The Docker proxy exposes the full Docker Engine API, including write operations (create, remove, exec). Always run with authentication configured, and restrict the BIND_ADDRESS or network rules to trusted clients only.


Authentication

All /api/v1/* endpoints and the Docker proxy require a valid credential. Supply it in any of the accepted headers:

HeaderUsage
Authorization: Bearer TOKENStandard bearer token (checked first)
X-Portwing-Token: TOKENAlternate header
X-Portwing-Signature + three companion headersEd25519 per-request signature

Ten failed attempts per IP per rolling minute result in HTTP 429 until the window expires.

For full details on token options (TOKEN, TOKEN_FILE, TOKEN_HASH) and Ed25519 key setup, see Authentication.


MCP server (read-only alternative)

Portwing also exposes a read-only Model Context Protocol server at POST /_portwing/mcp. It is available in all adapter modes, including generic. AI assistants (Claude, Cursor, Windsurf) can query live container state through this endpoint without issuing raw REST calls.

Available tools: list_containers, inspect_container, container_logs, host_metrics, container_stats.

See MCP Server for configuration and client setup instructions.


Further reading

  • Authentication — token options, Ed25519 key enrollment, rate limiting
  • Configuration — full environment variable reference including TLS_CERT, TLS_KEY, BIND_ADDRESS, PORT
  • API Reference — OpenAPI specification for all endpoints