Skip to content

Migrating from Watchtower

Replace an archived Watchtower installation with Drydock + Portwing — including architecture differences, label mapping, and a step-by-step cutover.

Watchtower (containrrr/watchtower) was archived on 17 December 2025. The repository is now read-only. No future releases, bug fixes, or security updates are planned. The final release is v1.7.1 (November 2023).

Running an archived, unpatched tool with full access to the Docker socket is a security liability. This guide shows how to replace Watchtower with the Drydock ecosystem:

  • Drydock — the central container monitoring and update platform (replaces Watchtower's registry-poll and auto-update logic).
  • Portwing — the lightweight remote Docker agent that gives Drydock secure access to your host's Docker daemon.

Architecture comparison

Watchtower runs as a container on every host. It polls the registry, decides whether to pull, and recreates containers — all autonomously on that one host with no coordination or visibility.

Drydock + Portwing invert the model. Portwing runs on each host and exposes the Docker API over authenticated HTTP. The Drydock controller connects inbound to each agent, performs registry checks centrally, and orchestrates updates through its dashboard.

AspectWatchtowerDrydock + Portwing
Update decisionOn each hostCentralised in Drydock
Registry pollingEach host independentlyDrydock controller
VisibilityLogs onlyFull dashboard with history
Multi-hostOne instance per host, no coordinationSingle Drydock, many Portwing agents
AuthenticationNone (Docker socket direct)Token auth, TLS, rate limiting
Compose supportRecreate onlyFull lifecycle (up/down/pull/ps/logs)
Container exec / terminalNoYes (WebSocket)
Container filteringLabel com.centurylinklabs.watchtower.enableLabel dd.watch=true
Self-hosted server requiredNoYes (Drydock)
Image size~50 MB~10 MB (Portwing) + Drydock server

Portwing does not automatically pull and recreate containers on a schedule. That decision is made by the Drydock controller through its UI and notification pipeline — not by the agent. If you relied on Watchtower's fully autonomous update loop, you will need to configure update policies in Drydock.


Label mapping

Watchtower uses com.centurylinklabs.watchtower.* labels. Drydock uses dd.* labels. There is no automatic migration — you must update your Compose files or container definitions manually.

PurposeWatchtower labelDrydock label
Enable monitoring / updatescom.centurylinklabs.watchtower.enable=truedd.watch=true
Custom display namedd.display.name=My App
Custom icondd.display.icon=docker
Include tag regexdd.tag.include=^v\d+\.\d+\.\d+$
Exclude tag regexdd.tag.exclude=latest
Tag transform ruledd.tag.transform=...
Groupdd.group=production

Containers without dd.watch=true appear in the Drydock inventory but are not checked for updates.


Deploy Portwing

Choose the connection mode that fits your network topology. See Connection Modes for a full comparison.

Standard mode (Drydock connects inbound)

Use this when the Drydock controller can reach your host directly — no NAT.

# docker-compose.yml
services:
  portwing:
    image: ghcr.io/codeswhat/portwing:latest
    restart: unless-stopped
    # The image runs as non-root UID 65532; grant the Docker socket's group:
    #   export DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock)
    group_add:
      - "${DOCKER_SOCK_GID:?set to the GID of /var/run/docker.sock}"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /data/stacks:/data/stacks        # Compose stack files (host dir must be writable by UID 65532)
    ports:
      - "3000:3000"
    environment:
      TOKEN: "${PORTWING_TOKEN}"           # Set a strong secret
      PORT: "3000"
      BIND_ADDRESS: "0.0.0.0"
      STACKS_DIR: "/data/stacks"
      # Optional TLS (recommended for production):
      # TLS_CERT: /certs/server.crt
      # TLS_KEY: /certs/server.key
      LOG_LEVEL: "info"
      DD_POLL_INTERVAL: "300"             # Container inventory refresh (seconds)

Key environment variables for Standard mode:

VariableDefaultDescription
TOKENAuth token (required; use TOKEN_HASH for hash-at-rest)
TOKEN_FILEPath to file containing the token
TOKEN_HASHArgon2id hash of the token (from portwing hash-token)
PORT3000HTTP listen port
BIND_ADDRESS0.0.0.0HTTP bind address
TLS_CERTServer TLS certificate path
TLS_KEYServer TLS key path
STACKS_DIR/data/stacksCompose stack directory
AGENT_IDUUID v4Stable agent identifier
AGENT_NAMEhostnameHuman-readable name in Drydock UI
LOG_LEVELinfodebug, info, warn, error
DD_POLL_INTERVAL300Container inventory refresh interval (seconds)
SKIP_DF_COLLECTIONSet to true to disable disk metrics

Edge mode (Portwing dials out)

Use this when your host is behind NAT, a firewall, or has a dynamic IP. Portwing initiates the outbound WebSocket connection to Drydock; no inbound port is required.

Edge mode is production supported under the stable portwing/1.0 protocol and is covered by a real multi-agent reconnect/load soak. Use Drydock v1.6.0-rc.11+ with Portwing v0.9.0 for the full controller-owned watcher/update feature path. The endpoint is Ed25519-only: set PRIVATE_KEY_FILE (token-only connections are rejected).

# docker-compose.yml
services:
  portwing:
    image: ghcr.io/codeswhat/portwing:latest
    restart: unless-stopped
    # The image runs as non-root UID 65532; grant the Docker socket's group:
    #   export DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock)
    group_add:
      - "${DOCKER_SOCK_GID:?set to the GID of /var/run/docker.sock}"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /data/stacks:/data/stacks        # host dir must be writable by UID 65532
    environment:
      DRYDOCK_URL: "wss://your-drydock.example.com:3001"
      # Edge requires an Ed25519 key; mount it and set PRIVATE_KEY_FILE
      # (register the public key in Drydock). Token-only auth is rejected.
      PRIVATE_KEY_FILE: "/run/secrets/portwing_key"
      AGENT_NAME: "my-server"
      STACKS_DIR: "/data/stacks"
      # Optional: custom CA for self-signed Drydock controller certs
      # CA_CERT: /certs/ca.crt
      HEARTBEAT_INTERVAL: "30"
      RECONNECT_DELAY: "1"
      MAX_RECONNECT_DELAY: "60"

Additional Edge mode variables:

VariableDefaultDescription
DRYDOCK_URLWebSocket URL (wss://...) — enables Edge mode
CA_CERTCustom CA certificate for controller TLS verification
TLS_SKIP_VERIFYfalseSkip TLS verification (testing only)
HEARTBEAT_INTERVAL30Ping interval (seconds)
RECONNECT_DELAY1Initial reconnect backoff (seconds)
MAX_RECONNECT_DELAY60Maximum reconnect backoff (seconds)
WELCOME_TIMEOUT30Seconds to wait for the Drydock welcome message

Edge mode requires DRYDOCK_URL and an Ed25519 key via PRIVATE_KEY_FILE — the Drydock controller rejects token-only connections. If DRYDOCK_URL is missing, Portwing falls back to Standard mode.


Migration checklist

  1. Stop Watchtower.
docker stop watchtower && docker rm watchtower
  1. Deploy Portwing using the Standard or Edge compose snippet above. See Getting Started for the hardened sockguard configuration.

  2. Update container labels — replace com.centurylinklabs.watchtower.* labels with their dd.* equivalents from the mapping table above.

  3. Add Portwing to Drydock — configure the agent endpoint in the Drydock UI, or let Edge mode auto-register.

  4. Verify — check that /_portwing/health returns {"status":"healthy"} and the Drydock UI shows the host and its containers.

curl http://your-host:3000/_portwing/health
  1. Remove the Watchtower image.
docker rmi containrrr/watchtower

Backward-compatible auth header

If you are migrating from an existing Drydock agent (Node.js) that used X-Dd-Agent-Secret, Portwing accepts that header transparently alongside X-Portwing-Token. No client-side changes are required during a phased migration.

For production deployments, upgrade to Ed25519 per-request signing after the initial cutover. See Authentication and Security Model for details.


Honest feature comparison

FeatureWatchtower (archived)Drydock + Portwing
StatusArchived Dec 2025; no security updatesActively developed
Setup complexityLow (single container)Medium (Portwing + Drydock)
Multi-hostPoor — one instance per host, no coordinationFirst-class
SecurityNo auth on Docker socket accessToken auth, TLS, rate limiting
Automatic updatesYes — pull and recreate autonomouslyControlled via Drydock UI
Update visibilityLogs onlyDashboard with history
Notification integrationsSlack, email, etc. via ShoutrrrDrydock UI + notification plugins
Compose supportRecreate onlyFull lifecycle (up/down/pull/ps/logs)
Exec / terminalNoYes (WebSocket)
Self-hosted requiredNoYes (Drydock server)
Resource footprint~50 MB image~10 MB Portwing + Drydock server

See Configuration for the full environment-variable reference and Security Model for Portwing's default-deny socket filtering and rate-limiting details.