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.
| Path | Status | Description |
|---|---|---|
/api/v1/* | Canonical | Versioned API path — use this for new integrations |
/api/* | Removed in v1.6.0 | Returns 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"
}| Field | Type | Description |
|---|---|---|
error | string | Human-readable error message (derived from the HTTP status when not explicitly set) |
details | object | Optional 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.
| Endpoint | Method | Required header |
|---|---|---|
/api/v1/containers/:id/rollback | POST | X-DD-Confirm-Action: container-rollback |
/api/v1/containers/:id | DELETE | X-DD-Confirm-Action: container-delete |
/api/v1/dependency-groups/:rootId/update | POST | X-DD-Confirm-Action: dependency-group-update |
/api/v1/api-keys/:keyId | DELETE | X-DD-Confirm-Action: api-key-revoke |
/api/v1/images/prune | POST | X-DD-Confirm-Action: image-prune |
Endpoint overview
| Category | Endpoints | Description |
|---|---|---|
| App | GET /api/v1/app, GET /api/v1/server | Application info, server configuration, security runtime |
| Config file | GET /api/v1/config, GET /:section, POST /validate, POST /reload, PUT /:section; GET / PATCH /editor/watchers, /editor/triggers, /editor/actions | v1.8.0-rc.1 file configuration and source-aware editors; reads require a session, writes require admin |
| Containers | GET /api/v1/containers, POST, PATCH, DELETE | Container state, actions, update policy, security |
| Agents | GET /api/v1/agents | Remote agent status and logs |
| Logs | GET /api/v1/log, GET /api/v1/log/entries, WS /api/v1/log/stream | Application logger config, buffered entries, and live stream |
| Authentications | GET /api/v1/authentications | Authentication provider configurations |
| API Keys | GET /api/v1/api-keys, POST, DELETE /:keyId | Scoped API key management (api-keys:manage) |
| Registries | GET /api/v1/registries | Registry configurations |
| Approvals | GET /api/v1/approvals, GET /summary, GET /:id, POST /:id/approve, POST /:id/reject, POST /:id/defer | Pending-approval queue: list, count, detail, and decide |
| Store | GET /api/v1/store | Data store info |
| Triggers | GET /api/v1/triggers | Trigger configurations |
| Watchers | GET /api/v1/watchers | Watcher configurations |
| Images | GET /api/v1/images, GET /api/v1/images/prune-preview, POST /api/v1/images/prune | Image inventory across hosts, prune preview, and prune (admin; requires X-DD-Confirm-Action: image-prune) |
Additional endpoints
| Endpoint | Method | Description |
|---|---|---|
/api/v1/openapi.json | GET | OpenAPI 3.1.0 specification (machine-readable API docs) |
/health | GET | Health check (returns 200 when healthy, 503 while startup readiness gates are not satisfied) |
/metrics | GET | Prometheus metrics (bearer token via DD_SERVER_METRICS_TOKEN, session auth fallback, or no auth via DD_SERVER_METRICS_AUTH=false) |
/api/v1/events/ui | GET | Server-Sent Events for real-time UI updates |
/api/v1/audit | GET | Audit trail (paginated, filterable by action/container/date) |
/api/v1/notifications | GET, PATCH /:id | Notification rules (per-event enable/disable and trigger assignments) |
/api/v1/notifications/outbox | GET, POST /:id/retry, DELETE /:id | Notification outbox (admin retry/discard for failed deliveries) |
/api/v1/settings | GET, PATCH | UI settings (internetless mode; PUT compatibility alias is deprecated) |
/api/v1/preferences | GET, PATCH | Synced UI preferences (opt-in cross-device sync; 403 for anonymous sessions) |
/api/v1/server | GET | Server configuration and compatibility info (see App API) |
/api/v1/server/security/runtime | GET | Security tools availability (see App API) |
/api/v1/containers/:id/label-overrides | GET, PATCH, DELETE | Durable overrides of display, dependency and routing labels (see Container API); writes need admin |
/api/v1/label-overrides | GET, DELETE /:overrideId | Every stored label override row, including ones with no matching container (see Container API) |
/api/v1/containers/groups | GET | Container groups (by stack/compose project) |
/api/v1/group-policies | GET, POST, GET/PUT/DELETE /:id | Group policies: update-policy defaults and restrict-only action rules per group (reads read, writes admin) |
/api/v1/containers/:id/stats | GET | Single container stats snapshot and history (see Container API) |
/api/v1/containers/:id/stats/stream | GET | SSE stream for real-time container stats (see Container API) |
/api/v1/stats/summary | GET | Fleet-aggregate CPU/memory snapshot (see Container API) |
/api/v1/stats/summary/stream | GET | SSE feed of fleet-aggregate stats — consumed by the dashboard Resource Usage widget (see Container API) |
/api/v1/update-operations/:id | GET | Single update/rollback operation by ID — SSE-miss fallback (see Container API) |
/api/v1/self-update/:operationId/status | GET | Self-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/cancel | POST | Cancel a queued or in-progress update operation (see Container API) |
/api/v1/containers/:id/release-notes | GET | Structured release notes for a container's current update target (see Container API) |
/api/v1/containers/:id/intermediate-release-notes | GET | Semver-only release notes between two tags, with from required and to defaulting to the pending update target (see Container API) |
/api/v1/portwing/keys | GET, POST | Stable Portwing edge-agent key registry, enabled by default (DD_EXPERIMENTAL_PORTWING=false is an emergency disable) |
/api/v1/portwing/keys/:keyId | DELETE | Revoke a Portwing Ed25519 public key and disconnect live sessions authenticated with it |
/api/v1/portwing/ws | WS | Stable Portwing edge-agent WebSocket endpoint using the portwing/1.0 protocol |
/api/v1/containers/update | POST | Bulk queue updates for multiple containers (see Container API) |
/api/v1/containers/backups | GET | All image backups (optionally filter by containerName) |
/api/v1/containers/:id/backups | GET | Image backups for one container, sorted by most recent first |
/api/v1/containers/:id/rollback | POST | Roll 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/:slug | GET | Proxy and cache registry icon images |
/api/v1/icons/cache | DELETE | Clear the icon proxy cache |
/api/v1/auth/status | GET | Authentication status check |
/api/v1/webhook/watch | POST | Webhook — trigger watch on all containers |
/api/v1/webhook/watch/:containerName | POST | Webhook — watch specific container |
/api/v1/webhook/update/:containerName | POST | Webhook — update specific container |
/api/v1/webhooks/registry | POST | Signed registry-provider webhook that triggers immediate checks for matching containers |
/api/v1/debug/dump | GET | Download a debug dump JSON file for troubleshooting |
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-signaturex-hub-signature-256x-quay-signaturex-harbor-signaturex-ms-signaturex-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
| Field | Type | Description |
|---|---|---|
data | array | List of notification rules |
total | integer | Number of rules returned |
data[].id | string | Rule identifier (e.g. update-available, security-alert) |
data[].name | string | Human-readable rule name |
data[].description | string | What this rule covers |
data[].enabled | boolean | Whether the rule is active |
data[].triggers | string[] | Trigger IDs that receive this notification (empty = all triggers) |
data[].bellEnabled | boolean | Whether this rule is included in the in-app bell when its event has audit-backed bell support |
data[].bellThreshold | string | Bell threshold: all, major, minor, or patch; threshold filtering applies only to update-available entries |
data[].templates | object | Per-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/retryReturns 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-idReturns 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
| Parameter | Type | Default | Description |
|---|---|---|---|
offset | integer | 0 | Number of entries to skip |
limit | integer | 50 | Results per page (max 200) |
action | string | Filter by a single action type | |
actions | string | Comma-separated list of action types (e.g. update-applied,update-failed) | |
container | string | Filter by container name | |
from | string | ISO 8601 date -- only entries on or after this timestamp | |
to | string | ISO 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/uiConnection 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
| Event | Payload | Description |
|---|---|---|
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-added | Container object | A new container was discovered by a watcher |
dd:container-updated | Container object | An existing container's state changed (new tag, status, scan result) |
dd:container-removed | Container object | A 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-connected | Agent payload | A remote agent connected to the controller |
dd:agent-disconnected | Agent payload | A 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"}'| Status | Response | Description |
|---|---|---|
| 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"
}| Field | Type | Default | Description |
|---|---|---|---|
internetlessMode | boolean | false | When enabled, disables registry icon lookups and other external requests |
updateMode | notify | manual | auto | manual on a fresh install | notify 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"
}| Field | Type | Description |
|---|---|---|
apiVersion | integer | API contract version; must match the server's supported version (currently 1) |
username | string | The signed-in username the preferences are keyed to |
schemaVersion | integer | null | The client preference schema version at time of write; null if never synced |
preferences | object | null | The full preferences blob, opaque to the server; null if never synced |
updatedAt | string | null | ISO 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
| Field | Type | Description |
|---|---|---|
data | array | List of container groups |
total | integer | Number of groups returned |
data[].name | string | null | Group name (null for ungrouped containers) |
data[].containers | array | List of containers with id, name, displayName, updateAvailable |
data[].containerCount | integer | Number of containers in the group |
data[].updatesAvailable | integer | Number 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.
| Endpoint | Method | Description |
|---|---|---|
/api/v1/group-policies | GET | All policies, each with members: { count, agents } |
/api/v1/group-policies | POST | Create a policy with { group, updatePolicy?, actions? }; 409 when the group already has one |
/api/v1/group-policies/:id | GET | One policy, or 404 |
/api/v1/group-policies/:id | PUT | Replace with { revision, updatePolicy?, actions? }; 409 on a stale revision, changed: false when nothing changes |
/api/v1/group-policies/:id?revision=N | DELETE | Delete 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/statusThe 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/userLogout
End the current session.
curl -X POST http://drydock:3000/auth/logoutWhen 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.
| Route | Purpose |
|---|---|
GET /api/v1/auth/totp-factor | Status: unenrolled or active, activatedAt, recoveryCodesRemaining, any pendingEnrollment. No secrets. |
POST /api/v1/auth/totp-enrollments | Start an enrollment (or a replacement). Returns the base32 secret and otpauthUri once. 409 if one is already pending. |
PUT /api/v1/auth/totp-enrollments/:id | Confirm 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/:id | Cancel. Always 204. |
POST /api/v1/auth/totp-recovery-code-sets | Replace the recovery codes with ten new ones, shown once. |
DELETE /api/v1/auth/totp-factor | Remove 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:
| Call | What's refused |
|---|---|
POST /api/v1/api-keys | Creating an API key. |
PUT /api/v1/config/:section and the three PATCH /api/v1/config/editor/* routes | Every 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/keys | Registering 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.jsonThe 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.