Skip to content
API

REST API

Access Drydock state and trigger actions using the HTTP REST API.

By default, the API is enabled and exposed on port 3000. You can override this behaviour using environment variables.

API Versioning

Starting in v1.4.0, the canonical API base path is /api/v1/*. The unversioned /api/* path was a backward-compatible alias during migration; it was removed in v1.6.0.

PathStatusDescription
/api/v1/*CanonicalVersioned API path — use this for new integrations
/api/*Removed in v1.6.0Returns 410 Gone with a JSON body pointing at the /api/v1/ equivalent

The four whitelisted wud-card compatibility endpoints keep responding at /api/* when DD_COMPAT_WUDCARD=true (three reshaped into a bare-array shape, the fourth unmodified), served by their own router rather than by falling through to the removed alias. GET /api/auth/status is also unaffected — unconditionally, not behind any flag — because it's registered directly on the app rather than through the removed alias router; see Authentication routes below. Everything else under /api/* now returns 410 Gone, including the former GET /api/auth/methods alias, removed in v1.7.0; use /api/v1/*. See Deprecations for details.

Error Contract

All error responses (HTTP 4xx/5xx) return a consistent JSON structure:

{
  "error": "Container not found"
}
FieldTypeDescription
errorstringHuman-readable error message (derived from the HTTP status when not explicitly set)
detailsobjectOptional contextual metadata (present only when additional context is available)

Authentication

When authentication is enabled, all API endpoints require either a valid session (cookie-based via Basic or OIDC login) or a scoped API key. For cross-domain OIDC providers, keep DD_SERVER_COOKIE_SAMESITE=lax (default) unless you have a specific same-site policy requirement.

A scoped API key is sent as Authorization: Bearer ddk_<keyId>_<secret>. It authenticates a single request, never creates a session, and therefore isn't subject to the same-origin CSRF checks that apply to cookie auth. Each route is reachable at exactly one scope; a key without it gets 403. A handful of routes are session-only and unreachable with a key at any scope. Manage keys through the API Keys API.

The webhook endpoints use a separate Bearer token authentication.

Destructive action confirmation

Some destructive endpoints require an explicit confirmation header:

X-DD-Confirm-Action: <token>

If the header is missing or does not match the expected token, the API responds with 428 Precondition Required.

EndpointMethodRequired header
/api/v1/containers/:id/rollbackPOSTX-DD-Confirm-Action: container-rollback
/api/v1/containers/:idDELETEX-DD-Confirm-Action: container-delete
/api/v1/dependency-groups/:rootId/updatePOSTX-DD-Confirm-Action: dependency-group-update
/api/v1/api-keys/:keyIdDELETEX-DD-Confirm-Action: api-key-revoke
/api/v1/images/prunePOSTX-DD-Confirm-Action: image-prune

Endpoint overview

CategoryEndpointsDescription
AppGET /api/v1/app, GET /api/v1/serverApplication info, server configuration, security runtime
Config fileGET /api/v1/config, GET /:section, POST /validate, POST /reload, PUT /:section; GET / PATCH /editor/watchers, /editor/triggers, /editor/actionsv1.8.0-rc.1 file configuration and source-aware editors; reads require a session, writes require admin
ContainersGET /api/v1/containers, POST, PATCH, DELETEContainer state, actions, update policy, security
AgentsGET /api/v1/agentsRemote agent status and logs
LogsGET /api/v1/log, GET /api/v1/log/entries, WS /api/v1/log/streamApplication logger config, buffered entries, and live stream
AuthenticationsGET /api/v1/authenticationsAuthentication provider configurations
API KeysGET /api/v1/api-keys, POST, DELETE /:keyIdScoped API key management (api-keys:manage)
RegistriesGET /api/v1/registriesRegistry configurations
ApprovalsGET /api/v1/approvals, GET /summary, GET /:id, POST /:id/approve, POST /:id/reject, POST /:id/deferPending-approval queue: list, count, detail, and decide
StoreGET /api/v1/storeData store info
TriggersGET /api/v1/triggersTrigger configurations
WatchersGET /api/v1/watchersWatcher configurations
ImagesGET /api/v1/images, GET /api/v1/images/prune-preview, POST /api/v1/images/pruneImage inventory across hosts, prune preview, and prune (admin; requires X-DD-Confirm-Action: image-prune)

Additional endpoints

EndpointMethodDescription
/api/v1/openapi.jsonGETOpenAPI 3.1.0 specification (machine-readable API docs)
/healthGETHealth check (returns 200 when healthy, 503 while startup readiness gates are not satisfied)
/metricsGETPrometheus metrics (bearer token via DD_SERVER_METRICS_TOKEN, session auth fallback, or no auth via DD_SERVER_METRICS_AUTH=false)
/api/v1/events/uiGETServer-Sent Events for real-time UI updates
/api/v1/auditGETAudit trail (paginated, filterable by action/container/date)
/api/v1/notificationsGET, PATCH /:idNotification rules (per-event enable/disable and trigger assignments)
/api/v1/notifications/outboxGET, POST /:id/retry, DELETE /:idNotification outbox (admin retry/discard for failed deliveries)
/api/v1/settingsGET, PATCHUI settings (internetless mode; PUT compatibility alias is deprecated)
/api/v1/preferencesGET, PATCHSynced UI preferences (opt-in cross-device sync; 403 for anonymous sessions)
/api/v1/serverGETServer configuration and compatibility info (see App API)
/api/v1/server/security/runtimeGETSecurity tools availability (see App API)
/api/v1/containers/:id/label-overridesGET, PATCH, DELETEDurable overrides of display, dependency and routing labels (see Container API); writes need admin
/api/v1/label-overridesGET, DELETE /:overrideIdEvery stored label override row, including ones with no matching container (see Container API)
/api/v1/containers/groupsGETContainer groups (by stack/compose project)
/api/v1/group-policiesGET, POST, GET/PUT/DELETE /:idGroup policies: update-policy defaults and restrict-only action rules per group (reads read, writes admin)
/api/v1/containers/:id/statsGETSingle container stats snapshot and history (see Container API)
/api/v1/containers/:id/stats/streamGETSSE stream for real-time container stats (see Container API)
/api/v1/stats/summaryGETFleet-aggregate CPU/memory snapshot (see Container API)
/api/v1/stats/summary/streamGETSSE feed of fleet-aggregate stats — consumed by the dashboard Resource Usage widget (see Container API)
/api/v1/update-operations/:idGETSingle update/rollback operation by ID — SSE-miss fallback (see Container API)
/api/v1/self-update/:operationId/statusGETSelf-update operation status — unauthenticated; polled by the UI overlay during a self-update (operation UUID acts as the access capability; returns { operationId, status, phase, completedAt? }, 404 when unknown)
/api/v1/operations/:id/cancelPOSTCancel a queued or in-progress update operation (see Container API)
/api/v1/containers/:id/release-notesGETStructured release notes for a container's current update target (see Container API)
/api/v1/containers/:id/intermediate-release-notesGETSemver-only release notes between two tags, with from required and to defaulting to the pending update target (see Container API)
/api/v1/portwing/keysGET, POSTStable Portwing edge-agent key registry, enabled by default (DD_EXPERIMENTAL_PORTWING=false is an emergency disable)
/api/v1/portwing/keys/:keyIdDELETERevoke a Portwing Ed25519 public key and disconnect live sessions authenticated with it
/api/v1/portwing/wsWSStable Portwing edge-agent WebSocket endpoint using the portwing/1.0 protocol
/api/v1/containers/updatePOSTBulk queue updates for multiple containers (see Container API)
/api/v1/containers/backupsGETAll image backups (optionally filter by containerName)
/api/v1/containers/:id/backupsGETImage backups for one container, sorted by most recent first
/api/v1/containers/:id/rollbackPOSTRoll back a container to a previous backup image (defaults to the most recent when backupId is omitted); requires X-DD-Confirm-Action: container-rollback
/api/v1/icons/:provider/:slugGETProxy and cache registry icon images
/api/v1/icons/cacheDELETEClear the icon proxy cache
/api/v1/auth/statusGETAuthentication status check
/api/v1/webhook/watchPOSTWebhook — trigger watch on all containers
/api/v1/webhook/watch/:containerNamePOSTWebhook — watch specific container
/api/v1/webhook/update/:containerNamePOSTWebhook — update specific container
/api/v1/webhooks/registryPOSTSigned registry-provider webhook that triggers immediate checks for matching containers
/api/v1/debug/dumpGETDownload a debug dump JSON file for troubleshooting
See each sub-page for detailed request/response examples.

Command endpoint convention

Some endpoints are intentional action commands and use POST under a resource path (for example container watch/scan/reveal). These are RPC-style operations rather than CRUD updates.

Registry webhook

POST /api/v1/webhooks/registry receives registry-provider webhook payloads and immediately checks matching containers.

The endpoint does not use the regular webhook bearer token. It requires an HMAC-SHA256 signature computed with DD_SERVER_WEBHOOK_SECRET. Drydock accepts sha256=<hex> or raw hex signatures from these headers:

  • x-drydock-signature
  • x-hub-signature-256
  • x-quay-signature
  • x-harbor-signature
  • x-ms-signature
  • x-registry-signature
payload='{"provider":"dockerhub","repository":"library/nginx"}'
signature="sha256=$(printf '%s' "$payload" | openssl dgst -sha256 -hmac "$DD_SERVER_WEBHOOK_SECRET" -binary | xxd -p -c 256)"

curl -X POST http://drydock:3000/api/v1/webhooks/registry \
  -H "Content-Type: application/json" \
  -H "x-drydock-signature: $signature" \
  -d "$payload"

Returns 202 Accepted with registry dispatch counts, 401 for a missing or invalid signature, 403 when registry webhooks are disabled, 429 when rate-limited, or 500 if the endpoint is enabled without a configured secret.

Notification rules

Notification rules control which events trigger notifications, which triggers receive them, the in-app bell filters, and optional per-provider message templates. Eight default rules are created automatically.

Get all notification rules

curl http://drydock:3000/api/v1/notifications

{
  "data": [
    {
      "id": "update-available",
      "name": "Update Available",
      "description": "When a container has a new version",
      "enabled": true,
      "triggers": [],
      "bellEnabled": true,
      "bellThreshold": "all",
      "templates": {}
    },
    {
      "id": "update-applied",
      "name": "Update Applied",
      "description": "After a container is successfully updated",
      "enabled": true,
      "triggers": [],
      "bellEnabled": true,
      "bellThreshold": "all",
      "templates": {}
    },
    {
      "id": "update-failed",
      "name": "Update Failed",
      "description": "When an update fails or is rolled back",
      "enabled": true,
      "triggers": [],
      "bellEnabled": true,
      "bellThreshold": "all",
      "templates": {}
    },
    {
      "id": "security-alert",
      "name": "Security Alert",
      "description": "Critical/High vulnerability detected",
      "enabled": true,
      "triggers": [],
      "bellEnabled": true,
      "bellThreshold": "all",
      "templates": {}
    },
    {
      "id": "agent-disconnect",
      "name": "Agent Disconnected",
      "description": "When a remote agent loses connection",
      "enabled": false,
      "triggers": [],
      "bellEnabled": true,
      "bellThreshold": "all",
      "templates": {}
    },
    {
      "id": "agent-reconnect",
      "name": "Agent Reconnected",
      "description": "When a remote agent reconnects after losing connection",
      "enabled": false,
      "triggers": [],
      "bellEnabled": false,
      "bellThreshold": "all",
      "templates": {}
    },
    {
      "id": "container-unhealthy",
      "name": "Container Unhealthy",
      "description": "When a container's Docker health check enters the unhealthy state",
      "enabled": false,
      "triggers": [],
      "bellEnabled": false,
      "bellThreshold": "all",
      "templates": {}
    },
    {
      "id": "maturity-cleared",
      "name": "Maturity Gate Cleared",
      "description": "When an update held back by the maturity policy becomes applicable",
      "enabled": true,
      "triggers": [],
      "bellEnabled": true,
      "bellThreshold": "all",
      "templates": {}
    }
  ],
  "total": 8
}

Response fields

FieldTypeDescription
dataarrayList of notification rules
totalintegerNumber of rules returned
data[].idstringRule identifier (e.g. update-available, security-alert)
data[].namestringHuman-readable rule name
data[].descriptionstringWhat this rule covers
data[].enabledbooleanWhether the rule is active
data[].triggersstring[]Trigger IDs that receive this notification (empty = all triggers)
data[].bellEnabledbooleanWhether this rule is included in the in-app bell when its event has audit-backed bell support
data[].bellThresholdstringBell threshold: all, major, minor, or patch; threshold filtering applies only to update-available entries
data[].templatesobjectPer-trigger overrides keyed by canonical trigger ID; each value may contain simpleTitle, simpleBody, and/or batchTitle

Update a notification rule

Update delivery routing, bell preferences, and/or template overrides for a rule. Only the fields you include are changed. Template keys must resolve to configured notification triggers; action triggers are rejected.

curl -X PATCH http://drydock:3000/api/v1/notifications/update-available \
  -H 'Content-Type: application/json' \
  -d '{
    "enabled": true,
    "triggers": ["slack.alerts", "ntfy.one"],
    "bellEnabled": true,
    "bellThreshold": "minor",
    "templates": {
      "slack.alerts": {
        "simpleTitle": "${container.name}: ${container.updateKind.remoteValue}",
        "simpleBody": "${container.result.releaseNotes.body}"
      }
    }
  }'

Returns 200 with the updated rule, 400 if the body is invalid or a trigger ID is not recognized, or 404 if the rule ID does not exist.

Preview notification templates

Render draft overrides against representative data without saving them:

curl -X POST http://drydock:3000/api/v1/notifications/update-available/preview \
  -H 'Content-Type: application/json' \
  -d '{
    "triggerId": "slack.alerts",
    "templates": {
      "simpleTitle": "${container.name}: ${container.updateKind.remoteValue}",
      "simpleBody": "${container.result.releaseNotes.body}",
      "batchTitle": "${containers.length} updates available"
    }
  }'

The response contains rendered simpleTitle, simpleBody, and batchTitle strings. Omitted draft fields fall back to the saved rule override, then to the trigger's configured/default template. The endpoint returns 404 for an unknown rule and 400 for an unknown trigger or one that cannot render previews.

Notification outbox

The notification outbox stores failed notification delivery attempts so admins can inspect, retry, or discard them. The outbox API and UI are session-authenticated admin surfaces.

Error body retention

lastError is diagnostic text from the latest failed delivery attempt. It may include downstream notification provider or webhook response bodies when those providers return useful failure text, with Authorization header values redacted before storage. Treat it as admin-visible troubleshooting data, not as sanitized end-user copy.

Outbox API responses expose only a safe payload projection. Full persisted delivery payloads, including full container objects, are not returned by GET /api/v1/notifications/outbox.

List notification outbox entries

curl "http://drydock:3000/api/v1/notifications/outbox?status=dead-letter"

Returns 200 with outbox entries, 400 if status is invalid, or 401 if authentication is required.

Retry a notification outbox entry

curl -X POST http://drydock:3000/api/v1/notifications/outbox/outbox-entry-id/retry

Returns 200 with the requeued entry, 404 if the entry does not exist or is not in dead-letter status, or 500 on an internal error.

Delete a notification outbox entry

curl -X DELETE http://drydock:3000/api/v1/notifications/outbox/outbox-entry-id

Returns 204 on success, 404 if the entry does not exist, or 500 on an internal error.

Audit trail

Returns paginated audit log entries for container lifecycle events, updates, rollbacks, webhooks, and more.

Get audit entries

curl "http://drydock:3000/api/v1/audit?offset=0&limit=25&action=update-applied&container=homeassistant"

{
  "data": [
    {
      "id": "a1b2c3d4-...",
      "timestamp": "2024-12-01T10:01:30.000Z",
      "action": "update-applied",
      "containerName": "homeassistant",
      "containerImage": "homeassistant/home-assistant",
      "fromVersion": "2024.11.3",
      "toVersion": "2024.12.0",
      "triggerName": "docker.local",
      "status": "success",
      "details": ""
    }
  ],
  "total": 42,
  "limit": 25,
  "offset": 0,
  "hasMore": true
}

Query parameters

ParameterTypeDefaultDescription
offsetinteger0Number of entries to skip
limitinteger50Results per page (max 200)
actionstringFilter by a single action type
actionsstringComma-separated list of action types (e.g. update-applied,update-failed)
containerstringFilter by container name
fromstringISO 8601 date -- only entries on or after this timestamp
tostringISO 8601 date -- only entries on or before this timestamp

Action types

update-available, update-applied, update-applied-dryrun, update-failed, notification-delivery-failed, container-update, security-alert, security-scan-skipped, scanner-asset-pull-started, scanner-asset-pull-succeeded, scanner-asset-pull-failed, scanner-asset-warm-started, scanner-asset-warm-succeeded, scanner-asset-warm-failed, agent-disconnect, container-unhealthy, maturity-cleared, container-added, container-removed, rollback, preview, container-start, container-stop, container-restart, webhook-watch, webhook-watch-container, webhook-update, hook-configured, hook-pre-success, hook-pre-failed, hook-post-success, hook-post-failed, auto-rollback, auth-login, env-reveal, debug-dump, auto-update-blocked, update-policy-override-set, update-policy-override-cleared, group-policy-set, group-policy-cleared, label-override-set, label-override-cleared, update-approved, update-rejected, update-deferred, mqtt-command-update, api-key-created, api-key-revoked, api-key-auth-failed

Server-Sent Events (SSE)

The /api/v1/events/ui endpoint provides real-time push notifications to the UI via Server-Sent Events.

Connect

curl -N -H "Accept: text/event-stream" http://drydock:3000/api/v1/events/ui

Connection limits: max 10 per IP, max 10 per session. Returns 429 when exceeded.

Reconnection and event replay

Each SSE event carries an id: field (<bootId>:<counter>). On reconnect, browsers automatically send a Last-Event-ID header and the server replays any buffered events since that ID. For clients that cannot set custom headers (for example EventSource in some environments), pass the same value via the ?last-event-id= query parameter instead — the server checks both and the header takes precedence.

The replay buffer is an in-memory 5-minute ring buffer. If the requested event is older than the buffer, the server sends dd:resync-required so the client can refresh full state. If the boot ID does not match the server's current boot (server restarted since the last connection), the server cannot replay events from before the restart; the client should treat that reconnect as a full re-sync.

Event types

EventPayloadDescription
dd:connected{ "clientId": "...", "clientToken": "..." }Sent immediately on connection. The clientToken is used for self-update acknowledgment.
dd:heartbeat{}Sent every 15 seconds to keep the connection alive
dd:container-addedContainer objectA new container was discovered by a watcher
dd:container-updatedContainer objectAn existing container's state changed (new tag, status, scan result)
dd:container-removedContainer objectA container was removed
dd:container-unhealthy{ "containerName": "...", "health": "unhealthy", "previousHealth": "...", "container": {...} }A watched container's health check transitioned to unhealthy
dd:scan-started{ "containerId": "...", "cycleId": "..." }A security scan started for a container; cycleId is present for bulk scans and optional otherwise
dd:scan-completed{ "containerId": "...", "status": "...", "cycleId": "..." }A security scan finished; cycleId is present for bulk scans and optional otherwise. Bulk requests with requestId also include that ID, cumulative completedCount, and scheduledCount. Filter by request ID, validate the accepted cycle/total, and use the highest count rather than adding events
dd:self-update{ "opId": "...", "requiresAck": bool, "ackTimeoutMs": int, "startedAt": "..." }Drydock is updating its own container. If requiresAck is true, the UI should POST an acknowledgment before the timeout.
dd:update-operation-changed{ "operationId": "...", "containerName": "...", "triggerName": "...", "status": "...", "phase": "...", "queuePosition": int, "queueTotal": int }An in-flight update operation's status or queue position changed. triggerName is optional.
dd:update-applied{ "containerName": "...", "containerId": "...", "newContainerId": "...", "operationId": "...", "batchId": "..." }An update was successfully applied to a container
dd:update-failed{ "containerName": "...", "error": "...", "operationId": "...", "batchId": "...", "phase": "...", "rollbackReason": "..." }An update attempt failed (or was rolled back)
dd:batch-update-completed{ "batchId": "...", "total": int, "succeeded": int, "failed": int, "durationMs": int, "items": [...] }A batch update run finished
dd:approval-created{ "id": "...", "containerId": "...", "containerName": "...", "decision": "pending", "pendingCount": int }A container update entered the pending-approval queue
dd:approval-decided{ "id": "...", "containerId": "...", "containerName": "...", "decision": "...", "pendingCount": int }An operator approved, rejected, or deferred a queued update; decision is approved, rejected, or deferred
dd:approval-resolved{ "id": "...", "containerId": "...", "containerName": "...", "decision": "...", "pendingCount": int }A queued row left the queue without an operator decision, for example its candidate was superseded or the container was removed
dd:agent-connectedAgent payloadA remote agent connected to the controller
dd:agent-disconnectedAgent payloadA remote agent disconnected
dd:agent-stats-changed{ "agentName": "..." }A remote agent's stats changed and should be refetched
dd:resync-required{ "reason": "..." }Replay could not cover the requested event range; clients should refresh full state
dd:preferences-updated{}A user's preferences changed on another device/tab; clients should refetch their own session-scoped preferences

Self-update acknowledgment

When a dd:self-update event has requiresAck: true, the UI must POST an acknowledgment before the timeout expires. The clientId and clientToken are provided in the initial dd:connected event.

curl -X POST http://drydock:3000/api/v1/events/ui/self-update/op-abc123/ack \
  -H 'Content-Type: application/json' \
  -d '{"clientId": "client-1", "clientToken": "tok-xyz"}'
StatusResponseDescription
202{ "status": "accepted", "operationId": "..." }Acknowledgment recorded
202{ "status": "ignored", "operationId": "..." }No pending ack for this operation
400{ "error": "..." }Missing required field (operationId, clientId, or clientToken)
403{ "status": "rejected", "operationId": "..." }Invalid token, mismatched client, or client not bound to operation

SSE message format

event: dd:container-updated
data: { "id": "31a61a...", "name": "homeassistant", ... }

Settings

Persistent server-wide application settings stored in the Drydock database. Unlike Preferences, these values apply to every user and browser.

Get settings

curl http://drydock:3000/api/v1/settings

{
  "internetlessMode": false,
  "updateMode": "manual"
}

Update settings

curl -X PATCH http://drydock:3000/api/v1/settings \
  -H 'Content-Type: application/json' \
  -d '{"updateMode": "notify"}'

{
  "internetlessMode": false,
  "updateMode": "notify"
}
FieldTypeDefaultDescription
internetlessModebooleanfalseWhen enabled, disables registry icon lookups and other external requests
updateModenotify | manual | automanual on a fresh installnotify detects and notifies but refuses Drydock-managed updates; manual allows UI/API updates but suppresses automatic action triggers; auto permits both. Existing pre-setting installs migrate to auto once to preserve their previous automatic-update behavior.

Returns 200 with the updated settings, or 400 if the body is invalid. PUT /api/v1/settings remains a deprecated compatibility alias; because /api/v1 is frozen, removal is deferred to API v2. New integrations should use PATCH.

Preferences

Cross-device sync of UI preferences (theme, layout, dashboard, language, and more) for signed-in users. Opt-in via the Sync across devices toggle in Config > Appearance — see UI Customization for the feature overview. Requires a real, non-anonymous user identity; there is no PUT alias for this endpoint. The examples below assume an authenticated, non-anonymous session (see Authentication); anonymous sessions receive 403.

Get preferences

curl http://drydock:3000/api/v1/preferences

{
  "apiVersion": 1,
  "username": "alice",
  "schemaVersion": null,
  "preferences": null,
  "updatedAt": null
}

schemaVersion, preferences, and updatedAt are null until the user has synced at least once.

Update preferences

Replaces the full stored preferences blob — this is a full replace, not a partial merge.

curl -X PATCH http://drydock:3000/api/v1/preferences \
  -H 'Content-Type: application/json' \
  -d '{"apiVersion": 1, "schemaVersion": 12, "preferences": {"theme": {"family": "dracula"}, "appearance": {"fontSize": 1.1}}}'

{
  "apiVersion": 1,
  "username": "alice",
  "schemaVersion": 12,
  "preferences": {"theme": {"family": "dracula"}, "appearance": {"fontSize": 1.1}},
  "updatedAt": "2026-07-11T12:00:00.000Z"
}
FieldTypeDescription
apiVersionintegerAPI contract version; must match the server's supported version (currently 1)
usernamestringThe signed-in username the preferences are keyed to
schemaVersioninteger | nullThe client preference schema version at time of write; null if never synced
preferencesobject | nullThe full preferences blob, opaque to the server; null if never synced
updatedAtstring | nullISO 8601 timestamp of the last server-side write; null if never synced

Returns 200 with the preferences envelope, 400 if the body is malformed, 401 if there is no session, 403 if the session is anonymous (sync requires a real user identity), 409 if apiVersion does not match the server's supported version, or 413 if the request body exceeds the global 256 kb JSON body limit.

Container groups

Returns server-derived container groups. Group priority: dd.group > com.docker.compose.project > com.docker.stack.namespace > ungrouped. Browser-side manual group overrides are UI preferences: they do not mutate Docker labels and are not reflected in this response.

curl http://drydock:3000/api/v1/containers/groups

{
  "data": [
    {
      "name": "monitoring",
      "containers": [
        {
          "id": "31a61a...",
          "name": "prometheus",
          "displayName": "Prometheus",
          "updateAvailable": false
        },
        {
          "id": "e5f6a7...",
          "name": "grafana",
          "displayName": "Grafana",
          "updateAvailable": true
        }
      ],
      "containerCount": 2,
      "updatesAvailable": 1
    },
    {
      "name": null,
      "containers": [
        {
          "id": "abc123...",
          "name": "standalone-app",
          "displayName": "standalone-app",
          "updateAvailable": false
        }
      ],
      "containerCount": 1,
      "updatesAvailable": 0
    }
  ],
  "total": 2
}

Response fields

FieldTypeDescription
dataarrayList of container groups
totalintegerNumber of groups returned
data[].namestring | nullGroup name (null for ungrouped containers)
data[].containersarrayList of containers with id, name, displayName, updateAvailable
data[].containerCountintegerNumber of containers in the group
data[].updatesAvailableintegerNumber of containers with a pending update

Group policies

Drydock-owned update policies keyed by exact group name. A policy sets maturityMode, maturityMinAgeDays, skipTags and skipDigests for every current and future member of the group, and can restrict what Drydock does automatically with actions.updateMode (manual or notify) and actions.exclude. Group rules only restrict, and updateMode: auto is rejected. Reads need the read scope and writes need admin. The behavior, precedence and field rules are on the group policies page.

EndpointMethodDescription
/api/v1/group-policiesGETAll policies, each with members: { count, agents }
/api/v1/group-policiesPOSTCreate a policy with { group, updatePolicy?, actions? }; 409 when the group already has one
/api/v1/group-policies/:idGETOne policy, or 404
/api/v1/group-policies/:idPUTReplace with { revision, updatePolicy?, actions? }; 409 on a stale revision, changed: false when nothing changes
/api/v1/group-policies/:id?revision=NDELETEDelete a policy; 409 on a stale revision

Writes return { policy, applied: { members }, warnings }, plus changed on replace and delete. Unknown fields are a 400, and the group can't change after creation.

Authentication routes

When authentication is enabled, these endpoints manage the login flow. Auth routes are mounted at /auth (not /api/auth).

Get authentication status

Returns registered authentication providers and startup registration errors. This endpoint is unauthenticated so the login screen can check auth readiness.

curl http://drydock:3000/api/v1/auth/status

The same status payload is also available at GET /auth/status; GET /api/auth/status remains as the unversioned compatibility alias.

The legacy { strategies, warnings } response shape at GET /auth/strategies was removed in v1.8.0. The route now answers 410 Gone regardless of whether the request is authenticated, with a JSON body of { "error": "...", "details": { "migration": "/api/v1/auth/status", "docs": "..." } }. Use GET /api/v1/auth/status instead; see Deprecations.

The legacy GET /api/auth/methods alias was removed in v1.7.0 and now returns 410 Gone; see Deprecations.

Login

Authenticate with credentials. The response sets a session cookie.

curl -X POST http://drydock:3000/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username": "admin", "password": "..."}'

Get current user

Returns the currently authenticated user. Requires a valid session.

curl http://drydock:3000/auth/user

Logout

End the current session.

curl -X POST http://drydock:3000/auth/logout

When the active auth strategy exposes an OIDC end-session URL, the response includes logoutUrl so the UI can complete IdP logout. If logoutUrl is missing, logout is local-session-only.

Remember me

Store a remember-me preference for the current authenticated session.

curl -X POST http://drydock:3000/auth/remember \
  -H 'Content-Type: application/json' \
  -d '{"remember": true}'

Two-factor management

These routes enroll and manage a TOTP second factor for a local Basic account. They are mounted at /api/v1/auth and work only from a browser session of that account: API keys at any scope, Authorization: Basic requests, OIDC sessions and anonymous access get a 403. Sessions that predate two-factor support also get a 403 until the person signs in again.

Every call that changes something needs a same-origin request with a JSON body, and needs HTTPS (as Express sees it, so set DD_SERVER_TRUSTPROXY behind a TLS-terminating proxy) or a client on loopback. The caller proves themselves again in the request itself: their password, plus exactly one current code or unused recoveryCode while a factor is active. Nothing carries over between calls. Wrong answers count against the same lockout budgets as a login and come back as 403, or 423 once locked. A re-authentication that passes clears those budgets, the way a completed login does; with a factor active that takes the password and the proof together. One re-authentication runs at a time per account, so a second one sent while the first is still being checked gets 429 with Retry-After: 1.

Over plain HTTP from anywhere but loopback, these calls are refused with 403 and details.reason set to https-required. An operator can lift that with DD_AUTH_TOTP_ALLOWHTTP=true. Don't, unless the network between the browser and Drydock is one you trust completely: the responses that start an enrollment and activate a factor carry the TOTP secret and the recovery codes, and over plain HTTP anyone on the path can read them and the password sent with each call.

RoutePurpose
GET /api/v1/auth/totp-factorStatus: unenrolled or active, activatedAt, recoveryCodesRemaining, any pendingEnrollment. No secrets.
POST /api/v1/auth/totp-enrollmentsStart an enrollment (or a replacement). Returns the base32 secret and otpauthUri once. 409 if one is already pending.
PUT /api/v1/auth/totp-enrollments/:idConfirm with { "code": "123456" }. Returns the ten recoveryCodes once. 410 expired, 422 wrong code. The fifth wrong code deletes the enrollment, and later calls get 404.
DELETE /api/v1/auth/totp-enrollments/:idCancel. Always 204.
POST /api/v1/auth/totp-recovery-code-setsReplace the recovery codes with ten new ones, shown once.
DELETE /api/v1/auth/totp-factorRemove the factor and its recovery codes. 204.
curl -X POST https://drydock.example.com/api/v1/auth/totp-enrollments \
  -H 'Content-Type: application/json' \
  -H 'Origin: https://drydock.example.com' \
  -b cookies.txt \
  -d '{"password": "..."}'

Enrollments expire after ten minutes. Confirming one ends every other session of the account and closes their open streams; the session that confirmed stays signed in. After that, POST /auth/login answers 202 with a challenge until a code completes it. Removing the factor also ends the other sessions, and password login works again.

Enrollment needs an encryption key ring configured on the server. Without one these routes answer 503 and change nothing. A person who has lost both their device and their recovery codes can't use these routes at all: an operator removes the factor with the offline command. API keys already minted from an account are independent of its sessions and are not revoked by any of this; review them after a recovery login.

What a recovery login can't do

Signing in with a recovery code gives a normal session with three limits. Each of these would leave something behind that still works after the factor is reset, so a sign-in that only proved a recovery code can't do them. They answer 403 with details.reason set to recovery-assurance:

CallWhat's refused
POST /api/v1/api-keysCreating an API key.
PUT /api/v1/config/:section and the three PATCH /api/v1/config/editor/* routesEvery write to the configuration file: any section, and a save from the watcher, notification or action editor. It isn't only auth that holds something long-lived. server has the webhook and metrics credentials, registry and agent have theirs, and a command action runs what it's given. One rule covers all of them and doesn't need updating as sections gain secrets. Reading the configuration still works.
POST /api/v1/portwing/keysRegistering an agent key.

Everything else still works from that session, including listing and revoking API keys and agent keys, replacing the recovery codes, and replacing or removing the factor with a second recovery code. Sign in with a code from the authenticator app to do any of the three.

OpenAPI

Machine-readable API documentation is available as an OpenAPI 3.1.0 specification.

curl http://drydock:3000/api/v1/openapi.json

The spec covers HTTP API endpoints with request/response schemas. You can import the URL into tools like Swagger UI, Redoc, or Postman for interactive exploration.