Watchtower Fork: Fixing Abandoned Docker Auto-Update Tool

{"time":1776378046152,"version":"2.28.2","blocks":[{"type":"paragraph","data":{"text":"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."}},{"type":"paragraph","data":{"text":"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."}},{"type":"header","data":{"text":"What Is Watchtower?","level":2}},{"type":"paragraph","data":{"text":"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."}},{"type":"header","data":{"text":"Phase 1: Code Audit and Bug Fixes","level":2}},{"type":"header","data":{"text":"lifecycle.go — Three Bugs, One File","level":3}},{"type":"paragraph","data":{"text":"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."}},{"type":"header","data":{"text":"registry.go — Log Ordering Fix","level":3}},{"type":"paragraph","data":{"text":"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."}},{"type":"header","data":{"text":"go.mod — Dependency Updates and Version Mismatch Fix","level":3}},{"type":"paragraph","data":{"text":"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."}},{"type":"header","data":{"text":"Phase 2: Documentation Updates","level":2}},{"type":"paragraph","data":{"text":"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."}},{"type":"header","data":{"text":"Phase 3: Building from Source with Docker","level":2}},{"type":"paragraph","data":{"text":"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:"}},{"type":"code","data":{"code":"# Build stage\nFROM golang:1.22-alpine AS builder\nWORKDIR /app\nCOPY go.mod go.sum ./\nRUN GOFLAGS=\"-mod=mod\" go mod download\nCOPY . .\nRUN CGO_ENABLED=0 GOOS=linux \\\n go build \\\n -ldflags=\"-X github.com/X4Applegate/watchtower/internal/meta.Version=${VERSION}\" \\\n -o watchtower ./cmd/watchtower/\n\n# Final stage\nFROM alpine:latest\nRUN apk --no-cache add ca-certificates tzdata\nWORKDIR /\nCOPY --from=builder /app/watchtower .\nENTRYPOINT [\"/watchtower\"]"}},{"type":"paragraph","data":{"text":"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."}},{"type":"header","data":{"text":"Phase 4: Docker SDK v26 Migration","level":2}},{"type":"paragraph","data":{"text":"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."}},{"type":"table","data":{"withHeadings":true,"content":[["Old (SDK v25 and Earlier)","New (SDK v26+)"],["types.ContainerListOptions","container.ListOptions"],["types.ContainerRemoveOptions","container.RemoveOptions"],["types.ContainerStartOptions","container.StartOptions"]]}},{"type":"paragraph","data":{"text":"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."}},{"type":"header","data":{"text":"Phase 5: Docker API Version Fix","level":2}},{"type":"paragraph","data":{"text":"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:"}},{"type":"code","data":{"code":"// Before\nconst DockerAPIMinVersion string = \"1.25\"\n\n// After\nconst DockerAPIMinVersion string = \"1.41\""}},{"type":"paragraph","data":{"text":"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."}},{"type":"header","data":{"text":"How to Use the Fork","level":2}},{"type":"paragraph","data":{"text":"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:"}},{"type":"code","data":{"code":"version: \"3.8\"\nservices:\n watchtower:\n image: x4applegate/watchtower:latest\n container_name: watchtower\n restart: unless-stopped\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock\n environment:\n - WATCHTOWER_CLEANUP=true\n - WATCHTOWER_POLL_INTERVAL=3600\n - TZ=America/Los_Angeles"}},{"type":"image","data":{"file":{"url":"/uploads/40ea486e94ada6c386f6.png"},"caption":"","withBorder":false,"stretched":false,"withBackground":false}},{"type":"image","data":{"file":{"url":"/uploads/29c9d705ae8c9859c255.png"},"caption":"","withBorder":false,"stretched":false,"withBackground":false}},{"type":"paragraph","data":{"text":"Prefer to build from source? That's now fully supported with no local Go toolchain required:"}},{"type":"code","data":{"code":"git clone https://github.com/X4Applegate/watchtower.git\ncd watchtower\ndocker build -t x4applegate/watchtower:latest ."}},{"type":"header","data":{"text":"Summary of All Changes","level":2}},{"type":"list","data":{"style":"unordered","items":["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"]}},{"type":"header","data":{"text":"What's Next","level":2}},{"type":"paragraph","data":{"text":"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."}},{"type":"paragraph","data":{"text":"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."}},{"type":"header","data":{"text":"Update: v1.0.1 Released — April 6, 2026","level":2}},{"type":"paragraph","data":{"text":"Since the initial fork, I've continued working on the project and have now shipped v1.0.1 — the first proper release of the fork. Here's a summary of everything that changed."}},{"type":"header","data":{"text":"Dockerfile Fixes","level":3}},{"type":"paragraph","data":{"text":"All four Dockerfiles had alpine:3.19.0 pinned to a December 2023 patch release. I bumped all of them to alpine:3.21 (current stable) to pull in up-to-date CA certificates and tzdata. The two legacy Dockerfiles in dockerfiles/ also had deeper issues: Dockerfile.dev-self-contained had the wrong module path in the ldflags (still pointing to containrrr/watchtower instead of X4Applegate/watchtower), meaning the version string was silently never injected into builds. The golang:alpine base was also completely unpinned, which would break on any future Go release. Both files now pin golang:1.22-alpine, use the correct module path, drop the dead GO111MODULE=on flag (a no-op since Go 1.17), and add a git describe fallback for untagged builds. Dockerfile.self-contained had an even more critical bug: it was cloning the abandoned upstream containrrr/watchtower repo instead of this fork."}},{"type":"header","data":{"text":"Docker Compose and Environment Variable Cleanup","level":3}},{"type":"paragraph","data":{"text":"The docker-compose.yml was simplified significantly. Watchtower has no web GUI — port 8080 only exists for an optional HTTP API (metrics and update trigger) that most homelab users don't need. I removed the ports: section entirely so nothing is exposed. The image now points to applegater/watchtower:v1.0.1 and all config is loaded from a .env file via env_file: .env."}},{"type":"paragraph","data":{"text":"A companion example.env file was added as a reference template. The HTTP API variables (WATCHTOWER_HTTP_API_METRICS, WATCHTOWER_HTTP_API_UPDATE, WATCHTOWER_HTTP_API_TOKEN) were removed since they only matter when a port is exposed. The file now focuses on the core use case: polling interval, cleanup, log level, optional notifications, and behaviour flags."}},{"type":"header","data":{"text":"README and Documentation","level":3}},{"type":"paragraph","data":{"text":"The README was updated with a new Running with Docker Compose section and a full Environment Variables reference table covering every WATCHTOWER_* variable. A Changelog section was also added directly to the README so the most recent changes are visible without having to dig into CHANGELOG.md."}},{"type":"header","data":{"text":"Getting v1.0.1","level":3}},{"type":"paragraph","data":{"text":"The easiest way to deploy is to grab the two config files from the v1.0.1 release page and run:"}},{"type":"code","data":{"code":"cp example.env .env\n# edit .env as needed\ndocker compose up -d"}},{"type":"paragraph","data":{"text":"Or build from source directly on your server:"}},{"type":"code","data":{"code":"git pull\ndocker build --build-arg VERSION=v1.0.1 -t applegater/watchtower:v1.0.1 .\ndocker compose up -d"}}]}