DEV Community

Alex Georgiev
Alex Georgiev

Posted on AI-assisted

Docker Compose's daily pull policy cuts registry checks by 80% during up

Five docker compose up cycles against the same project produced five HEAD requests to my registry when the service used pull_policy: always, one when it used pull_policy: daily. That part worked exactly as documented. The surprise came when I ran the same comparison against docker compose pull directly: the registry got hit every single time, regardless of the policy or how long I waited between calls.

Docker Compose's pull_policy field has supported time-based refresh windows — daily, weekly, and every_<duration> — since v2.34.0. The idea is straightforward: instead of choosing between "always re-check the registry" and "never re-check unless the image is missing locally", you tell Compose how stale an image is allowed to get before it bothers asking the registry again. The v5.5.0 changelog, released on 17 August 2026, adds this line: "compose pull now honors pull_policy refresh windows (daily, weekly, every_N)." I wanted to know what that was worth in practice, and whether it actually did what the changelog said.

The Docker Engine install I was working with (29.3.1) shipped the Compose plugin at v5.1.1, already older than the release that claims to fix this. So I downloaded the v5.1.1 and v5.5.1 binaries directly from GitHub and ran both against the same setup: a local registry:2 container holding a single small image, with its access log tailed so I could count exactly when a manifest got checked.

What the window actually saves under up

With a fresh project — local image removed with docker rmi -f so there was no trace of it left on the daemon — and pull_policy: daily, I ran up -d then down five times in a row, immediately after each other. Compose only checked the registry on the first cycle. The other four reused the image it already had, without a single request leaving the box:

Policy Cycles Manifest checks Image re-pulled
always 5 5 every cycle
daily 5 1 first cycle only

That's the headline number: four out of five checks avoided, on a workload that's realistic for anyone who runs docker compose up repeatedly during a working session, or as part of a CI job that spins the same stack up and down. Locally, against a registry on the same machine, the wall-clock difference across five cycles was noise — both runs took about 53 seconds, dominated by container and network setup rather than the registry call. That call is not free everywhere, though. Against Docker Hub, resolving an anonymous pull token and checking one manifest took between 0.40 and 1.01 seconds across three tries from this machine:

trial 1: token+HEAD roundtrip 1.014s
trial 2: token+HEAD roundtrip 0.431s
trial 3: token+HEAD roundtrip 0.404s
Enter fullscreen mode Exit fullscreen mode

That is the cost daily is built to avoid, and on a slow or rate-limited registry it adds up over a day of repeated up calls.

I also checked that the window genuinely expires rather than just remembering "already pulled once, never again". With pull_policy: every_3s, an immediate second up skipped the check as expected, but after sleeping six seconds a third up checked the registry again and re-pulled. The mechanism is time-based, not a permanent cache flag.

Where the changelog claim didn't hold up

The part I couldn't reproduce is the specific one the release notes highlight. I built a project with pull_policy: every_3s and ran docker compose pull — the explicit subcommand, not up — three times: cold, immediately again, and a third time after sleeping five seconds so the window had clearly elapsed. Both v5.1.1 (before the fix) and v5.5.1 (the release that claims to have shipped it) hit the registry all three times:

pull #1 (cold): 1 check
pull #2 (immediate): 1 check
pull #3 (after 5s, window elapsed): 1 check
Enter fullscreen mode Exit fullscreen mode

Same pattern with daily in place of every_3s. docker compose pull --help shows no new flag between the two versions, and docker compose up --help is identical too, so whatever changed in v5.5.0 isn't something a flag exposes. I can't tell from the outside what the fix actually touched, but on the exact scenario the changelog names — plain daily/weekly/every_N values, explicit pull, nothing else in play — I saw no difference in behaviour between the pre-fix and post-fix binary. If you were hoping the fix meant your CI pipeline's docker compose pull step would start skipping unnecessary registry calls, it doesn't, at least not in this scenario.

The tradeoff nobody puts on the label

A refresh window that skips checks also skips seeing real changes. I pushed a different image to the same tag while a daily-policied service was still inside its window, then reset the local tag back to the old image ID to rule out Compose picking up the change through the local Docker image store rather than the registry. Running up again produced zero manifest checks and started the container with the old image, silently:

manifest hits: 0
NAME="Alpine Linux"
Enter fullscreen mode Exit fullscreen mode

