ConfigurationAgents

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).

The controller dials out to each agent — the agent does not need to know where the controller is. Only the controller side gets 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.sock

Controller — 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 below

That'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 varRequiredDescriptionDefault
DD_AGENT_SECRET🔴Secret token for authentication (must match Controller configuration)
DD_AGENT_SECRET_FILEPath to file containing the secret token
DD_SERVER_PORTPort to listen on3000
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)
Watcher and trigger env vars use different naming patterns. Watchers are 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)

This example uses a socket proxy for secure Docker socket access. Agents also support direct socket mount and remote TLS connections.
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=true

Controller 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 varRequiredDescriptionDefault
DD_AGENT_{name}_SECRET🔴Secret token to authenticate with the Agent
DD_AGENT_{name}_SECRET__FILEPath 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}_PORTPort of the Agent3000
DD_AGENT_{name}_CAFILECA certificate path for TLS connection
DD_AGENT_{name}_CERTFILEClient certificate path for TLS connection
DD_AGENT_{name}_KEYFILEClient key path for TLS connection
DD_AGENT_ALLOW_INSECURE_SECRETInsecure. 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.

Setting 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:3000
By default, the controller also watches its own local Docker socket. If you're running the controller as a pure aggregator with no local containers to monitor, set DD_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: 5s

Agent (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=true
Do not configure notification triggers (Slack, SMTP, etc.) on agents. Only docker and dockercompose triggers are supported in agent mode. Notification triggers must be configured on the controller.
You do not need to define 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:
    • docker and dockercompose triggers 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 with DD_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 controllerWhat it meansFix
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 HTTPConfigure 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 matchMake 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/reachableCheck 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 pathOpen the path from controller to agent, or check NAT
Repeated 401s against an agent that looks like it's runningThe agent container was started without command: --agent, so it's running as a controller and never validated the secretAdd 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.