Skip to content
All posts
migrationwatchtower

Migrating from Watchtower to freshdock in 5 minutes

A practical, copy-paste migration from archived Watchtower to freshdock: translate labels, swap the service in docker-compose, and verify read-only first.

Watchtower is archived and breaks on Docker Engine 29+. Moving to freshdock is mostly a service swap. Since v1.4 freshdock reads your existing watchtower.* labels directly, so relabelling is optional. Here's the whole thing, start to finish.

1. Install freshdock

Pick whichever fits. The result is the same single binary:

cargo install freshdock
# or pull the multi-arch image
docker pull ghcr.io/turbootzz/freshdock:latest

Full options on the install page.

2. Translate your labels (or don't)

Since v1.4 freshdock reads the com.centurylinklabs.watchtower.* labels directly: enable, monitor-only, and the lifecycle hook labels all keep working, and a freshdock.* label always wins when both are present. Two things to know if you keep them: an enable=true container lands on freshdock's safe watch mode (detect and notify) until you give it an updating mode, and Watchtower's hook timeouts stay in minutes while freshdock's own are in seconds.

The native spelling, if and when you translate. The big one to internalise: freshdock is opt-in by default, so you rarely need to disable anything. Unlabelled containers are simply ignored.

Watchtowerfreshdock
com.centurylinklabs.watchtower.enable=truefreshdock.enable=true
watchtower.monitor-only=truefreshdock.mode=watch
watchtower.lifecycle.pre-update / post-updatefreshdock.lifecycle.pre-update / post-update
WATCHTOWER_SCHEDULE (one global cron)per-container freshdock.mode + freshdock.schedule
watchtower.enable=false (with global watch)just omit the labels

no-pull has no freshdock equivalent: freshdock always pulls before recreate. The depends-on label is not read either, but ordering is covered a different way, since inside a Compose project freshdock follows Compose's own depends_on and needs no extra labels for it. Unsupported labels are warned about rather than silently dropped. If you rely on either, check the comparison page before switching.

Keeping Watchtower's opt-out model

If your fleet was built the Watchtower way, where everything is updated unless a label says otherwise, you don't have to relabel dozens of services to move. Turn the model on for the daemon instead:

  freshdock:
    image: ghcr.io/turbootzz/freshdock:latest
    command: ["run"]
    environment:
      FRESHDOCK_WATCH_ALL: "true"        # every container counts as enabled
      FRESHDOCK_DEFAULT_MODE: "nightly"  # ...and updates at 04:00
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    restart: unless-stopped

Exclusions read the way you expect: freshdock.mode=off or freshdock.enable=false on a container, and a com.centurylinklabs.watchtower.enable=false label you already have keeps working as one. freshdock also skips its own container unless you label it explicitly. Leave FRESHDOCK_DEFAULT_MODE out and everything picked up this way sits on watch, which is a good first week: you get the full inventory reported without a single restart. This is opt-in itself, so the default is still the safe one.

3. Swap the service in docker-compose

Replace the Watchtower service; relabel your apps now or later. Before:

services:
  app:
    image: ghcr.io/example/app:latest
    labels:
      - "com.centurylinklabs.watchtower.enable=true"

  watchtower:
    image: containrrr/watchtower
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WATCHTOWER_SCHEDULE=0 0 4 * * *

After:

services:
  app:
    image: ghcr.io/example/app:latest
    labels:
      - "freshdock.enable=true"
      - "freshdock.mode=nightly"   # 04:00 daily

  freshdock:
    image: ghcr.io/turbootzz/freshdock:latest
    command: ["run"]
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    restart: unless-stopped

A read-only socket (:ro) is enough while everything is on watch; give freshdock a writable socket once a container is on an updating mode like nightly.

4. Verify read-only, then commit

Before you let it change anything, run the read-only check. It lists your opted-in containers and what has updates, and never pulls, stops, or recreates:

freshdock check

Happy with the table? You're done. The daemon (freshdock run) will now health-gate every update and roll back any that fail to come up.

Notifications and registries

If you used Watchtower's shoutrrr notifications, freshdock has webhook, Discord, Telegram, and SMTP backends. Declare a target with one env var (FRESHDOCK_NOTIFY_<NAME>_URL, shoutrrr-style) or in a small freshdock.toml; no config file is required. Private registry credentials (Docker Hub, GHCR, Quay, lscr) come from FRESHDOCK_REGISTRY_* env vars, no file required.

The complete label, flag, env, notification, and registry translation table (more than fits here) is the migration guide on the docs site. That's the authoritative reference; this post is just the five-minute path.