Migrating from Watchtower

August 1, 2026 · View on GitHub

Watchtower Status

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

Source: Bobby Borisov, "Docker Update Tool Watchtower Reaches End of Maintenance," Linuxiac, 23 December 2025. https://linuxiac.com/docker-update-tool-watchtower-reaches-end-of-maintenance/

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

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

Architecture Comparison

flowchart LR
    subgraph legacy ["Watchtower (legacy)"]
        W["Watchtower — on each host<br/>polls registry · pulls image · recreates container"]
    end

    subgraph repl ["Drydock + Portwing (replacement)"]
        direction LR
        DD["Drydock — central<br/>polls registry · decides updates · orchestrates"]
        LK["Portwing — on each host<br/>exposes Docker API via auth'd HTTP"]
        DD -- "connects inbound" --> LK
    end

Key differences:

AspectWatchtowerDrydock + Portwing
Update decisionOn each hostCentralised in Drydock
Registry pollingEach host independentlyDrydock controller
VisibilityNone (just logs)Full dashboard
Multi-hostRun one instance per hostSingle Drydock, many Portwing agents
AuthenticationNone (Docker socket direct)Token-based, TLS, rate-limited
Compose supportLimitedFull lifecycle via /_portwing/compose
Container filteringLabel com.centurylinklabs.watchtower.enableLabel dd.watch=true

Standard Mode (inbound HTTP)

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"                        # Default; change if needed
      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)

Environment variables reference:

VariableDefaultDescription
TOKENAuth token (required; use TOKEN_HASH for hash-at-rest)
TOKEN_FILEPath to file containing token
TOKEN_HASHArgon2id hash of token (from portwing hash-token)
TOKEN_HASH_FILEPath to file containing Argon2id hash
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 (outbound WebSocket)

Use this when your host is behind NAT, a firewall, or has a dynamic IP. Portwing initiates the outbound connection to the Drydock controller; no inbound control port is needed. The stable portwing/1.0 controller endpoint is production supported (Ed25519 key required). Use Drydock v1.6.0-rc.11+ with Portwing v0.9.0 for the complete controller-owned watcher/update feature path; older controllers may remain wire-compatible without it.

Drydock's edge endpoint is Ed25519-only, so generate a keypair first and register the public key with your controller:

portwing keygen -comment "my-server" > portwing_ed25519.pem
sudo chown 65532:65532 portwing_ed25519.pem && sudo chmod 0400 portwing_ed25519.pem
# 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
      - ./portwing_ed25519.pem:/run/secrets/portwing_key:ro
    environment:
      DRYDOCK_URL: "wss://your-drydock.example.com:3001"
      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 (agent dials out to /api/portwing/ws)
PRIVATE_KEY_FILERequired. Ed25519 private key (PEM PKCS#8) used to sign the hello; edge mode fails to start without it
CA_CERTCustom CA certificate for Drydock 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 Drydock controller welcome message

Edge mode requires DRYDOCK_URL and PRIVATE_KEY_FILE to be set — Drydock rejects token-only agents, so the Ed25519-signed hello is mandatory. (TOKEN applies only to standard/HTTP-mode authentication; it plays no part in the edge handshake.) A missing PRIVATE_KEY_FILE with DRYDOCK_URL set is a fatal startup error, not a fallback to Standard mode.


Label Mapping

Watchtower uses com.centurylinklabs.watchtower.* labels. Drydock uses dd.* labels. There is no automatic migration; update your compose files or container definitions.

PurposeWatchtower labelDrydock (Portwing) label
Enable monitoringcom.centurylinklabs.watchtower.enable=truedd.watch=true
Custom display name(not available)dd.display.name=My App
Custom icon(not available)dd.display.icon=docker
Include tag regex(not available)dd.tag.include=^v\d+\.\d+\.\d+$
Exclude tag regex(not available)dd.tag.exclude=latest
Tag transform(not available)dd.tag.transform=...
Group(not available)dd.group=production

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


Honest 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 coordination)First-class
SecurityNo auth on Docker socket accessToken auth, TLS, rate limiting
Automatic updatesYes (pull + recreate automatically)Controlled via Drydock UI
Update visibilityLogs onlyDashboard with history
Notification integrationsSlack, email, etc. (via Shoutrrr)Drydock UI + notification plugins
Compose supportRecreate onlyFull lifecycle (up/down/pull/ps/logs)
Exec / terminalNoYes (WebSocket)
Self-hosted requiredNo (just Docker)Yes (Drydock server)
Resource footprint~50 MB image~10 MB Portwing image + Drydock server

Migration Checklist

  1. Stop Watchtowerdocker stop watchtower && docker rm watchtower.
  2. Deploy Portwing — use the Standard or Edge compose snippet above.
  3. Update container labels — replace com.centurylinklabs.watchtower.* labels with dd.* equivalents.
  4. Add Portwing to Drydock — configure the agent endpoint or let Edge mode auto-register.
  5. Verify — check /_portwing/health returns {"status":"healthy"} and the Drydock UI shows the host and its containers.
  6. Remove Watchtower imagedocker 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.