Skip to content
ConfigurationGroup policies

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.

Group policies are on the v1.8.0-rc.1 candidate. A group policy can set update-policy fields and can restrict what Drydock does automatically. It never grants anything, and it does not change notification routing.

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.group does not fall through to the Compose project. A container with dd.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 payments reaches every container in that group on the controller and on every agent. If two hosts need different policies, give their containers different dd.group values.
  • 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:

FieldValues
maturityModeall or mature
maturityMinAgeDaysinteger from 1 to 365
maturitySkipReleasesinteger from 0 to 100; 0 disables release counting
maturityReleaseFallbackDaysinteger from 0 to 365; 0 disables the count gate age fallback
skipTagslist of tags to skip
skipDigestslist 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.

RuleValuesEffect
actions.updateModemanual or notifyA 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.excludeTrigger referencesA 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 is auto and the container has dd.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 the group-notify-only blocker, the API rejects an update request with Group 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 manual under global notify stays notify, 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:

PriorityLayerExample
1 (highest)UI/API overridePATCH /api/v1/containers/:id/update-policy
2Container labeldd.updatePolicy.maturityMode=all
3Group policyPUT /api/v1/group-policies/:id
4Watcher environmentDD_WATCHER_LOCAL_MATURITY_MODE
5 (lowest)Built-inmaturityMode=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": []
}
RequestScopeNotes
GET /api/v1/group-policiesreadEvery policy with members: { count, agents }. agents lists the hosts, with null for the controller
GET /api/v1/group-policies/:idreadOne policy with its members, or 404
POST /api/v1/group-policiesadminBody { group, updatePolicy?, actions? }. Returns 201, or 409 when the group already has a policy
PUT /api/v1/group-policies/:idadminFull 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=NadminDeletes the policy and re-resolves its members

Things worth knowing:

  • revision guards 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 PUT that matches the stored policy returns changed: false, writes nothing, doesn't bump the revision and isn't audited.
  • The group is immutable. A PUT with a different group is a 400. Delete the policy and create one for the new group.
  • applied.members counts 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.