Reviving Watchtower: How I Fixed and Forked an Abandoned Docker Project

If you’ve ever relied on Watchtower to automatically update your Docker containers, you may have noticed something alarming: the project was officially abandoned in late 2024. The maintainers posted a notice that they were no longer keeping it up, leaving thousands of self-hosters in a tough spot.

I decided to do something about it. This post documents everything I did to fork, fix, and modernize Watchtower — turning a dead project back into a working, production-ready tool. The result is X4Applegate/watchtower, an actively maintained fork you can use today.

What is Watchtower?

Watchtower is a Docker container that watches your running containers and automatically pulls updated images when they become available — then restarts them with the new image. It’s a “set it and forget it” solution for keeping self-hosted services up to date without manual intervention. It was widely used by homelab enthusiasts and small teams, but when the upstream project went unmaintained it started breaking against modern Docker versions. That’s where this fork comes in.

Phase 1: Code Audit and Bug Fixes

lifecycle.go — Three bugs, one file

The container lifecycle management code had three distinct issues. First, there was a nil pointer dereference panic — under certain conditions, the code attempted to dereference a pointer without checking if it was nil first, causing a crash. Second, errors from container stop operations were silently swallowed rather than logged or surfaced, making debugging nearly impossible. Third, log messages were emitted in the wrong order, making the timeline confusing when reading logs.

registry.go — Log ordering fix

The registry code had log statements that fired out of sequence. When Watchtower checked for image updates, the log output made it look like operations completed before they started. A simple reordering of the log calls made the output sensible again.

go.mod — Dependency updates and version mismatch fix

The go.mod file had around 30 outdated dependencies, some of them years behind. I bumped Go itself from 1.20 to 1.22 and updated all packages. This also uncovered a tricky major-version mismatch: go.mod referenced ginkgo/v2 and robfig/cron/v3, but the actual source code still used v1 import paths. Mixing major versions like this causes compile failures in Go. I reverted both to their v1-compatible versions (ginkgo v1.16.5 and robfig/cron v1.2.0) to match what the source code actually imports.

Phase 2: Documentation Updates

I created a CHANGELOG.md documenting every change made in the fork so users can see exactly what’s different from the upstream project. I also updated the README.md with a fork notice clearly stating this is actively maintained, and removed the upstream “this project is no longer maintained” warning that had been left in the README.

Phase 3: Building from Source with Docker

The original project’s Dockerfiles were designed to use pre-built binaries — they expected a compiled watchtower binary to already exist and simply copied it into a scratch image. There was no way to build from source without a full local Go toolchain. I created a new root Dockerfile with a proper multi-stage build.

# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN GOFLAGS="-mod=mod" go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux \
    go build \
    -ldflags="-X github.com/X4Applegate/watchtower/internal/meta.Version=${VERSION}" \
    -o watchtower ./cmd/watchtower/

# Final stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates tzdata
WORKDIR /
COPY --from=builder /app/watchtower .
ENTRYPOINT ["/watchtower"]

Note the GOFLAGS="-mod=mod" flag. Because the go.sum file was stale after dependency updates (and can’t be regenerated via GitHub’s web editor), this flag tells Go to resolve and update module requirements on the fly during the build, bypassing the stale go.sum issue entirely.

Phase 4: Docker SDK v26 Migration

When building against a modern Docker SDK (v26+), the build failed with undefined: types.ContainerListOptions, types.ContainerRemoveOptions, and types.ContainerStartOptions. Docker SDK v26 reorganized its type system — these types were moved from the top-level types package into a types/container sub-package and renamed.

Old (SDK v25 and earlier)New (SDK v26+)
types.ContainerListOptionscontainer.ListOptions
types.ContainerRemoveOptionscontainer.RemoveOptions
types.ContainerStartOptionscontainer.StartOptions

After adding the correct import for github.com/docker/docker/api/types/container and updating all three usages in pkg/container/client.go, the build succeeded.

Phase 5: Docker API Version Fix

After a successful build, running the container produced: “client version 1.25 is too old. Minimum supported API version is 1.40.” The culprit was a constant in internal/flags/flags.go that had never been updated from 2017-era Docker. The fix was simple but easy to miss:

// Before
const DockerAPIMinVersion string = "1.25"

// After
const DockerAPIMinVersion string = "1.41"

Modern Docker daemons (20.10+) require API version 1.40 at minimum. Setting it to 1.41 puts us in a safe range that works with any Docker installation from the last several years.

How to Use the Fork

You can use this fork as a drop-in replacement for the upstream Watchtower. Here’s a docker-compose example:

version: "3.8"
services:
  watchtower:
    image: x4applegate/watchtower:latest
    container_name: watchtower
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WATCHTOWER_CLEANUP=true
      - WATCHTOWER_POLL_INTERVAL=3600
      - TZ=America/New_York

Or build from source:

git clone https://github.com/X4Applegate/watchtower.git
cd watchtower
docker build -t x4applegate/watchtower:latest .

Summary of All Changes

  • lifecycle.go — Fixed nil pointer panic, silent error swallowing, and log ordering
  • registry.go — Fixed log statement ordering
  • go.mod — Updated Go 1.20 → 1.22, bumped ~30 packages, fixed ginkgo/cron major version mismatch, renamed module path to X4Applegate
  • Dockerfile — Created new multi-stage build from scratch with Go compiler stage
  • pkg/container/client.go — Migrated three types for Docker SDK v26 compatibility
  • internal/flags/flags.go — Updated DockerAPIMinVersion from 1.25 to 1.41
  • CHANGELOG.md — Created to document all changes
  • README.md — Added fork notice, removed upstream deprecation warning

What’s Next

The fork is working and tested. Future plans include setting up GitHub Actions for automated builds and Docker Hub image pushes, migrating the test suite from Ginkgo v1 to v2, and exploring a full module path rename across all source files.

If you find this useful, feel free to star the repo at github.com/X4Applegate/watchtower or open an issue if you run into problems. The project is alive again — let’s keep it that way.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

Secret Link