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.
| Aspect | Watchtower | Drydock + Portwing |
|---|---|---|
| Update decision | On each host | Centralised in Drydock |
| Registry polling | Each host independently | Drydock controller |
| Visibility | Logs only | Full dashboard with history |
| Multi-host | One instance per host, no coordination | Single Drydock, many Portwing agents |
| Authentication | None (Docker socket direct) | Token auth, TLS, rate limiting |
| Compose support | Recreate only | Full lifecycle (up/down/pull/ps/logs) |
| Container exec / terminal | No | Yes (WebSocket) |
| Container filtering | Label com.centurylinklabs.watchtower.enable | Label dd.watch=true |
| Self-hosted server required | No | Yes (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.
| Purpose | Watchtower label | Drydock label |
|---|---|---|
| Enable monitoring / updates | com.centurylinklabs.watchtower.enable=true | dd.watch=true |
| Custom display name | — | dd.display.name=My App |
| Custom icon | — | dd.display.icon=docker |
| Include tag regex | — | dd.tag.include=^v\d+\.\d+\.\d+$ |
| Exclude tag regex | — | dd.tag.exclude=latest |
| Tag transform rule | — | dd.tag.transform=... |
| Group | — | dd.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:
| Variable | Default | Description |
|---|---|---|
TOKEN | — | Auth token (required; use TOKEN_HASH for hash-at-rest) |
TOKEN_FILE | — | Path to file containing the token |
TOKEN_HASH | — | Argon2id hash of the token (from portwing hash-token) |
PORT | 3000 | HTTP listen port |
BIND_ADDRESS | 0.0.0.0 | HTTP bind address |
TLS_CERT | — | Server TLS certificate path |
TLS_KEY | — | Server TLS key path |
STACKS_DIR | /data/stacks | Compose stack directory |
AGENT_ID | UUID v4 | Stable agent identifier |
AGENT_NAME | hostname | Human-readable name in Drydock UI |
LOG_LEVEL | info | debug, info, warn, error |
DD_POLL_INTERVAL | 300 | Container inventory refresh interval (seconds) |
SKIP_DF_COLLECTION | — | Set 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:
| Variable | Default | Description |
|---|---|---|
DRYDOCK_URL | — | WebSocket URL (wss://...) — enables Edge mode |
CA_CERT | — | Custom CA certificate for controller TLS verification |
TLS_SKIP_VERIFY | false | Skip TLS verification (testing only) |
HEARTBEAT_INTERVAL | 30 | Ping interval (seconds) |
RECONNECT_DELAY | 1 | Initial reconnect backoff (seconds) |
MAX_RECONNECT_DELAY | 60 | Maximum reconnect backoff (seconds) |
WELCOME_TIMEOUT | 30 | Seconds 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
- Stop Watchtower.
docker stop watchtower && docker rm watchtower-
Deploy Portwing using the Standard or Edge compose snippet above. See Getting Started for the hardened sockguard configuration.
-
Update container labels — replace
com.centurylinklabs.watchtower.*labels with theirdd.*equivalents from the mapping table above. -
Add Portwing to Drydock — configure the agent endpoint in the Drydock UI, or let Edge mode auto-register.
-
Verify — check that
/_portwing/healthreturns{"status":"healthy"}and the Drydock UI shows the host and its containers.
curl http://your-host:3000/_portwing/health- Remove the Watchtower image.
docker rmi containrrr/watchtowerBackward-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
| Feature | Watchtower (archived) | Drydock + Portwing |
|---|---|---|
| Status | Archived Dec 2025; no security updates | Actively developed |
| Setup complexity | Low (single container) | Medium (Portwing + Drydock) |
| Multi-host | Poor — one instance per host, no coordination | First-class |
| Security | No auth on Docker socket access | Token auth, TLS, rate limiting |
| Automatic updates | Yes — pull and recreate autonomously | Controlled via Drydock UI |
| Update visibility | Logs only | Dashboard with history |
| Notification integrations | Slack, email, etc. via Shoutrrr | Drydock UI + notification plugins |
| Compose support | Recreate only | Full lifecycle (up/down/pull/ps/logs) |
| Exec / terminal | No | Yes (WebSocket) |
| Self-hosted required | No | Yes (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.