Agents
Agent Mode allows running Drydock in a distributed manner across multiple Docker hosts.
The Agent Mode allows running drydock in a distributed manner.
- Agent Node: Runs near the Docker socket (or other container sources). It performs discovery and update checks.
- Controller Node: The central instance. It manages its own local watchers AND connects to remote Agents. It aggregates containers from Agents, and handles persistence, UI, and Notifications.
Architecture
The Controller connects to one or more Agents via HTTP/HTTPS. The Agent pushes real-time updates (container changes, new versions found) to the Controller using Server-Sent Events (SSE).
DD_AGENT_{name}_HOST. The agent just needs its port reachable from the controller. (Both ends share the same secret so they trust each other.)Quickstart
A minimal two-host setup: one agent watching a remote host, one controller aggregating it.
Agent (on the remote host) — run drydock with --agent, give it a secret and a watcher:
services:
drydock-agent:
image: codeswhat/drydock
command: --agent
ports:
- "3000:3000" # must be reachable from the controller
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- DD_AGENT_SECRET=pick-a-long-random-string
- DD_WATCHER_LOCAL_SOCKET=/var/run/docker.sockController — point at the agent's host with the same secret:
services:
drydock:
image: codeswhat/drydock
ports:
- "3000:3000"
environment:
- DD_LOCAL_WATCHER=false # optional: pure aggregator, no local watcher
- DD_AGENT_REMOTE1_HOST=192.168.1.50
- DD_AGENT_REMOTE1_SECRET=pick-a-long-random-string # must match the agent's DD_AGENT_SECRET
- DD_AGENT_ALLOW_INSECURE_SECRET=true # plain-HTTP LAN only; see note belowThat's the whole connection. The {name} segment (REMOTE1) is a label you choose. See Controller Configuration for naming rules. A working connection logs Handshake successful. Received N containers. on the controller. If it doesn't connect, see Troubleshooting. The rest of this page covers the full option set.
This example talks to the agent over plain HTTP, so it sets DD_AGENT_ALLOW_INSECURE_SECRET=true. Without that flag (or TLS) the controller refuses to boot rather than send the secret in cleartext. On anything but a trusted LAN, configure TLS instead (see Controller Environment Variables).
Agent Configuration
To run drydock in Agent mode, start the application with the --agent command line flag.
Agent Environment Variables
| Env var | Required | Description | Default |
|---|---|---|---|
DD_AGENT_SECRET | 🔴 | Secret token for authentication (must match Controller configuration) | |
DD_AGENT_SECRET_FILE | ⚪ | Path to file containing the secret token | |
DD_SERVER_PORT | ⚪ | Port to listen on | 3000 |
DD_SERVER_TLS_* | ⚪ | Standard Server TLS options | |
DD_WATCHER_{name}_* | 🔴 | Watcher configuration (At least one is required) | |
DD_ACTION_DOCKER_{name}_* | ⚪ | Docker trigger for container updates (required to update containers from UI/API) | |
DD_REGISTRY_{name}_* | ⚪ | Registry configuration (For update checks) |
DD_WATCHER_{name}_{option} (no provider segment — Docker is the only watcher type). Triggers are DD_ACTION_DOCKER_{name}_{option} (provider segment required). A common mistake is writing DD_ACTION_DOCKER_PRUNE=true (missing the name) — this creates a broken trigger named "prune" instead of a trigger with PRUNE=true. Use DD_ACTION_DOCKER_{name}_PRUNE=true instead.Agent Example (Docker Compose)
services:
drydock-socket-proxy:
image: lscr.io/linuxserver/socket-proxy:latest
container_name: drydock-socket-proxy
restart: unless-stopped
labels:
- dd.watch=true
- dd.update.mode=infrastructure
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- CONTAINERS=1
- IMAGES=1
- EVENTS=1
- INFO=1
- POST=1
- DELETE=1
- NETWORKS=1
- VOLUMES=1
healthcheck:
test: wget --spider http://localhost:2375/version || exit 1
interval: 5s
timeout: 3s
retries: 3
start_period: 5s
drydock-agent:
image: codeswhat/drydock
command: --agent
depends_on:
drydock-socket-proxy:
condition: service_healthy
ports:
- "3000:3000"
environment:
# Shared secret (must match controller's DD_AGENT_{name}_SECRET)
- DD_AGENT_SECRET=mysecretkey
# Watcher — discovers containers on this host
- DD_WATCHER_LOCAL_HOST=drydock-socket-proxy
- DD_WATCHER_LOCAL_PORT=2375
# Docker trigger — allows updating containers from UI/API
- DD_ACTION_DOCKER_LOCAL_PRUNE=trueController Configuration
To connect a Controller to an Agent, use the DD_AGENT_{name}_* environment variables. The {name} segment is a label you choose to identify the agent (e.g. REMOTE1, NAS, WORKER2). It's stored lowercased by the config parser, so DD_AGENT_REMOTE1_* shows up as remote1 in logs and the UI.
Controller Environment Variables
| Env var | Required | Description | Default |
|---|---|---|---|
DD_AGENT_{name}_SECRET | 🔴 | Secret token to authenticate with the Agent | |
DD_AGENT_{name}_SECRET__FILE | ⚪ | Path to file containing the secret token. Note the double underscore, which follows the general DD_*__FILE secret-file convention. | |
DD_AGENT_{name}_HOST | 🔴 | Hostname or IP of the Agent | |
DD_AGENT_{name}_PORT | ⚪ | Port of the Agent | 3000 |
DD_AGENT_{name}_CAFILE | ⚪ | CA certificate path for TLS connection | |
DD_AGENT_{name}_CERTFILE | ⚪ | Client certificate path for TLS connection | |
DD_AGENT_{name}_KEYFILE | ⚪ | Client key path for TLS connection | |
DD_AGENT_ALLOW_INSECURE_SECRET | ⚪ | Insecure. When true, allows configuring agents with a secret over plain HTTP. Drydock will log a warning on every startup but won't refuse to boot. Only enable on trusted private networks where you accept that the agent secret is sent in cleartext. | false |
If HOST or SECRET is missing for an agent, the controller logs Skipping agent {name}: Missing host or secret and starts without that agent rather than failing to boot. So a typo'd agent name shows up as a missing agent in the UI, not a startup crash. Check the controller log for Skipping agent warnings if an agent never appears.
DD_AGENT_ALLOW_INSECURE_SECRET=true means your agent secret is transmitted over the network in cleartext on every request. An attacker with network access can capture it and impersonate the controller. Prefer HTTPS (configure DD_AGENT_{name}_CERTFILE/DD_AGENT_{name}_CAFILE) or terminate TLS at a reverse proxy. Only use this flag on isolated private networks where you explicitly accept that risk.Without this flag set, a secret configured over plain HTTP is a hard startup error — the controller refuses to boot. You'll see:
Agent remote1 is configured with a secret over insecure HTTP (http://192.168.1.50:3000). Configure HTTPS (certfile/cafile) to protect X-Dd-Agent-Secret.Fix it by configuring TLS (DD_AGENT_{name}_CAFILE/CERTFILE), terminating TLS at a reverse proxy, or — on a trusted LAN only — setting DD_AGENT_ALLOW_INSECURE_SECRET=true (which demotes the error to a one-time warning).
Controller Example (Docker Compose)
services:
drydock-controller:
image: codeswhat/drydock
environment:
- DD_AGENT_REMOTE1_HOST=192.168.1.50
- DD_AGENT_REMOTE1_SECRET=mysecretkey
- DD_AGENT_ALLOW_INSECURE_SECRET=true # trusted LAN only, use TLS in production
ports:
- 3000:3000DD_LOCAL_WATCHER=false to disable the default local watcher.Complete Multi-Host Example
This example shows a controller watching its own containers and connecting to one remote agent. The controller handles notifications (Slack, SMTP, etc.); each agent handles its own container updates.
Controller
services:
drydock:
image: codeswhat/drydock
ports:
- "3000:3000"
depends_on:
drydock-socket-proxy:
condition: service_healthy
volumes:
- /path/to/store:/store
environment:
# --- Local watcher (controller's own containers) ---
- DD_WATCHER_LOCAL_HOST=drydock-socket-proxy
- DD_WATCHER_LOCAL_PORT=2375
- DD_WATCHER_LOCAL_WATCHBYDEFAULT=true
# --- Local docker trigger (update controller's own containers) ---
- DD_ACTION_DOCKER_LOCAL_PRUNE=true
# --- Remote agent connection ---
- DD_AGENT_REMOTE1_HOST=192.168.1.50
- DD_AGENT_REMOTE1_PORT=3000
- DD_AGENT_REMOTE1_SECRET=mysecretkey
- DD_AGENT_ALLOW_INSECURE_SECRET=true # trusted LAN only, use TLS in production
# --- Notifications (run on controller only) ---
- DD_NOTIFICATION_SMTP_ALERTS_HOST=smtp.example.com
- DD_NOTIFICATION_SMTP_ALERTS_PORT=587
- DD_NOTIFICATION_SMTP_ALERTS_USER=user@example.com
- DD_NOTIFICATION_SMTP_ALERTS_PASS=password
- DD_NOTIFICATION_SMTP_ALERTS_FROM=drydock@example.com
- DD_NOTIFICATION_SMTP_ALERTS_TO=admin@example.com
drydock-socket-proxy:
image: lscr.io/linuxserver/socket-proxy:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- CONTAINERS=1
- IMAGES=1
- EVENTS=1
- INFO=1
- POST=1
- DELETE=1
- NETWORKS=1
- VOLUMES=1
healthcheck:
test: wget --spider http://localhost:2375/version || exit 1
interval: 5s
timeout: 3s
retries: 3
start_period: 5sAgent (on 192.168.1.50)
services:
drydock-socket-proxy:
image: lscr.io/linuxserver/socket-proxy:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- CONTAINERS=1
- IMAGES=1
- EVENTS=1
- INFO=1
- POST=1
- DELETE=1
- NETWORKS=1
- VOLUMES=1
healthcheck:
test: wget --spider http://localhost:2375/version || exit 1
interval: 5s
timeout: 3s
retries: 3
start_period: 5s
drydock-agent:
image: codeswhat/drydock
command: --agent
depends_on:
drydock-socket-proxy:
condition: service_healthy
ports:
- "3000:3000"
environment:
# Must match controller's DD_AGENT_REMOTE1_SECRET
- DD_AGENT_SECRET=mysecretkey
# Watcher — discovers containers on this host
- DD_WATCHER_LOCAL_HOST=drydock-socket-proxy
- DD_WATCHER_LOCAL_PORT=2375
# Docker trigger — required to update containers via UI/API
- DD_ACTION_DOCKER_LOCAL_PRUNE=truedocker and dockercompose triggers are supported in agent mode. Notification triggers must be configured on the controller.DD_ACTION_DOCKER_* on the controller for each agent. During the handshake, the controller automatically discovers the agent's triggers and creates internal proxies. The controller only needs its own DD_ACTION_DOCKER_* for updating containers on its local host.Reliability notes
Transient enumeration failures
When the Docker watcher on an agent fails to enumerate containers (e.g. a brief socket-proxy hiccup or Docker daemon restart), the agent suppresses the snapshot emission rather than broadcasting an empty container list. The controller preserves the last-known container state instead of wiping it. The next successful cron cycle restores the accurate state.
Container list payload
When the controller proxies an update to an agent, it posts to the agent's POST /api/triggers/{type}/{name} route (or …/batch for batch updates). For single-update calls on docker and dockercompose triggers it sends only { id, name } (plus an operationId when one is in flight) rather than the full container object, which prevents HTTP 413 errors where full payloads exceeded the agent's 256 KB body cap. Batch update calls (…/batch) still send the full container per item, so a very large fleet updated in one batch can still approach that cap.
Features in Agent Mode
- Watchers: Run on the Agent to discover containers.
- Registries: Configured on the Agent to check for updates.
- Triggers:
dockeranddockercomposetriggers are configured and executed on the Agent (allowing update of remote containers). The controller automatically proxies update requests to the correct agent.- Notification triggers (e.g.
smtp,discord) are configured and executed on the Controller. Notifications automatically include a[server-name]prefix identifying which server each update comes from. The controller name defaults to the detected Docker or Podman daemon host name when available, then the process hostname, and can be overridden withDD_SERVER_NAME.
Troubleshooting
Agent connections are driven entirely from the controller, so the controller log (docker logs <controller>) is where you diagnose them. The controller retries with exponential backoff (up to 60s between attempts), so a failing connection repeats in the log until the agent is healthy.
| What you see on the controller | What it means | Fix |
|---|---|---|
Connecting to agent remote1 at http://…:3000 then Handshake successful. Received N containers. | Connected and working | — |
Startup Error: Agent remote1 is configured with a secret over insecure HTTP … | A secret is set but the agent is plain HTTP | Configure TLS, or set DD_AGENT_ALLOW_INSECURE_SECRET=true on a trusted LAN |
SSE Connection failed: Request failed with status code 401. Retrying... | The secrets don't match | Make DD_AGENT_{name}_SECRET (controller) equal DD_AGENT_SECRET (agent) |
SSE Connection failed: connect ECONNREFUSED 192.168.1.50:3000. Retrying... | Agent unreachable — not running, crashed at boot, or its port isn't published/reachable | Check docker logs <agent> (see below), confirm its ports: are published, and check firewalls |
SSE Connection failed: connect ETIMEDOUT 192.168.1.50:3000. Retrying... | A firewall or NAT is blocking the path | Open the path from controller to agent, or check NAT |
Repeated 401s against an agent that looks like it's running | The agent container was started without command: --agent, so it's running as a controller and never validated the secret | Add command: --agent to the agent service |
ECONNREFUSED only tells you the agent isn't answering, not why, so check docker logs <agent>. The agent throws and exits at boot in two cases: neither DD_AGENT_SECRET nor DD_AGENT_SECRET_FILE is set, or no watcher is configured (it logs Agent mode requires at least one watcher configured.). Add the missing secret or a DD_WATCHER_* (e.g. DD_WATCHER_LOCAL_SOCKET=/var/run/docker.sock) and restart.
The agent also exposes an unauthenticated GET /health on its port: it returns 200 once at least one watcher is registered, or 503 with {"status":"unhealthy","reason":"no watchers registered"} otherwise. Use it for a compose healthcheck or an uptime probe.
Still stuck? Set DD_LOG_LEVEL=debug on the controller for the full agent-client handshake trace, and open a Q&A discussion with those lines.