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 deprecation notice and walked away, leaving thousands of homelab enthusiasts and self-hosters without a maintained solution for automatic Docker container updates.
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 drop into your stack today.
What Is Watchtower?
Watchtower is a Docker container that monitors your running containers, automatically pulls updated images when new versions become available, and restarts them in place — all without manual intervention. It’s a “set it and forget it” solution widely used by homelab enthusiasts and small teams who want their self-hosted services to stay current without constant babysitting. When the upstream project went unmaintained, it began breaking against modern Docker versions and newer SDK releases. That’s exactly the problem this fork solves.
Phase 1: Code Audit and Bug Fixes
lifecycle.go — Three Bugs, One File
The container lifecycle management code had three distinct issues. First, a nil pointer dereference panic — under certain conditions, the code attempted to dereference a pointer without first confirming it was non-nil, causing a hard crash. Second, errors returned from container stop operations were silently swallowed rather than logged or surfaced, turning production debugging into a guessing game. Third, log messages were emitted in the wrong order, making the event timeline genuinely confusing when reviewing logs after the fact. All three issues are now resolved.
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 appear as though operations completed before they actually started. A straightforward reordering of the log calls restored sensible, chronological output.
go.mod — Dependency Updates and Version Mismatch Fix
The go.mod file had roughly 30 outdated dependencies, some of them years behind their current releases. I upgraded Go itself from 1.20 to 1.22 and updated all packages accordingly. This process 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 this way causes compile failures in Go modules. I reverted both to their v1-compatible releases (ginkgo v1.16.5 and robfig/cron v1.2.0) to align with what the source code actually imports.
Phase 2: Documentation Updates
Good documentation matters, especially for a community-maintained tool. I created a CHANGELOG.md that documents every change made in the fork, giving users full transparency into what’s different from the upstream project. I also updated README.md with a prominent fork notice clearly stating this version is actively maintained, and removed the upstream “this project is no longer maintained” warning that had been left in place — because it simply no longer applies.
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 on disk and simply copied it into a scratch image. There was no path to building from source without a full local Go toolchain installed. I solved this by creating a new root Dockerfile with a proper multi-stage build that handles compilation entirely inside Docker:
# 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 the dependency updates — and cannot be regenerated through GitHub’s web editor — this flag instructs Go to resolve and update module requirements on the fly during the build, bypassing the stale checksum issue entirely without requiring a local environment.
Phase 4: Docker SDK v26 Migration
Building against a modern Docker SDK (v26+) failed immediately with undefined: types.ContainerListOptions, types.ContainerRemoveOptions, and types.ContainerStartOptions. Docker SDK v26 significantly reorganized its type system — these types were moved out of the top-level types package and into a types/container sub-package with new names.
| Old (SDK v25 and Earlier) | New (SDK v26+) |
|---|---|
types.ContainerListOptions | container.ListOptions |
types.ContainerRemoveOptions | container.RemoveOptions |
types.ContainerStartOptions | container.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 completed successfully.
Phase 5: Docker API Version Fix
After a successful build, the first test run produced: “client version 1.25 is too old. Minimum supported API version is 1.40.” The culprit was a hardcoded constant in internal/flags/flags.go that had never been updated since the project’s early days in 2017. The fix was a single-line change — easy to overlook, but critical to get right:
// Before
const DockerAPIMinVersion string = "1.25"
// After
const DockerAPIMinVersion string = "1.41"
Modern Docker daemons (version 20.10 and later) require API version 1.40 at minimum. Setting the constant to 1.41 puts the fork in a safe range that works reliably with any Docker installation from the past several years.
How to Use the Fork
This fork is designed as a drop-in replacement for upstream Watchtower — no configuration changes required. Here’s a Docker Compose example to get started:
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
Prefer to build from source? That’s now fully supported with no local Go toolchain required:
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 — Upgraded 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, eliminating the need for a pre-compiled binary
- pkg/container/client.go — Migrated three container 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 transparently
- README.md — Added fork notice, removed upstream deprecation warning
What’s Next
The fork is working and tested against current Docker releases. Planned improvements include setting up GitHub Actions for automated builds and Docker Hub image pushes, migrating the test suite from Ginkgo v1 to v2, and completing a full module path rename across all source files to finalize the fork’s independence from the upstream codebase.
If you find this useful, give the repo a star at github.com/X4Applegate/watchtower, or open an issue if you run into any problems. Watchtower is alive again — let’s keep it that way.
