š API Documentation¶
Base URL: /api (REST endpoints use /api/v1 prefix unless otherwise noted)
Note: All API responses are JSON formatted. All protected endpoints require a valid session: the
dim_sessioncookie set by the login, or the same JWT in anAuthorization: Bearer <token>header.Validation: Every endpoint that takes a body or query parameters checks them against a Zod schema from
@dim/sharedbefore doing anything else. A request that does not match is answered with400and a single message that starts with the path of the first offending field, e.g.{ "error": "entries.0.containerName: Too small: expected string to have >=1 characters" }. Only the first problem is reported; fix it and the next request names the next one.
š Table of Contents¶
- Authentication
- Users
- Clients
- Docker
- Settings & Maintenance
- Projects
- Activity
- Webhooks
- Misc
- WebSockets
š Authentication¶
Revocation. A valid signature is not enough. The JWT carries the user's
token_version as tv, and every protected request and every dashboard handshake
compares it with the user's row. Changing a user's password or auth methods raises that
counter, and deleting the user removes the row, so all of that user's sessions end at
once: the next request answers 401, an open dashboard socket is closed with 4001.
A user who changes their own password gets a fresh cookie in the same response; their
dashboard socket closes with 4002 and reconnects on it. Tokens issued before this
existed carry no tv and are refused ā one login after the update.
Login¶
POST /api/login
Description: Authenticates a user with local credentials and starts a session. The session token is set as a cookie and is not part of the response body.
Request Body¶
| Field | Type | Required | Description |
|---|---|---|---|
username |
string | Yes | The username of the user. |
password |
string | Yes | The password of the user. |
Example Request:
Response¶
The response sets two cookies, both with Path=/, SameSite=Strict and a Max-Age that ends when the token does (jwtExpiresIn, default 12h). Secure is added when the request came in over HTTPS (behind a reverse proxy listed in security.trusted_proxies: X-Forwarded-Proto: https).
| Cookie | HttpOnly |
Content |
|---|---|---|
dim_session |
yes | The JWT. Sent by the browser on every request and on the WebSocket handshake. |
dim_auth |
no | 1. Carries no secret; tells the dashboard that a session exists. |
Every protected endpoint accepts the session either as the dim_session cookie or as Authorization: Bearer <token>. A script can log in with this endpoint and send the value of dim_session as a bearer token.
- 400 ā
usernameorpasswordmissing or empty. A malformed request is not a failed login. - 401 ā
Invalid credentials: unknown user, wrong password, or an account without a local password (OIDC only). All three answer the same.
Rate Limit¶
At most 10 attempts per 15 minutes per client IP, successful or not. Further attempts
are answered with 429 Too Many Requests until the window has passed; the response carries
x-ratelimit-* and retry-after headers. No other endpoint is rate limited.
Logout¶
POST /api/auth/logout
Description: Ends the browser session by clearing both cookies. Unauthenticated, so that an expired session can be logged out of too. The token itself stays valid until it expires; the server keeps no session list to revoke it from.
Response¶
Current Session¶
GET /api/v1/me
Description: Who the current session belongs to and when it expires. The dashboard reads the username, the user id and the expiry (for its automatic logout) from here, because it cannot read the httpOnly cookie. Protected like every /api/v1 endpoint: without a valid session it answers 401.
Response¶
| Field | Type | Description |
|---|---|---|
id |
number | The user's id. |
username |
string | The user's name. |
expiresAt |
string | null | ISO 8601 time at which the session expires. |
OIDC Configuration¶
GET /api/auth/config
Description: Returns the public authentication configuration. Used by the frontend to determine whether to show local login, OIDC login, or both.
Response¶
| Field | Type | Description |
|---|---|---|
type |
string | "oidc" while an OIDC provider is configured and was discovered at startup, otherwise "local". Local login stays available either way. |
OIDC Login¶
GET /api/auth/login
Description: Redirects the user's browser to the OIDC provider's login page. Generates a PKCE code verifier/challenge and stores state for CSRF protection.
Response¶
- 302 Redirect: Redirects to the OIDC provider.
- 404 ā OIDC is not configured.
OIDC Callback¶
GET /api/auth/callback
Description: Handles the callback from the OIDC provider. Exchanges the authorization code for a local JWT session token.
Query Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
code |
string | Yes | The authorization code returned by the OIDC provider. |
state |
string | Yes | The state parameter for CSRF protection. |
Response¶
- 302 Redirect: Sets the session cookies (see Login) and redirects to
/. The token is not put into the redirect URL. - 500 ā
Authentication failed: <reason>: OIDC is disabled, the state does not match, or the code exchange failed.
š¤ Users¶
List Users¶
GET /api/v1/users
Description: Retrieves a list of all registered users.
Response (Array of User objects)¶
| Field | Type | Description |
|---|---|---|
id |
number | The unique identifier of the user. |
username |
string | The username. |
auth_methods |
string | Comma-separated list of allowed authentication methods. |
created_at |
string | ISO 8601 timestamp of creation. |
updated_at |
string | ISO 8601 timestamp of last update. |
Example Response:
[
{
"id": 1,
"username": "admin",
"auth_methods": "local",
"created_at": "2024-01-01T10:00:00.000Z",
"updated_at": "2024-01-01T10:00:00.000Z"
}
]
Create User¶
POST /api/v1/users
Description: Creates a new user.
Request Body¶
| Field | Type | Required | Description |
|---|---|---|---|
username |
string | Yes | The desired username. |
password |
string | Conditional | Required when auth_methods includes "local". |
auth_methods |
string | No | Auth methods: "local", "oidc", or "local,oidc". Defaults to "local". Any other value is rejected with 400. |
Example Request:
Response¶
- 400 ā invalid body, or
auth_methodsincludeslocalwithout apassword. - 409 ā username already exists.
Update User¶
PUT /api/v1/users/:userId
Description: Updates an existing user's password or authentication methods.
Path Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
userId |
string | Yes | The ID of the user to update. |
Request Body¶
| Field | Type | Required | Description |
|---|---|---|---|
password |
string | No | The new password. Only valid if user has "local" auth method. |
auth_methods |
string | No | New comma-separated list of local and oidc. An empty string leaves the methods unchanged. |
Response¶
- 400 ā invalid body, a
passwordfor a user withoutlocal, orlocalfor a user who has no password and gets none. - 404 ā user not found.
Delete User¶
DELETE /api/v1/users/:userId
Description: Deletes a user. Cannot delete yourself or the last remaining user.
Path Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
userId |
string | Yes | The ID of the user to delete. |
Response¶
- 400 ā
Cannot delete yourselforCannot delete the last user. - 404 ā user not found.
š„ļø Clients¶
List Clients¶
GET /api/v1/clients
Description: Retrieves a list of all registered clients enriched with their live connection status from ProxyService.
Response (Array of Client objects)¶
| Field | Type | Description |
|---|---|---|
id |
string | Client UUID. |
hostname |
string | Hostname of the client machine. Empty for an inbound client whose agent has not registered yet. |
displayName |
string | null | Optional human-readable name. |
status |
string | "online" or "offline". |
lastSeen |
string | null | ISO 8601 timestamp of last connection. null for an inbound client whose agent has not registered yet ā the dashboard shows it as Awaiting registration. |
version |
string | null | Agent version reported on last connection. |
connectionMode |
string | "inbound" (agent dials in) or "outbound" (server dials the agent). |
inboundAllowedIp |
string | null | Inbound clients: the address or IPv4 network connections must come from; null when the check is switched off. |
inboundLastIp |
string | null | Inbound clients: the address the agent last authenticated from. Read-only and written only after the check above has passed, so it is always an address that was let in. The client editor measures a new inboundAllowedIp against it and warns before a value is saved that would refuse the agent. null until the agent has connected once. |
registrationExpiresAt |
string | null | Inbound clients: until when the registration token issued for the client is valid, or null when none is open. Set while the client waits for its agent and after a new token was issued; cleared by the registration that uses it. A token that ran out keeps its date. |
outboundTargetAddress |
string | null | Outbound clients: host:port the server dials. |
autoUpdateCron |
string | null | This host's auto-update schedule for containers outside any project. null inherits the default from the settings, "" means the host takes part through its projects only. |
timezone |
string | null | IANA time zone the agent reported on its last connect, the one it reads its auto-update cron expressions in. null until the agent has connected, or for an agent that predates the field. |
autoUpdateLastRun |
string | null | When the host last finished an auto-update run, on the agent's clock ā any of its schedules, whether the run changed anything or not. Reported by the agent (AGENT_STATUS). Kept while the host is offline and never moved back; null for a host not seen running one. |
autoUpdateNextRun |
string | null | The earliest run the host's schedules have planned, as its agent last reported it. null for a host without a schedule. |
autoUpdateSchedules |
object[] | null | Every schedule the agent has planned, each as { "schedule": string, "cron": string, "nextRun": string \| null }. schedule is "host" for the containers outside any project and "project:<id>" for a project; cron is the expression the server resolved for it. A schedule the agent did not plan ā a project switched off, an expression it cannot read ā is not listed. null until an agent that reports them has done so. |
autoUpdateRunningSince |
string | null | Since when the connected agent has had an auto-update run under way. null while none runs, and while the client is offline. |
docker |
object | null | Whether the connected agent reaches its Docker daemon: { "reachable": bool, "error": string \| null, "since": string }, where since is when the answer last changed. null while the client is offline, or before the agent has tried. While reachable is false, the Docker state the server holds for the host is the last one read. |
capabilities |
string[] | null | What the agent currently connected says it can do, as it named it in its AUTH payload (see below). null while the client is offline ā capabilities belong to the build on the wire, not to the stored client; [] is a connected agent that names none. An agent that lacks a capability is still fully manageable, which is what makes updating it possible. |
createdAt |
string | When the client was created ā for an inbound client, when its first token was issued. |
updatedAt |
string | null | When the stored record last changed. |
Example Response:
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"hostname": "backup-client-01",
"displayName": "Backup Client",
"status": "online",
"lastSeen": "2024-01-01T12:30:00.000Z",
"version": "1.0.0",
"connectionMode": "inbound",
"inboundAllowedIp": "192.168.1.50",
"inboundLastIp": "192.168.1.50",
"registrationExpiresAt": null,
"outboundTargetAddress": null,
"autoUpdateCron": null,
"timezone": "Europe/Berlin",
"autoUpdateLastRun": "2024-01-01T03:01:12.000Z",
"autoUpdateNextRun": "2024-01-02T03:00:00.000Z",
"autoUpdateRunningSince": null,
"docker": { "reachable": true, "error": null, "since": "2024-01-01T12:29:58.000Z" },
"capabilities": ["auto-update", "project-query"],
"createdAt": "2024-01-01 10:00:00",
"updatedAt": "2024-01-01 12:30:00"
}
]
Create Inbound Client¶
POST /api/v1/clients/inbound
Description: Adds a client whose agent dials the server (inbound mode). The client exists from this call on ā in the list, under the name given here ā and waits for its agent. The response carries the registration token the agent registers with (Register Client); it is the only place the token appears in the clear, the server stores just its SHA-256 hash on the client. The token is valid for 30 minutes and for one registration; a new one is issued with Issue A Registration Token.
Request Body¶
| Field | Type | Required | Description |
|---|---|---|---|
displayName |
string | Yes | Name the client is listed under, at most 100 characters. Until the agent reports its hostname it is all the client is known by. |
inboundAllowedIp |
string | No | IPv4 address or CIDR network the client is restricted to. Without it the address the agent registers from is used. |
Response¶
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"token": "a1b2c3d4e5...",
"expiresAt": "2024-01-01T10:30:00.000Z"
}
- 400 ā
displayNameempty or longer than 100 characters, orinboundAllowedIpnot an IPv4 address or network.
Create Outbound Client¶
POST /api/v1/clients/outbound
Description: Adds a client that the server connects to (outbound mode), instead of the agent dialling in. The server opens <scheme>://<outboundTargetAddress>/ws/register ā wss:// when the stored address carries that prefix, ws:// otherwise ā hands over the agent's setup PIN (or its DIM_REGISTRATION_SECRET) together with a newly generated auth token and the client's server-issued clientId (stored by the agent in identity.json in its data directory), and then opens the regular agent session on /ws/agent, presenting both halves of that identity in the query string ā the agent refuses a caller that does not name the id it was registered under. The client is written to the database only after that session has authenticated.
Request Body¶
| Field | Type | Required | Description |
|---|---|---|---|
outboundTargetAddress |
string | Yes | host, host:port or wss://host:port of the agent's web server. Without a port, :3001 is appended. wss:// dials the agent over TLS, which requires the agent to serve it (see client.md); a bare address, or one written ws://, is stored and dialled as plaintext. Any other scheme, and a path, query or credentials, are refused ā the value is interpolated into a WebSocket URL. |
registrationSecret |
string | Yes | The setup PIN from the agent's log, or the value of DIM_REGISTRATION_SECRET if the agent has one. The agent tells the two apart. |
hostname |
string | No | Name shown for the client. Defaults to outboundTargetAddress. |
Response¶
An empty outboundTargetAddress or registrationSecret is answered with 400 before any connection is attempted. On a failed handshake the endpoint answers 503 with { "error": "Could not establish connection to client. <reason>" }. The reason is derived from how the agent ended the handshake, and the request returns as soon as the agent closes the connection:
| Agent response | Reason given |
|---|---|
Close 4003 Already registered |
The agent already holds an identity; remove its identity.json, restart it and use the new setup PIN. |
Close 4003 No registration secret configured |
Only from agents older than the setup PIN: they need registrationSecret in their config.yaml. |
REGISTRATION_FAILURE / close 4003 Invalid secret |
Neither the setup PIN nor the secret matches. After 5 wrong attempts the agent logs a new PIN. |
Close 4001 Registration timed out |
The agent gave up waiting for the registration request. |
| Connection error / no answer within 10 s | The underlying error, or a timeout message. |
| Registration succeeded, AUTH failed | The agent has already stored its token; it must be reset before retrying. |
Update Client¶
PUT /api/v1/clients/:clientId
Description: Updates a client's display name, its auto-update schedule, for inbound clients the address its connections must come from, and for outbound clients the address the server dials. At least one field is required.
Path Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
clientId |
string | Yes | The UUID of the client. |
Request Body¶
| Field | Type | Required | Description |
|---|---|---|---|
displayName |
string | No | The new display name for the client. |
inboundAllowedIp |
string | null | No | Inbound clients only. An IPv4 address or CIDR network restricts connections to it; null switches the check off; leaving the field out keeps the stored value. |
outboundTargetAddress |
string | No | Outbound clients only. host or host:port the server dials; without a port, :3001 is appended. Same rule as on POST /clients/outbound. |
autoUpdateCron |
string | null | No | This host's auto-update schedule for containers that belong to no project. An expression sets it, null goes back to the default from the settings, "" means the host takes part through its projects only, and leaving the field out keeps the stored value. Reaches the agent as part of its policy the moment it is saved. |
Response¶
A changed outboundTargetAddress takes effect immediately: the open agent socket is closed, any pending reconnect is cancelled, and the new address is dialled at once rather than at the next backoff step. The reply does not wait for that attempt ā an unreachable new address answers 200 and the client goes offline until a reconnect succeeds. Stored addresses are validated only when written, so a value saved before this check existed keeps working until it is edited.
- 400 ā no field given,
inboundAllowedIpnot an IPv4 address or network,inboundAllowedIpsent for an outbound client,outboundTargetAddresssent for an inbound client,outboundTargetAddressnot a usablehost:port, orautoUpdateCronneither empty,nullnor a valid cron expression. - 404 ā client not found.
Delete Client¶
DELETE /api/v1/clients/:clientId
Description: Removes a client registration. If the client is currently connected, its WebSocket connection is terminated.
Path Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
clientId |
string | Yes | The UUID of the client to delete. |
Response¶
The client's stored Docker state goes with it (foreign key cascade). For an outbound client any pending reconnect is cancelled first; an open agent socket is closed with 4000 Client deleted. Nothing on the host is touched.
- 404 ā client not found.
Reconnect An Outbound Client¶
POST /api/v1/clients/:clientId/reconnect
Description: Dials an outbound client's agent again, now. Any open socket is closed and a pending backoff step is dropped, so the attempt starts from scratch rather than waiting out the ladder. The reply does not wait for the attempt: an unreachable agent answers 200 and the client stays offline until a later attempt succeeds. Inbound clients own their own reconnect ladder, so the route refuses them.
Path Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
clientId |
string | Yes | The UUID of the client. |
Response¶
- 400 ā the client is not an outbound client.
- 404 ā no such client.
Run Auto-Update On One Client¶
POST /api/v1/clients/:clientId/auto-update/run
Description: Asks one agent to run its auto-update now, for every schedule it holds. The command carries nothing: which containers take part is the host's own reading of its labels, the same reading a scheduled run makes. The reply says whether the agent was asked, not what came of it ā the run reports itself as an autoupdate.run event, which is where every reader of a run's outcome takes it from. Nothing in the dashboard calls this route any more; it is kept for scripted use.
Path Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
clientId |
string | Yes | The UUID of the client. |
Response¶
- 404 ā no such client.
- 409 ā the client is offline, or its agent could not be asked (it does not declare the
auto-updatecapability, or sending failed). The message names which.
Issue A Registration Token¶
POST /api/v1/clients/:clientId/registration-token
Description: Issues a new registration token for an inbound client and replaces one that was still open. For a client that waits for its agent, this is the way past a token that ran out. For a registered client, it puts a reinstalled or moved agent back under the same client ā with its id, settings, projects and activity: the agent connected now keeps working until an agent registers with the new token, and is cut off at that moment. Like on Create Inbound Client, the response is the only place the token appears in the clear.
Path Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
clientId |
string | Yes | The UUID of the client. |
Response¶
- 400 ā the client is an outbound client; those register through the server dialling them.
- 404 ā no such client.
Register Client (Public)¶
POST /api/v1/register
Description: Public endpoint used by the client agent to register itself using a valid token. Returns the client's identity: a clientId and a permanent authToken for subsequent WebSocket connections. Both are issued by the server: the id is the one of the client the token was issued for, the auth token is new.
Request Body¶
| Field | Type | Required | Description |
|---|---|---|---|
token |
string | Yes | The registration token issued for the client, unused and not expired. |
hostname |
string | No | Hostname of the client device. Stored as unknown when missing. |
A body without a token is answered with 400, before the token is looked up. An unknown, used, replaced or expired token gets 403.
A clientId in the body, as sent by older agents, is ignored. It used to be taken over and
upserted, which let anyone holding a registration token name an existing client and replace its
auth token. The id the token was issued for is the only one it can register.
Example Request:
Response¶
The returned
tokenis the permanentauthTokenandclientIdthe id the server knows the client by. The agent saves both inidentity.jsonin its data directory; thetokenis used for all future WebSocket connections.The client's display name was given when it was created. Its allowed address too, if one was; otherwise the address the agent registers from becomes it. The token is cleared, so it registers nothing a second time.
With a token issued for a client that is already registered, the new agent takes the client over: its hostname is stored, the previous auth token stops being accepted, and a connected agent is closed with
4000 Client re-registered. The allowed address stays as the client has it ā a check switched off stays off. Theclient.registeredevent then carriesreregistered: true.
š³ Docker¶
Get Docker State¶
GET /api/v1/clients/:clientId/docker
Description: Returns the most recent Docker state snapshot stored for a client (containers, images, volumes, networks). The state is persisted each time an agent pushes a DOCKER_UPDATE message.
Path Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
clientId |
string | Yes | The UUID of the client. |
Response¶
{
"containers": [ /* DockerContainer[] */ ],
"images": [ /* DockerImage[] */ ],
"volumes": [ /* DockerVolume[] */ ],
"networks": [ /* DockerNetwork[] */ ],
"updatedAt": "2024-01-01T12:30:00.000Z"
}
A container carries no status text ("Up 2 hours"): Docker's would be frozen at the moment the agent took the state and stand still until the next push. The agent sends the timestamps instead, and the dashboard derives the text from them:
| Field | Type | Description |
|---|---|---|
startedAt |
string | State.StartedAt, ISO 8601. Missing when the container never started. |
finishedAt |
string | State.FinishedAt, ISO 8601. Missing when the container never stopped. |
exitCode |
number | State.ExitCode of the last run. |
All three are optional: an agent from before them does not send them, and a state stored before them does not hold them. A state from such an agent may still carry the old status field; nothing reads it.
An image carries the platform it was built for, from image inspect, and ā when a container runs it ā the cached result of the last update check:
| Field | Type | Description |
|---|---|---|
platform |
object | { "os": "linux", "architecture": "amd64" }. A local image holds one platform, even when its tag points to an index of several. Missing from agents that predate it. |
updateCheck |
object | { hasUpdate, remoteDigest, checkedAt, error? } for this tag, platform and local digest. Only on images a container runs: nothing else is checked. |
- 404 if no state has been received yet for this client.
Send Docker Action¶
POST /api/v1/clients/:clientId/docker/action
Description: Forwards a Docker action to the connected client agent and waits for the DOCKER_ACTION_RESULT. Used by the dashboard to start/stop/remove containers, pull/update/remove/prune images, and remove volumes/networks.
Request Body¶
| Field | Type | Required | Description |
|---|---|---|---|
action |
string | Yes | One of the DOCKER_ACTION_TYPES (see below). |
target |
string | Yes | Target identifier (container ID, image ref, volume name, network ID). Optional for image:prune. |
params |
object | No | Action-specific parameters. |
Supported actions:
container:start, container:stop, container:restart, container:remove, container:pause, container:unpause, container:recreate, image:remove, image:pull, image:update, image:prune, volume:remove, network:remove.
image:update is pull & recreate: the agent pulls target, then recreates every container configured with it that does not run the new image. params.containerIds (a list of container IDs on that host) limits the recreate to those containers, and params.force: true recreates them even when they already run the new image. For image:remove, params.force forces the removal. image:prune takes no target and removes every image on the host that no container uses, not even a stopped one, tagged or not (docker image prune -a).
Response¶
The body is the agent's DOCKER_ACTION_RESULT. When the agent reports success: false, the status is 500 and the body carries its error; the agent also reports the failure as action.failed in the activity list (the server does, for an agent too old to report it). When the connection closes or the time runs out, the server records action.unconfirmed instead: the action may still be running, and the agent reports how it ended once it is reconnected.
- 400 ā
actionnot in the list above,targetmissing for anything butimage:prune, orparamsnot an object. Checked before the agent is contacted: whatever passes goes to that host's Docker socket. - 503 ā client is not connected, or its connection closed before it reported a result (
"Client disconnected before reporting a result"). This used to wait for the full timeout and answer504. - 504 ā client did not respond within the action timeout (120 s).
On a successful
image:pullorimage:update, the backend automatically re-runsImageUpdateService.checkForUpdate(from@dim/shared/node) against the pulledtargetand updates the cached digest.
Refresh Docker State¶
POST /api/v1/clients/:clientId/docker/refresh
Description: Asks a connected client agent (fire-and-forget) to re-scan its Docker daemon and push a fresh DOCKER_UPDATE.
Response¶
- 202 ā request forwarded to the agent.
- 503 ā client is not connected.
Check Image Update¶
GET /api/v1/docker/images/check-update
Description: Checks the image registry for a newer image behind the given tag. Supports Docker Hub, ghcr.io, and lscr.io. Caches the result in image_update_checks.
The same tag is a different image on every platform, so the server looks up which hosts run the image in a container and on which platform, asks the registry once per tag, platform and local digest, and answers per host. A digest counts as outdated only if the registry's entry for that platform changed; a rebuild for another architecture is no update. An image no container runs is not checked.
Query Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
repoTag |
string | Yes | Image reference as stored in repoTags (e.g. nginx:latest). |
repoDigests |
string | No | Comma-separated repoDigests from the local image; narrows the check to the hosts holding one of them. |
Response¶
{
"repoTag": "nginx:latest",
"localDigest": "sha256:ā¦",
"remoteDigest": "sha256:ā¦",
"hasUpdate": true,
"platform": { "os": "linux", "architecture": "arm64" },
"remotePlatformDigest": "sha256:ā¦",
"remoteLabels": {
"org.opencontainers.image.version": "1.27.2",
"org.opencontainers.image.revision": "4f1cā¦",
"org.opencontainers.image.source": "https://github.com/ā¦"
},
"results": [
{
"clientId": "ā¦",
"platform": { "os": "linux", "architecture": "arm64" },
"hasUpdate": true,
"remoteDigest": "sha256:ā¦"
}
]
}
results holds one entry per host that runs the image; the top-level fields sum them up (hasUpdate if any host has one). An entry carries error when the remote digest cannot be fetched or the registry has no image for the host's platform (No image for linux/arm64) ā hasUpdate is then false. A refused request is named by its status: Registry rate limit reached (429), Registry denied access (401), Tag not found in registry (404), Registry request failed (HTTP 500) or Registry unreachable. A rate limit additionally sets rateLimited and retryAfterSeconds ā the registry's Retry-After, or 3600 when it sent none ā and, where the registry sends ratelimit-remaining (Docker Hub does), rateLimitRemaining carries its count. When no container runs the image, results is empty and error is No container runs this image. A request without repoTag gets 400.
remoteLabels holds the org.opencontainers.image.* labels of the image the update would bring. They are fetched only when hasUpdate is true and the platform is known, cost one more registry request, and are stored with the check until the remote digest changes. An image that sets no such labels gives {}; null means the registry did not give them. The same field appears on updateCheck of the images in a Docker state.
š ļø Settings & Maintenance¶
Get Settings¶
GET /api/v1/settings/cleanup
Description: Retrieves the settings block of config.yaml, every default filled in. The known values are returned as strings; a key added to the file by hand comes back as YAML read it. The security block is not part of the response: it is configured in config.yaml only.
Response¶
The defaults:
{
"image_version_cache_ttl_days": "30",
"image_version_cache_cleanup_orphans": "true",
"image_version_cache_cleanup_interval_hours": "24",
"image_update_check_interval_seconds": "0",
"container_auto_update_cron": "",
"container_auto_update_label": "dim.auto-update=true",
"container_auto_update_delay_label": "dim.auto-update-delay",
"notification_retention_days": "90",
"notification_retention_count": "500",
"notification_cleanup_interval_hours": "24",
"activity_level_overrides": ""
}
| Setting | Description |
|---|---|
image_version_cache_ttl_days |
Max age of a cached image_update_checks row (measured against checked_at). "0" disables TTL cleanup. |
image_version_cache_cleanup_orphans |
"true"/"false" ā also remove cache rows whose image_ref is no longer referenced by any client state. |
image_version_cache_cleanup_interval_hours |
Interval of the automatic cache cleanup scheduler. "0" disables the scheduler. |
image_update_check_interval_seconds |
Interval of the image update check sweep. "0" disables it. |
container_auto_update_* |
See Container Auto-Update. |
notification_retention_days |
Days to keep activity events, measured against occurredAt. "0" is not "forever": it falls back to 90. |
notification_retention_count |
Minimum number of the newest activity events always kept. |
notification_cleanup_interval_hours |
Interval of the automatic activity cleanup. "0" disables the scheduler. |
activity_level_overrides |
The level an event kind is recorded with: kind=level entries separated by commas, "" for none. level is trace, info, warning, error, or none to not record the kind at all. |
The notification_* names predate the rename to activity and are kept because they are stored values. token_retention_days and token_cleanup_interval_hours went with the list of registration tokens ā a token is a property of its client now and leaves nothing to clean up. Left in config.yaml, these and older removed keys are ignored and logged as unknown at startup.
Update Settings¶
PUT /api/v1/settings/cleanup
Description: Updates settings. All fields are optional; only provided fields are updated.
Request Body¶
Pass any of the setting keys to update them.
| Kind of setting | Keys | Accepted values |
|---|---|---|
| Counts, days, intervals | image_version_cache_ttl_days, image_version_cache_cleanup_interval_hours, image_update_check_interval_seconds, notification_* |
A non-negative whole number, as string or number. Stored as string. |
| Switches | image_version_cache_cleanup_orphans |
"true", "false" or a boolean. Stored as string. |
| Cron | container_auto_update_cron |
Empty, or a valid cron expression. |
| Labels | container_auto_update_label, container_auto_update_delay_label |
Any string. |
| Event levels | activity_level_overrides |
Empty, or kind=level entries separated by commas or line breaks. Any kind is accepted, each only once; none is refused for action.requested, action.completed, action.failed and autoupdate.run. |
Keys not listed are accepted and written as they are: the settings page sends back everything it read, including keys an operator added to config.yaml by hand, and rejecting or dropping them would delete them from the file.
Response¶
- 400 ā a value does not match the table above, or the body contains
security. Network and HSTS settings are configured inconfig.yamlonly; a session token must not be enough to lock every agent out.
A changed value takes effect without a restart:
image_version_cache_*restarts theImageUpdateCacheCleanupServicescheduler,image_update_check_interval_secondstheImageUpdateCheckSchedulerService, andnotification_*theNotificationCleanupService. A change to one of thecontainer_auto_update_*keys sends every connected agent a fresh policy; a changed label is also broadcast to the dashboards.activity_level_overridesis read whenever an event is written, so it applies to the next event and leaves the stored ones as they are.
Run Image Version Cache Cleanup¶
POST /api/v1/settings/cleanup/image-version-cache
Description: Runs ImageUpdateCacheCleanupService now, recorded as a manual run of image-cache-cleanup. Removes orphaned image_update_checks rows (if enabled) and expired rows (if image_version_cache_ttl_days > 0).
Response¶
Run Activity Cleanup¶
POST /api/v1/settings/cleanup/notifications
Description: Runs NotificationCleanupService now, recorded as a manual run of notification-cleanup, applying the retention policy to the activity table. The path keeps the old name; what it prunes is the activity list.
Response¶
Scheduler Status¶
GET /api/v1/settings/scheduler-status
Description: Returns the current status of the schedulers the server itself runs. Auto-update is not among them ā the agents run their own, and what they did is read from the activity list instead.
Response¶
{
"schedulers": {
"image-update-check": {
"isRunning": false,
"nextRun": "2026-04-18T11:00:00.000Z",
"lastRun": {
"trigger": "schedule",
"status": "partial",
"startedAt": "2026-04-18T10:00:00.000Z",
"finishedAt": "2026-04-18T10:00:41.000Z",
"result": { "checked": 7, "total": 12, "pausedRegistries": ["registry-1.docker.io"] },
"error": null
},
"registries": [
{
"registry": "registry-1.docker.io",
"targets": 12,
"checked": 7,
"lastCheckedAt": "2026-04-18T10:00:00.000Z",
"pausedUntil": "2026-04-18T16:00:00.000Z",
"remaining": 0,
"error": "Registry rate limit reached (429)"
}
]
},
"image-cache-cleanup": {
"isRunning": false,
"nextRun": "2026-04-19T04:00:00.000Z",
"lastRun": {
"trigger": "manual",
"status": "success",
"startedAt": "2026-04-18T09:12:00.000Z",
"finishedAt": "2026-04-18T09:12:00.000Z",
"result": { "orphansRemoved": 2, "expiredRemoved": 7 },
"error": null
}
},
"notification-cleanup": { "isRunning": false, "nextRun": "2026-04-19T04:00:00.000Z", "lastRun": null }
}
}
Every scheduler reports isRunning, nextRun (null while its interval is 0) and lastRun, the last run it finished (null before its first). lastRun.status is success, partial (the run finished but a rate limit paused a registry), failed (with error) or interrupted (the server stopped during the run; no finishedAt, no result). result has one shape per scheduler: { checked, total, pausedRegistries }, { orphansRemoved, expiredRemoved } or { removed }. The state is kept in the database and survives a restart.
registries (image update check only) has one entry per registry host the checked images come from, built from the current images, so it is there before the first sweep. pausedUntil is set while a rate limit pauses the host, remaining is the last ratelimit-remaining it sent (null for registries that send none), and error is the rate limit while the pause lasts, or the error every check of the last sweep failed with.
Run Image Update Check Now¶
POST /api/v1/settings/image-update-check/run
Description: Triggers a full image update check sweep synchronously. Every known image tag is checked against its registry and the result is written to image_update_checks. Unlike the scheduled sweep, it also asks registries that are paused by a rate limit; one that refuses again is paused anew.
Response¶
Container Auto-Update¶
A container takes part in automatic updates if it carries the configured Docker label (container_auto_update_label), or if the project it belongs to has auto-update switched on (see Projects). The label wins over the project, and the same label key carrying false opts a container out of both.
Neither source is stored against a container: both are read off its labels, which is what lets the agent decide it. The server performs no runs and contacts no registry on a host's behalf ā it resolves the schedule inheritance, sends each agent its policy (AUTO_UPDATE_POLICY), and reads back the autoupdate.run events the agents report. A host that updated itself while this server was down therefore appears in full as soon as it hands its queue over.
Related settings:
| Key | Description |
|---|---|
container_auto_update_cron |
The default schedule every host and project inherits while it names none of its own. Empty means only hosts and projects with an expression of their own take part. |
container_auto_update_label |
Docker label marking a container for auto-update. Format key=value or key (any value). |
container_auto_update_delay_label |
Docker label holding a per-container delay in days. Empty disables delay support. |
Changing any of them sends every connected agent a fresh
AUTO_UPDATE_POLICY.
The server has no auto-update endpoint beyond these settings. What each host did is in the
activity, where the host itself put it ā autoupdate.run events, queried through
Activity ā and a run is asked for one host at a time, see
Run Auto-Update On One Client.
Two endpoints that used to sit here are gone, with the fleet panel they fed:
GET /api/v1/settings/container-auto-update/status, which folded the newest autoupdate.run
per host and schedule into one response, and POST /api/v1/settings/container-auto-update/run,
which asked every connected agent at once and answered with a count of commands sent.
Validate Cron¶
POST /api/v1/settings/container-auto-update/validate-cron
Request:
Response:
An invalid or empty expression answers 200 with { "valid": false } and no reason. A body without a string expr gets 400.
Auto-Update Label¶
GET /api/v1/settings/container-auto-update/label ā the configured label on its own.
Response:
The container lists read it to show which containers carry the label, and every
signed-in user sees those lists ā reading the whole settings block for one string
is more than they need. A change to the setting broadcasts
AUTO_UPDATE_LABEL_UPDATE.
There is no endpoint for enrolling a single container: a container takes part
because it carries this label, or because the project it belongs to has auto-update
switched on (see Projects). The same label key carrying false
opts a container out of both.
š¦ Projects¶
A project is a group of containers across the whole fleet, defined by a query. DIM stores the name, the query and the settings; membership is resolved from the Docker state the agents report on every request, so a container that stops matching leaves the project by itself.
A container belongs to one project at most. Creating or changing a query that would take
containers another project already has is refused with 409. A container started later can
still match two queries. It is then a conflict: it is listed under every project it
matches, counted in their conflictCount and marked as an error in the dashboard, and it is
updated through none of them.
- Without the auto-update label it is excluded from auto-update altogether. Every run of a
project it matches reports
autoupdate.conflictat levelerror. - With the label it is updated on the host schedule instead, and the host run reports
autoupdate.conflictat levelwarning. A host without a schedule of its own leaves it excluded, as above.
The event carries data.projectIds, data.projectNames and data.fallback ("host" or
null), and the container in subject. A run that reported a conflict is never a silent
"nothing to do" run; its autoupdate.run carries data.conflicts.
cron is nullable on purpose ā null means inherit the default from the settings, not
off. Auto-update is switched off through autoUpdate.
The query¶
A query is a list of criteria. Each one is asked about one container on one host:
field |
Compared with |
|---|---|
client.displayName |
The client's display name, or its hostname when it has none. |
client.hostname |
The client's hostname. |
container.name |
The container name, without the leading /. |
container.composeProject |
The Compose project the container was started by (its com.docker.compose.project label). A container started without Compose has none. |
image.name |
configImage (or image). A value without a tag is compared with the repository only, so every tag matches; with a tag, with repository:tag (a reference without a tag counts as latest). |
op:equalsorwildcard(*any characters,?exactly one). Both ignore case.negate: inverts the criterion. A missing attribute (an image without a reference, a container started without Compose) matches nothing, so its negation matches.join:andoror, joining the criterion to everything before it. Criteria are evaluated strictly from top to bottom without precedence:A or B and Cis(A or B) and C. The first criterion'sjoinis ignored.
[
{ "id": "c1", "join": "and", "field": "container.name", "op": "wildcard", "negate": false, "value": "nextcloud-*" },
{ "id": "c2", "join": "or", "field": "image.name", "op": "wildcard", "negate": false, "value": "redis:7*" },
{ "id": "c3", "join": "and", "field": "client.displayName", "op": "wildcard", "negate": true, "value": "test-*" }
]
List Projects¶
GET /api/v1/projects
Response:
{
"projects": [
{
"id": "3f0cā¦",
"name": "web",
"query": [{ "id": "c1", "join": "and", "field": "container.name", "op": "wildcard", "negate": false, "value": "web-*" }],
"autoUpdate": true,
"cron": "0 3 * * *",
"createdAt": "2026-09-13T08:00:00.000Z",
"clientIds": ["ā¦"],
"containerCount": 4,
"imageCount": 3,
"conflictCount": 0
}
]
}
Preview a Query¶
POST /api/v1/projects/preview
Request:
Answers what the query matches right now, and which of those containers already belong to
another project. excludeId is the project being edited, whose own members are no conflict.
{
"members": [{ "clientId": "ā¦", "containerId": "ā¦", "containerName": "web-app" }],
"conflicts": [{ "clientId": "ā¦", "containerId": "ā¦", "containerName": "redis", "projectId": "ā¦", "projectName": "cache" }]
}
Create Project¶
POST /api/v1/projects
Request:
name and a query with at least one criterion are required; autoUpdate defaults to false
and cron to null. A query that matches nothing yet is allowed. Answers 201 with the
created project. A name that is already taken answers 409, as does a query that overlaps
another project (the body then carries conflicts as above); an invalid cron answers 400.
Update Project¶
PATCH /api/v1/projects/:id
Request:
Every field is optional, but at least one has to be given; a field that is absent is left as
it is, which is why "cron": null (inherit) has to be distinguishable from "not mentioned".
The same 409 checks apply as on create. Answers the updated project, or 404 for an
unknown id.
Delete Project¶
DELETE /api/v1/projects/:id
Removes the DIM entry and nothing else ā no container is touched. Answers { "ok": true },
or 404 for an unknown id.
Every mutating endpoint broadcasts PROJECTS_UPDATE with the full list response.
š£ Activity¶
Everything that happened, as its originator reported it.
An event carries no message. It carries a kind, a level, what it is about and the facts
of that kind ā an exit code, a health status, a run's counts ā and the text is composed in
@dim/shared out of those, for the dashboard and the webhooks alike. That is what lets an
agent of an older version stay useful: it reports the same facts and how they are worded is
not its business. It also means filtering by kind and level is exact rather than a search
through prose.
level is one of trace, info, warning and error, lowest first. trace marks routine
bookkeeping ā an agent connecting or disconnecting ā which the dashboard hides by default.
A level this build does not know is read as info.
kind is not a closed set on the wire. An agent of another version may report a kind
this server does not know; it is stored as it is, and the dashboard falls back to printing
the kind itself rather than dropping an observation nobody can make again. The
Webhooks page lists the kinds with the facts each carries.
Two timestamps, and the difference matters. occurredAt is the originator's clock and
orders the list; receivedAt is the server's. After an offline stretch an event from 03:00
arrives at 08:00: it belongs at 03:00 in the list, while the seen state is per event, so a
late arrival cannot slip in under entries a user has already worked through. The gap between
the two also exposes an agent whose clock is wrong.
correlationId is entered by whoever caused the group ā the server's actionId for an
action from the dashboard, the agent's runId for an auto-update run. Nothing matches
names, and nothing depends on arrival order.
subject.projectIds lists the projects an event is about. The server enters it when it stores
the event, from the projects and host states at that moment: the projects of the container,
found by id or name, or ā for a container that is already gone ā matched from the name and
image the event carries; for an event about an image alone, the projects of every container on
that host configured with that reference. A project the originator named (subject.projectId
of an auto-update run, data.projectIds of a conflict) is always included. A query changed
later does not rewrite stored events. Events without a host or without a container or image
carry none.
List Activity¶
GET /api/v1/activity
seen is that of the calling user; who else has seen an event is not part of the answer.
Response:
[
{
"id": "d3f1ā¦",
"occurredAt": "2026-09-13T03:00:07.412Z",
"receivedAt": "2026-09-13T08:14:02.900Z",
"source": "agent",
"clientId": "ā¦",
"kind": "container.died",
"level": "warning",
"correlationId": "run-ā¦",
"subject": {
"containerName": "nextcloud-app",
"containerId": "ā¦",
"imageRef": "nextcloud:31",
"projectIds": ["3f0cā¦"]
},
"data": { "exitCode": 1 },
"seen": false
}
]
Newest first by occurredAt.
Mark Seen¶
POST /api/v1/activity/seen with { "ids": ["ā¦"] } marks the listed events seen by the
calling user in one request (ids that are not there are skipped; an empty or missing list is a
400) and answers { "ok": true }. There is no endpoint for a single event: the dashboard
marks a whole group, or everything the filters leave, with this one.
Delete Activity¶
DELETE /api/v1/activity removes all events and answers { "ok": true }. Single events
cannot be deleted; retention and "Delete all" are the only ways an event goes.
Retention runs on its own through notification_retention_days and
notification_retention_count ā the setting names predate the rename and are kept because
they are stored values; the page they are set on is called "Activity History".
Over the dashboard WebSocket: a new event goes out as ACTIVITY_APPENDED with only the events
stored for the first time (a repeat from the at-least-once delivery is not sent again); marking
sends ACTIVITY_SEEN with the ids that turned seen, to the sessions of the calling user only;
"Delete all" broadcasts ACTIVITY_UPDATE with an empty list.
Every event stored for the first time also goes to the webhooks whose filters it passes.
šŖ Webhooks¶
Targets outside the dashboard that events are reported to, each with a JSON body template of its own. The template syntax, the context a template can read and examples for common services are on the Webhooks page.
A webhook as the API returns it:
{
"id": "10d18932-ā¦",
"name": "Ops channel",
"enabled": true,
"url": "https://hooks.example.com/services/ā¦",
"method": "POST",
"headers": { "Authorization": "Bearer ā¦" },
"bodyTemplate": "{ \"text\": \"{{event.message}}\" }",
"minLevel": "warning",
"kinds": ["container.*", "autoupdate.run"],
"timeoutMs": 10000,
"lastStatus": 200,
"lastError": null,
"lastAttemptAt": "2026-10-02T21:01:49.380Z",
"createdAt": "2026-10-02T20:58:12.004Z",
"updatedAt": null
}
| Field | Description |
|---|---|
url |
http:// or https://. May contain placeholders, inserted as text. |
method |
POST (default) or PUT. |
headers |
Sent with every delivery; values may contain placeholders. Content-Type: application/json is always set. Returned in the clear. |
bodyTemplate |
JSON with {{ā¦}} placeholders, filters and $if/$map/$join directives (see Webhooks), stored as written, at most 64 KiB. Refused with 400 when it is not valid JSON or a placeholder or directive does not parse. |
minLevel |
The lowest level sent: trace, info, warning (default) or error. |
kinds |
Kind patterns, * as wildcard. Empty sends every kind. |
timeoutMs |
Per attempt, 1000ā60000 (default 10000). |
lastStatus / lastError / lastAttemptAt |
How the last delivery ended. Read-only. |
List Webhooks¶
GET /api/v1/webhooks answers every webhook, ordered by name.
Create Webhook¶
POST /api/v1/webhooks with the fields above except id and the read-only ones. Only
name, url and bodyTemplate are required. Answers 201 with the stored webhook.
Update Webhook¶
PUT /api/v1/webhooks/:webhookId with the same body as a create; it replaces the whole
configuration. Answers the stored webhook, or 404.
Delete Webhook¶
DELETE /api/v1/webhooks/:webhookId answers { "success": true }, or 404.
Send A Test Delivery¶
POST /api/v1/webhooks/test with the same body as a create ā saved or not, so the editor can
try a target before storing it. Renders the sample event for the webhook's kinds (see
Webhooks; container.died when none matches), sends it once
without retries, and records nothing.
{
"ok": true,
"status": 200,
"error": null,
"body": { "text": "[warning] docker-01: Container nextcloud-app exited with code 1" },
"response": "ok"
}
body is what was sent, response the first 500 characters of the answer. A target that
refuses is still a 200 here, with ok: false; 400 means the webhook itself is invalid.
š Misc¶
Health¶
GET /api/health (no /v1 prefix)
Description: Liveness probe, used by the image's HEALTHCHECK and the CI smoke test. No authentication ā a probe has no session, and the answer discloses nothing.
| Status | Body | Meaning |
|---|---|---|
200 |
{"status":"ok"} |
The process serves requests and its database is reachable |
503 |
{"status":"error"} |
The database could not be queried |
Agent connections are not consulted: one offline agent must not mark the control plane as broken. The agent's web UI has its own GET /api/health on port 3001, which reports only that the agent process answers ā not whether it is connected to the server. It serves the container's HEALTHCHECK alone: it exists only in the container image and answers only loopback, everyone else gets 404.
Why under /api: the server answers every path outside /api with the dashboard's index.html and HTTP 200, so a probe on /health would report success even without the route. Under /api, an unknown path is a 404.
Reachability¶
GET /api/v1/ping
Description: Answers "is there a DIM server at this URL?". The agent's status and register pages call it against the agent's configured server; an address an operator has just typed is checked by the registration itself. It deliberately checks nothing else: a server with a broken database is still reachable, and reporting otherwise during setup would point at the wrong problem. For "can this instance serve requests", use Health.
Response¶
š WebSockets¶
Dashboard Connection¶
GET /ws/dashboard
Description: WebSocket endpoint for the web dashboard to receive real-time client status updates.
Authentication¶
The dim_session cookie, which the browser sends with the handshake by itself. A token query parameter is no longer accepted. Without the cookie the server closes with 4001 Unauthorized, with an invalid or expired token with 4001 Invalid Token.
Behavior¶
- On connect: The server immediately sends a
CLIENTS_UPDATEwith the full client list, then oneDOCKER_STATE_UPDATEper client that has a stored state, then anACTIVITY_UPDATEwith the activity list. A dashboard therefore needs no REST call to fill its first screen. - A ping/pong heartbeat runs every 30 seconds to detect dead connections.
- All broadcasts from
ProxyService(e.g., agent connects/disconnects) are forwarded to all active dashboard sessions.
Events (Server -> Client)¶
The list below is a contract in code: DashboardMessageSchema in shared/src/dashboardMessages.ts holds every message with its payload as one discriminated union. The server's senders take that type, and the dashboard parses each message against the schema and drops what does not match.
| Event | Payload | Description |
|---|---|---|
CLIENTS_UPDATE |
Client[] |
Full list of all clients and their statuses. |
DOCKER_STATE_UPDATE |
{ clientId, state: DockerState } |
Docker state snapshot pushed by an agent, rebroadcast to dashboards. |
DOCKER_ACTION_RESULT |
{ clientId, result: DockerActionResult } |
Result of a previously dispatched Docker action. Sent to every dashboard; the request that asked for the action gets the same result as its own answer, which is what the dashboard acts on. |
SCHEDULER_STATUS_UPDATE |
{ scheduler, status } |
One scheduler's status, in the shape of Scheduler Status, whenever a run starts or ends or its timer is set. Auto-update has none, because the server runs none. |
AUTO_UPDATE_LABEL_UPDATE |
{ labelFilter: string } |
The auto-update label setting changed. |
PROJECTS_UPDATE |
{ projects: ProjectSummary[] } |
A project was added, changed or removed. |
ACTIVITY_UPDATE |
ActivityRecord[] |
The whole activity list: on connect, with the seen state of the session's user, and empty after "Delete all". |
ACTIVITY_APPENDED |
ActivityRecord[] |
Events stored for the first time, to be merged into the list by id. |
ACTIVITY_SEEN |
{ ids: string[] } |
Events the session's user has just marked seen. Sent to that user's sessions only. |
Agent Connection¶
GET /ws/agent
Description: WebSocket endpoint for client agents. Requires the identity issued during registration ā the clientId and the authToken together.
Query Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
clientId |
string | Yes | The server-issued clientId from the agent's identity.json. |
token |
string | Yes | The permanent authToken from the agent's identity.json. |
A request missing either half is closed with 4001 Authentication required. The token may
also be sent as Authorization: Bearer <token>; the id has no header form.
Authentication Stages¶
- Id and token are looked up as a pair ā both have to name the same row (
4003 Invalid credentialsotherwise). The id alone is no secret, and a token alone used to make a client whoever its token happened to belong to. - Client's IP is checked against
security.allowed_networks(4003 Access denied). - A token that belongs to an outbound client is refused (
4003 Access denied): those are dialled by the server and never connect here. - Client's IP is checked against the client's allowed address or network; a client whose check is switched off skips this step (
4003 IP address mismatch). - A 5-second window is given for the client to send an
AUTHhandshake message (4001 Authentication timed outotherwise). AnAUTHwhose payload does not parse is closed with4000 Invalid payload; any other first message is answered withAUTH_FAILUREand closed with4003 Forbidden.
A second connection under the same client id replaces the first, which is closed with 4000 Replaced by new connection.
Client -> Server Events¶
After AUTH, everything an agent sends is one of three kinds:
| Kind | Messages | Delivery |
|---|---|---|
| State ā only the latest value means anything | DOCKER_UPDATE, AGENT_STATUS, IMAGE_CHECKS |
Not queued. Sent in full after every AUTH_SUCCESS, then whenever it changes. |
| Event ā each one counts | ACTIVITY |
Queued by the agent until the server acknowledges the ids (ACTIVITY_ACK). |
| Answer ā to one request | DOCKER_ACTION_RESULT |
Carries the actionId of the DOCKER_ACTION it answers. |
Every payload is checked against its schema in @dim/shared before the server acts on it; one that does not parse is logged and dropped.
AUTH
Description: Initial handshake, sent immediately after connection.
Payload:
{
"hostname": "client-hostname",
"version": "1.0.0",
"capabilities": ["auto-update"],
"timezone": "Europe/Berlin"
}
All four fields are required. timezone is the IANA zone of the agent process (TZ, UTC in a
container without it). The agent plans its auto-update cron expressions in that zone; the
server stores the value only to show it.
capabilities says what this agent's build can do (currently auto-update and
project-query). The server reads it by asking whether an entry is in the list, never by exhausting it,
so a newer agent may name something this server has never heard of. auto-update means the
agent runs its own auto-update and is sent an AUTO_UPDATE_POLICY.
DOCKER_UPDATE
Description: Full Docker state snapshot (containers, images, volumes, networks). Sent after connect, on relevant Docker events, and on REQUEST_STATE_UPDATE.
Payload: DockerState without updatedAt (the server stamps it on persist).
DOCKER_ACTION_RESULT
Description: Response to a server-dispatched DOCKER_ACTION. Resolves the backend's pending promise and is rebroadcast to dashboards.
Payload:
ACTIVITY
Description: Events the agent has observed and has not had acknowledged yet. Sent as they happen while connected, and as a batch on every reconnect. A batch, not one message per event: an agent that was offline has a queue to hand over.
Payload:
{ "events": [ { "id": "ā¦", "occurredAt": "ā¦", "kind": "container.died", "level": "warning", "correlationId": "ā¦", "subject": { }, "data": { } } ] }
source and clientId are taken from the connection, not from the payload ā an agent may only ever speak about itself. Delivery is at-least-once: the event keeps its id until the server acknowledges it, and the primary key makes a second copy a no-op.
The events are parsed one by one. A batch whose envelope does not parse (no events array) is dropped without an ack, so the agent keeps offering it. A single event that does not parse but carries an id is the exception: it is acknowledged without being stored and logged. Offering it again would change nothing, and because the agent hands its queue over in order, it would block every event behind it. An unparsable event without an id is simply dropped.
AGENT_STATUS
Description: How the agent is doing ā what only it can know. A state: sent after every AUTH_SUCCESS and whenever any of it changes, also after a run that had nothing to do, which reports no autoupdate.run.
Payload:
{
"autoUpdate": {
"lastRun": "2024-01-01T03:01:12.000Z",
"nextRun": "2024-01-02T03:00:00.000Z",
"runningSince": null,
"schedules": [
{ "schedule": "host", "cron": "0 3 * * *", "nextRun": "2024-01-02T03:00:00.000Z" },
{ "schedule": "project:9c44f3a5-ā¦", "cron": "0 4 * * 0", "nextRun": "2024-01-07T04:00:00.000Z" }
]
},
"docker": { "reachable": true, "error": null, "since": "2024-01-01T12:29:58.000Z" }
}
autoUpdate folds the host's schedules into one answer: the newest run any of them finished,
the earliest one any of them has planned, and the earliest start of a run still under way.
ISO 8601 times on the agent's clock, null for none. lastRun and nextRun are stored as the
client's autoUpdateLastRun and autoUpdateNextRun; lastRun only ever moves the stored value
forward, so an agent that lost its state file does not take back a run the server knows of.
schedules gives the same plan schedule by schedule and is stored as the client's
autoUpdateSchedules; an agent that sends no schedules (one that predates them) clears it.
runningSince and docker are kept with the connection only and reported as the client's
autoUpdateRunningSince and docker. docker is null before the agent has tried to reach
its daemon; since is when reachable last changed. A report that changes anything is
followed by a CLIENTS_UPDATE to the dashboards.
IMAGE_CHECKS
Description: The registry answers of the last run of each of the agent's auto-update schedules ā including runs that had nothing to update. A state: sent after every AUTH_SUCCESS and whenever a run brought new answers.
Payload:
{
"checks": [
{
"imageRef": "nginx:latest",
"platform": { "os": "linux", "architecture": "amd64" },
"localDigest": "sha256:ā¦",
"remoteDigest": "sha256:ā¦",
"hasUpdate": false,
"checkedAt": "2024-01-01T03:00:41.000Z"
}
]
}
Written into image_update_checks, the cache the update indicator reads. Only an answer newer
than the stored one wins, so the same answers sent again on the next connect change nothing.
error is present for a check that failed.
Server -> Client Events¶
AUTH_SUCCESS
Payload: {}
AUTH_FAILURE
Description: Sent when the first message of an inbound connection is not AUTH, directly before the socket is closed with 4003 Forbidden. Every other refusal is a close code alone (see Authentication Stages).
Payload: {}
DOCKER_ACTION
Description: Instructs the agent to run a Docker action (start/stop/pull/update/prune/remove/...). Fire-and-forget; the agent answers with DOCKER_ACTION_RESULT.
Payload:
{
"actionId": "ā¦",
"action": "container:start",
"target": "<container-id | image-ref | volume | network>",
"params": { }
}
The agent validates the payload before running anything. An unknown action, a missing target (allowed empty only for image:prune) or a params value that is not an object is answered at once with { "actionId": "ā¦", "success": false, "error": "Invalid action: <field>: <reason>" }. A payload without actionId cannot be answered and is dropped with a warning in the agent's log.
REQUEST_STATE_UPDATE
Description: Asks the agent to immediately emit a fresh DOCKER_UPDATE.
Payload: {}
AUTO_UPDATE_POLICY
Description: Everything the agent needs to run auto-update by itself. Sent right after AUTH_SUCCESS, and again whenever the settings, a project, this client's own schedule or its display name change. Only to agents that declared the auto-update capability; projects stays empty for agents that did not also declare project-query, because they would read a project as a Compose stack name.
Payload:
{
"updatedAt": "2026-09-13T08:12:00.000Z",
"host": { "hostname": "docker-01", "displayName": "Prod Web" },
"labelKey": "dim.auto-update",
"labelValue": "true",
"delayLabelKey": "dim.auto-update-delay",
"hostCron": "0 4 * * 0",
"projects": [
{
"id": "3f0cā¦",
"name": "nextcloud",
"query": [ ⦠],
"createdAt": "2026-09-13T08:00:00.000Z",
"autoUpdate": true,
"cron": "0 3 * * *"
}
]
}
Every schedule in it is already resolved ā default, then host, then project ā so the
agent never sees a null and never has to know the inheritance rules. hostCron covers the
containers on this host that belong to no project; an empty string means it has no schedule
of its own. Projects with autoUpdate: false are listed too, because a container may be
enrolled through its label while still belonging to a project, and the project is what decides
when it is updated. The agent evaluates the queries itself; client criteria are matched
against host, the host as the server knows it, so the agent reaches the membership the
dashboard shows. labelValue: null means the presence of labelKey is enough; an empty
labelKey switches the label route off, and with it the =false opt-out.
The agent stores the policy on disk and keeps acting on it while the server is unreachable.
AUTO_UPDATE_RUN
Description: Run the configured auto-update now, without waiting for a schedule. Sent by POST /api/v1/clients/:clientId/auto-update/run, and only to agents that declared the auto-update capability. The agent runs every schedule it holds, each with its own runId, and marks the resulting autoupdate.run events manual: true.
Payload: {}
It deliberately carries no list of containers: which of them take part is the host's own reading of the labels in front of it, and the server does not hold the better one. A run asked for differs from a scheduled one only in that somebody is waiting ā so it skips the spreading jitter, and reports even when there was nothing to do.
ACTIVITY_ACK
Description: The ids the server has stored. The agent drops them from its queue; ids it does not name stay and are offered again.
Payload: { "ids": ["ā¦"] }
After a successful
AUTH/AUTH_SUCCESSexchange, the server registers the client inProxyServiceand broadcasts aCLIENTS_UPDATEto all connected dashboards.