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. Two standalone auth aliases, GET /api/auth/methods and GET /api/auth/status, are also unaffected — unconditionally, not behind any flag — because both are registered directly on the app rather than through the removed alias router; see Authentication routes below. Everything else under /api/* now returns 410 Gone; 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 a valid session (cookie-based via Basic or OIDC login). For cross-domain OIDC providers, keep DD_SERVER_COOKIE_SAMESITE=lax (default) unless you have a specific same-site policy requirement.

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

Endpoint overview

CategoryEndpointsDescription
AppGET /api/v1/app, GET /api/v1/serverApplication info, server configuration, security runtime
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
RegistriesGET /api/v1/registriesRegistry configurations
StoreGET /api/v1/storeData store info
TriggersGET /api/v1/triggersTrigger configurations
WatchersGET /api/v1/watchersWatcher configurations

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/groupsGETContainer groups (by stack/compose project)
/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, POSTExperimental Portwing edge-agent key registry, available only with DD_EXPERIMENTAL_PORTWING=true
/api/v1/portwing/keys/:keyIdDELETERevoke an experimental Portwing Ed25519 public key and disconnect live sessions authenticated with it
/api/v1/portwing/wsWSExperimental 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/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. Seven 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": {}
    }
  ],
  "total": 7
}

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-failed, container-update, security-alert, agent-disconnect, 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

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:scan-started{ "containerId": "..." }A security scan started for a container
dd:scan-completed{ "containerId": "...", "status": "..." }A security scan finished
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:agent-connectedAgent payloadA remote agent connected to the controller
dd:agent-disconnectedAgent payloadA remote agent disconnected
dd:resync-required{ "reason": "..." }Replay could not cover the requested event range; clients should refresh full state

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": 11, "preferences": {"theme": {"family": "dracula"}, "appearance": {"fontSize": 1.1}}}'

{
  "apiVersion": 1,
  "username": "alice",
  "schemaVersion": 11,
  "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 containers grouped by stack or group label. Group priority: dd.group > com.docker.compose.project > com.docker.stack.namespace > ungrouped.

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

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.

Get available strategies

Returns the list of configured authentication strategies. This endpoint is unauthenticated so the login screen can render.

curl http://drydock:3000/auth/strategies

The { strategies, warnings } response shape from GET /auth/strategies is deprecated and scheduled for removal in v1.8.0. Migrate to GET /api/v1/auth/status; see Deprecations.

A legacy alias is available at GET /api/auth/methods (rate-limited), deprecated and scheduled for removal in v1.7.0 — 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}'

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.