Group policies
Store one update policy per container group in Drydock, inherited by current and future members.
A group policy is an update policy that Drydock stores for one named group. Every container in that group inherits it, including containers that join the group later, so you set a rule once instead of labeling each container.
What a group is
Drydock derives a container's group from its labels, in this order: dd.group, then com.docker.compose.project, then com.docker.stack.namespace. A container with none of them has no group and no group policy. This is the same identity notification group routing and GET /api/v1/containers/groups use.
- Names match exactly. Case, whitespace and punctuation count, and nothing is trimmed or normalized.
- An empty
dd.groupdoes not fall through to the Compose project. A container withdd.group=""has the group"", which can't carry a policy, so that is the way to opt one container out of its Compose project's policy. - Matching is fleet-wide by name. A policy for
paymentsreaches every container in that group on the controller and on every agent. If two hosts need different policies, give their containers differentdd.groupvalues. - The browser's "Set group" override is a display preference. It never changes which policy applies.
What a policy sets
A policy carries the six declarative update-policy fields, with the same values the dd.updatePolicy.* labels accept:
| Field | Values |
|---|---|
maturityMode | all or mature |
maturityMinAgeDays | integer from 1 to 365 |
maturitySkipReleases | integer from 0 to 100; 0 disables release counting |
maturityReleaseFallbackDays | integer from 0 to 365; 0 disables the count gate age fallback |
skipTags | list of tags to skip |
skipDigests | list of digests to skip |
A policy has to set at least one field. An empty skipTags or skipDigests list is dropped, because the only layer below a group is the watcher environment, which has no skip lists. maturityMode: all is kept, since it deliberately overrides a watcher default of mature. An explicit 0 in either release-count field overrides the watcher default. See release-count maturity for family and evidence rules. snoozeUntil is not a group field: a snooze stays a per-container override.
Action rules
A policy can also carry two action rules. They only restrict: a group can stop Drydock from doing something to its members, and it can never allow something that was already refused.
| Rule | Values | Effect |
|---|---|---|
actions.updateMode | manual or notify | A ceiling on the update mode. The mode that binds a member is the more restrictive of the global update mode and the group's, ordered notify, manual, auto. |
actions.exclude | Trigger references | A hard stop. A matching update-action trigger is never used for a member. |
auto is not a valid group value, and there is no group include list, auto list or un-exclude.
Update mode ceiling
The ceiling is applied everywhere the global update mode is applied to a container.
- Group
manual. Members are never updated automatically, even when the global mode isautoand the container hasdd.action.auto. Their candidates go to the approval queue, and the Update button stays enabled for a person to click. - Group
notify. Drydock won't apply updates to members. The Update button is locked with thegroup-notify-onlyblocker, the API rejects an update request withGroup policy 'payments' allows notifications only, and no approval rows queue. - Global stricter than the group. A group can't relax the global mode. A group
manualunder globalnotifystaysnotify, and the container reports the global mode as the one that applies.
A member's own dd.action.auto or dd.action.include label does not lift a group ceiling or a group exclusion. A container leaves the ceiling by changing or clearing dd.group (or its Compose project), not by labeling around it.
When a held member is something other members depend on through dd.depends_on, its dependents in the same automatic batch or digest wait too, the way they do for a closed maintenance window, and update after the held member has been approved and updated.
Excluding triggers
actions.exclude entries use the same grammar as the dd.action.exclude label: a trigger id, a trigger name or provider.name, optionally followed by :threshold (for example docker.local:major). Matching is case-insensitive. The API rejects an entry that is empty, contains a comma or has a threshold Drydock doesn't support. An entry that matches no registered action trigger is saved with a warning, since an agent's triggers may be offline or register later.
An exclusion reaches exactly what dd.action.exclude reaches. Update-action triggers (docker, dockercompose, portainer) are blocked on the manual and automatic paths, with the trigger-excluded blocker naming the group. command triggers are blocked on automatic dispatch only, and a manual run of a command trigger is not gated, as with the label. Notification triggers ignore actions.exclude. The container's own dd.action.exclude is checked first and keeps the attribution when both match.
A group rule is an operator safety net against unintended automation. It is as strong as your control over each member's labels, since whoever sets dd.group decides whether the group applies. Container lifecycle actions, rollback, image pruning, scans and notification delivery are not restricted by it.
Agents behave the same way. A group exclusion can only narrow what a trigger already compatible with the container's agent would do, and the policy is enforced on the controller.
Precedence
Each field resolves on its own, and the last layer that sets it wins:
| Priority | Layer | Example |
|---|---|---|
| 1 (highest) | UI/API override | PATCH /api/v1/containers/:id/update-policy |
| 2 | Container label | dd.updatePolicy.maturityMode=all |
| 3 | Group policy | PUT /api/v1/group-policies/:id |
| 4 | Watcher environment | DD_WATCHER_LOCAL_MATURITY_MODE |
| 5 (lowest) | Built-in | maturityMode=all, empty skip lists |
Skip lists replace the lower layer's list instead of being added to it. The container's updatePolicySources names the layer each field came from, and group is one of the values. The container response also carries groupPolicy, a snapshot of the policy (id, group, revision and values) it was last resolved against.
A label or an override on a member still wins over the group for the same update-policy field, so those fields are a default for the group, not a lock. Action rules work the other way round: they restrict whatever the lower and higher layers produce. Whoever controls a container's labels, including dd.group, controls whether the group policy reaches it. Don't treat a group policy as a security boundary.
Existing override actions work the same against the new layer. revert-to-declarative removes an override and reveals the label, then the group, then the watcher value. skip-current copies the effective skip list, group values included, into the override, so later edits to the group's list no longer reach that field on that container until you revert it.
Membership changes
Membership is worked out from the container's current labels every time Drydock writes the container.
- A container that joins a group gets its policy on its next write, and a policy write re-resolves every current member immediately.
- A container that leaves a group, or whose group's policy is deleted, loses those values. Its fields fall back to label, watcher or built-in values. Nothing from the old group is carried over.
- Overrides belong to the container, so they survive both, including when Docker recreates the container. A recreated container picks up the policy of whichever group its new labels name.
API
Reads need the read scope. Writes need admin, because one write changes every current and future member of the group. A containers:update key can still set per-container overrides but can't write a group policy. Group names aren't URL-safe, so policies are addressed by id.
# Create a policy that holds the group's members to manual updates and keeps one trigger away
curl -X POST http://drydock:3000/api/v1/group-policies \
-H 'Content-Type: application/json' \
-d '{"group":"payments","actions":{"updateMode":"manual","exclude":["docker.local:major"]}}'
# Create an update-policy default
curl -X POST http://drydock:3000/api/v1/group-policies \
-H 'Content-Type: application/json' \
-d '{"group":"payments","updatePolicy":{"maturityMode":"mature","maturityMinAgeDays":14}}'{
"policy": {
"id": "5b0d7c2e-4f6a-4c55-9a53-0a1f6f1f3c11",
"group": "payments",
"revision": 1,
"updatePolicy": { "maturityMode": "mature", "maturityMinAgeDays": 14 },
"actions": {},
"createdAt": "2026-10-03T12:00:00.000Z",
"createdBy": "user:admin",
"updatedAt": "2026-10-03T12:00:00.000Z",
"updatedBy": "user:admin"
},
"applied": { "members": 3 },
"warnings": []
}| Request | Scope | Notes |
|---|---|---|
GET /api/v1/group-policies | read | Every policy with members: { count, agents }. agents lists the hosts, with null for the controller |
GET /api/v1/group-policies/:id | read | One policy with its members, or 404 |
POST /api/v1/group-policies | admin | Body { group, updatePolicy?, actions? }. Returns 201, or 409 when the group already has a policy |
PUT /api/v1/group-policies/:id | admin | Full replace. Body { revision, updatePolicy?, actions? }, with an optional group that must match the stored one. Leaving actions out clears them |
DELETE /api/v1/group-policies/:id?revision=N | admin | Deletes the policy and re-resolves its members |
Things worth knowing:
revisionguards against lost updates. Send the revision you read. A stale one gets a 409 and writes nothing, so reload the policy and apply the change again.- A
PUTthat matches the stored policy returnschanged: false, writes nothing, doesn't bump the revision and isn't audited. - The group is immutable. A
PUTwith a differentgroupis a 400. Delete the policy and create one for the new group. applied.memberscounts the stored container records that were re-resolved, on the controller and on agents, connected or not. A policy for a group with no current members still saves, with a warning that it applies to future members.- Unknown fields are rejected with a 400, and so is
actions.updateMode: "auto", because group rules only restrict. - If re-resolving members fails after the policy was saved, the response carries a warning instead of an error. Members pick the policy up on their next write, and Drydock reconciles them at startup.
Audit
Creating or replacing a policy records group-policy-set, and deleting one records group-policy-cleared. The entry's container name is the exact group name, and its details are JSON with the policy id, the group, the operation (created, replaced or deleted), the revision, who made the change (user:<name> or api-key:<keyId>), the changed fields as before and after (action rules appear as actions.updateMode and actions.exclude), and the member count. The policy row and its audit row are written in one transaction, so a saved change is always audited and an audited change is always saved. Rejected requests aren't audited.
Per-container override audit entries (update-policy-override-set and update-policy-override-cleared) also carry the group layer's value for each field, next to the env and label values.
Agents
Policies live on the controller and are applied there. Containers reported by an agent get their group from the labels the agent reports, and the controller adds the group layer when it stores them. Agents don't receive policies and the policy routes aren't mounted on the agent API.