That's the documented tradeoff, working as intended rather than as a bug, but it's worth being explicit about: any push to that tag during the window is invisible to up until the window closes. If you deploy hotfixes by re-pushing a mutable tag, pull_policy: daily will hide them from you for up to a day.

What it refuses

The pull_policy field is validated against a fairly narrow pattern. hourly and monthly are both rejected outright:

services.app.pull_policy 'monthly' does not match pattern
'^(always|never|build|if_not_present|missing|refresh|daily|weekly|every_([0-9]+[wdhms])+)+$'
Enter fullscreen mode Exit fullscreen mode

every_3x fails the same way — the suffix after the number has to be w, d, h, m or s, nothing else. Neither rejection surprised me once I'd read the regex. What did surprise me was a sixth word sitting in that same pattern, one the docs page never names: refresh, on its own, with no number attached. I set pull_policy: refresh and ran up twice in a row. It checked the registry both times — same as always, no window at all. My guess is that daily, weekly and every_N all compile down to this same internal policy with a duration attached, and a duration of zero just means always trip it. That's a guess, not something I read in source; what I did confirm is that dropping the duration entirely is accepted by the schema and does something, and none of the three docs pages I checked mention it.

What I got wrong on the way

My first attempt at the staleness test looked like it disproved the whole feature: I pushed a new image under the same tag, ran up again inside the window, and the container came back running the new image with zero manifest checks logged. That looked like daily was somehow psychic. It wasn't. Pushing the new image from the same Docker daemon that was also running the compose project updates that daemon's local tag mapping immediately, with no registry round trip involved — docker tag and docker push together mean the local image store already knows about the new image before Compose ever looks at it. Compose was comparing local state, not talking to the registry at all, and getting the right answer for the wrong reason. Resetting the local tag back to the old image ID before the second up — so the only place the new content existed was the registry — is what actually isolated the registry-check behaviour I was trying to measure.

The same mechanism bit me a second time while writing the reproduction script below. I tagged and pushed the test image, then went straight into the five-cycle loop without removing the local image first. Every single cycle came back with zero registry checks, including the first one, which made it look like daily was broken in the opposite direction. It wasn't that either: docker tag stamps the image's LastTagTime the moment it runs, and Compose's freshness window reads that same timestamp. As far as Compose was concerned, the image had already been "checked" a second earlier, by an operation that never went near the registry. The fix was mechanical — docker rmi -f the tag before the loop, so the only honest timestamp left is the one Compose itself sets on a real pull — but it means the window isn't tracking "the last time Compose checked", it's tracking "the last time anything touched this tag locally", which is worth knowing if you also build or docker tag images under the same name your compose file uses.

Run it yourself

This needs Docker with a running daemon; no cloud account or paid registry involved.

# start a local registry and push a tiny test image
docker run -d -p 5500:5000 --name test-registry registry:2
docker pull alpine:3.20
docker tag alpine:3.20 localhost:5500/pulltest:latest
docker push localhost:5500/pulltest:latest

# tagging and pushing just now already stamped this image's local
# LastTagTime, which is what the refresh window reads -- remove the
# local tag so the first cycle below is a genuinely cold check
docker rmi -f localhost:5500/pulltest:latest

# tail the registry's access log in another terminal
docker logs -f test-registry

# a minimal project using the refresh window
cat > compose.yaml <<'EOF'
services:
  app:
    image: localhost:5500/pulltest:latest
    pull_policy: daily
    command: ["sleep", "3600"]
EOF

# five cycles: only the first should touch the registry
for i in 1 2 3 4 5; do
  docker compose up -d
  docker compose down
done
Enter fullscreen mode Exit fullscreen mode

Switch pull_policy to always and repeat to see the difference in the registry log. Swap daily for every_3s and add a sleep 6 between two up calls to see the window actually expire. And to reproduce the part that didn't work as advertised, replace the for loop with two back-to-back docker compose pull calls and watch the registry log show a check both times regardless.

What to do with this

If you're using pull_policy: daily or weekly anywhere and relying on it to make docker compose pull in a CI step cheaper, check your registry's own access logs during that step. On the evidence here, it isn't skipping anything, on v5.5.1 or on the older v5.1.1 it's supposed to have fixed. The saving is real, but it's currently only real under up. If your workflow calls pull directly, and you want fewer registry round trips, you'd need to drop the explicit pull step and let up do the pulling instead — with the tradeoff that any mutable-tag push you make will stay invisible to your stack until the window you chose runs out.

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.