A single-file cron script that keeps all your Docker Compose projects up to date.
It walks a directory tree, finds every docker-compose.yml / docker-compose.yaml,
pulls the latest images, and recreates only the services whose image actually
changed — with minimal downtime and without touching anything you stopped on
purpose.
Built for small self-hosted setups (home servers, Raspberry Pi, a single VPS) where tools like Watchtower feel like overkill.
For each discovered Compose project the script:
- Skips it if it has no external images (build-only project).
- Skips it if it is not currently running — intentionally stopped projects are never woken up.
- Runs
docker compose pull --ignore-buildable. - Compares the image ID each running container uses against the image ID its tag now points to. This catches updates reliably, including the case where two projects share the same image and the first one already pulled the new version.
- If anything changed, runs
docker compose up -d, which recreates only the outdated services (no fulldown, networks stay intact).
Afterwards it removes dangling images only — images belonging to stopped or manually managed projects are left alone.
- A failure in one project (registry outage, rate limit, broken compose file) is logged and never aborts the run for the remaining projects.
- An
flock-based lock prevents overlapping runs if cron fires while a previous run is still in progress. - Paths with spaces are handled correctly (
find -print0). set -euo pipefailguards against silent breakage everywhere else.
- Bash, GNU coreutils,
flock(present on any normal Linux) - Docker with the Compose v2 plugin (
docker compose), v2.20+ for--ignore-buildable- On older Compose, replace the flag with
--ignore-pull-failuresor remove it (projects mixingbuild:+image:will then be skipped on pull errors, but the run continues).
- On older Compose, replace the flag with
git clone https://github.com/WhyElEss/docker-compose-auto-update.git
cd docker-compose-auto-update
chmod +x update-all-docker.shRun it once manually to verify:
BASE_DIR=/home/you ./update-all-docker.sh
tail -50 ~/cron-tasks/update-all-docker.logThen schedule it, e.g. daily at 05:00:
0 5 * * * BASE_DIR=/home/you /home/you/docker-compose-auto-update/update-all-docker.shEverything is overridable via environment variables:
| Variable | Default | Purpose |
|---|---|---|
BASE_DIR |
$HOME |
Root of the tree searched for projects (max depth 5) |
LOGDIR |
$HOME/cron-tasks |
Directory for the log and lock files |
LOGFILE |
$LOGDIR/update-all-docker.log |
Log file (append-only) |
LOCKFILE |
$LOGDIR/update-all-docker.lock |
Lock file for flock |
DOCKER_BIN |
/usr/bin/docker |
Docker binary (absolute path is cron-safe) |
=== 2026-07-20 05:00:01 : start ===
--- Checking directory: /home/you/homebridge
Pulling latest images...
Image updated: homebridge/homebridge:latest
Updates detected. Recreating services in /home/you/homebridge...
Recreate complete.
--- Checking directory: /home/you/uptime-kuma
Pulling latest images...
No updates found.
Removing dangling images...
Image cleanup complete.
=== 2026-07-20 05:03:12 : end ===
- Services pinned to a fixed digest (
image: foo@sha256:...) will never update — which is exactly what pinning is for. - The script updates whatever the tag moves to; if you track
:latest, you accept upstream's release cadence. Pin major-version tags (e.g.postgres:16) for more predictable behavior. - Compose projects deeper than 5 directory levels below
BASE_DIRare not discovered (tune-maxdepthin the script if needed).