Docker Compose Build vs Pull vs --no-cache
I edited a Dockerfile, ran docker compose up -d, and the container came back up running the exact same code it had before. No error, no warning, just silence, and a service that looked started but was not the one I had just changed.

I edited a Dockerfile, ran docker compose up -d, and the container came back up running the exact same code it had before. No error, no warning — just silence, and a service that looked started but wasn't the one I'd just changed.
TL;DR
- Plain
docker compose upnever checks whether a Dockerfile changed — it reuses the locally tagged image if one already exists. docker compose up --buildrebuilds, but it still uses Docker's layer cache — only the layers from the first changedCOPYonward actually re-run.docker compose build --no-cachediscards every cached layer and rebuilds from scratch — slowest, but the only one guaranteed to reflect every change.- A rebuilt image's files can still look stale if a volume is mounted over the app directory — that's a volume problem, not a build-cache problem.
- Pull a prebuilt image when it's an official image with no local code; build only when a Dockerfile or local source actually needs to run.
KEY-STAT: 3 — Distinct things "rebuild my container" can mean in Compose — and each one rebuilds a different amount
What docker compose up actually does with your image
docker compose up looks at the image tag a service references and, if that tag already exists locally, uses it — full stop. It does not diff the Dockerfile, does not check file timestamps in the build context, and does not know or care that you just edited a COPY'd file five seconds ago. That's not a bug; a plain up is meant to start what's already built, the same way up on a pull-only service just starts the last image it pulled. The confusion comes from expecting "start" to also mean "rebuild if needed" — Compose keeps those as separate concerns on purpose.
The three commands, what each one rebuilds
| Command | What it does |
|---|---|
docker compose up | Starts existing containers/images as-is; never rebuilds, never checks if the Dockerfile changed |
docker compose up --build | Rebuilds first, then starts — but still uses the layer cache, so only layers after the first changed instruction actually re-run |
docker compose build --no-cache | Discards every cached layer and re-executes the entire Dockerfile from scratch, every time |
Why --build alone sometimes still looks stale
Docker's layer cache works top-to-bottom: each Dockerfile instruction produces a layer, and a layer is reused as long as its inputs (the instruction itself, and for COPY/ADD, the file contents) haven't changed. --build respects that cache — it doesn't disable it. So if a COPY package.json . layer near the top of the Dockerfile is unchanged but a later COPY . . layer picking up your actual code did change, only that layer and everything after it re-runs. That's usually exactly what you want — fast, correct rebuilds. It only surprises people who expect --build to mean --no-cache; it doesn't.
Note
--pull is a separate flag from both of these — it checks the registry for a newer version of the Dockerfile's FROM base image before building, independent of whether the cache is used for your own layers. Combine --no-cache --pull for the actual cleanest-possible rebuild: new base image, zero reused layers.
The volume-masking trap
A genuinely rebuilt image can still look stale for a reason that has nothing to do with the build cache: a named or anonymous volume mounted over the same path as your application code. If a compose file mounts a volume at /app and that volume already has old files in it from a previous run, the container starts from the new image but the volume's contents shadow whatever the image just built — the old files win, because volumes mount on top of the image filesystem at that path.
Warning
If a rebuild looks correct in the build log but the running container still behaves like the old code, check for a volume mounted over the code path before assuming the build cache is lying to you. docker compose down -v (removing volumes, covered in the volumes and backups guide) clears it — or docker compose up --build --renew-anon-volumes if the culprit is specifically an anonymous volume.
How long a forced rebuild actually costs
On this homelab's server, a docker compose build --no-cache on a small custom-Dockerfile service — a Python-based background worker with a handful of pip dependencies — runs somewhere around two minutes: reinstalling every OS package and every pip dependency from scratch, with nothing reused. The same rebuild with the cache intact and nothing actually changed finishes in single-digit seconds, because every layer hits cache and Docker just re-tags the existing image. That gap is the whole reason --no-cache isn't the default — for the vast majority of edits, only the last one or two layers actually need to redo work, and forcing a full from-scratch rebuild every time throws that away for no benefit.
Pull vs build: when a prebuilt image is the right call
Pull a prebuilt image — image: postgres:16 style, no build: key — whenever the service is an off-the-shelf app with no local code changes: a database engine, Jellyfin, Vaultwarden, Caddy. There's nothing to build, and pulling gets a maintainer-tested image with none of the local build-cache questions above. Reach for build: from a local Dockerfile only when the service actually runs your own code or needs a base image customized beyond what environment variables and volumes can configure — otherwise build: just adds rebuild-cache complexity to solve a problem that doesn't exist yet.
Decision guide
| Symptom | What to run |
|---|---|
| Just started a stack for the first time, images already exist locally | docker compose up -d |
| Edited a Dockerfile or code copied into the image | docker compose up --build -d |
--build ran but the container still looks like the old code, and no volume is mounted over the code path | docker compose build --no-cache then up -d |
Container looks stale even after a clean --no-cache rebuild | Check for a volume mounted over the app directory — likely down -v or --renew-anon-volumes |
| Want the newest base image too, not just your own layers | docker compose build --no-cache --pull |
| Off-the-shelf service, no local Dockerfile at all | No build: key — just image: and docker compose pull to update |
Restart behavior after a rebuild is a separate decision from any of this — see the restart policies guide — and which network a rebuilt container joins doesn't change either, covered in Docker Compose networking. Build, restart policy, and networking are three independent per-service choices.
The bottom line
docker compose up never rebuilds on its own, --build rebuilds but still trusts the layer cache, and --no-cache is the only command that guarantees a from-scratch result — at real time cost, not just a flag to add out of habit. If a rebuild still looks stale after --no-cache, stop suspecting the build and check whether a volume is mounted over the code path instead.
Frequently asked questions
Does docker compose up rebuild the image automatically?
No. docker compose up starts whatever image is already tagged locally and never checks whether the Dockerfile or build context changed. Use docker compose up --build to force a rebuild before starting.
What's the difference between docker compose up --build and docker compose build --no-cache?
--build rebuilds but still uses Docker's layer cache, so only layers after the first changed instruction actually re-run. --no-cache discards the entire cache and rebuilds every layer from scratch, which is slower but guarantees every instruction re-executes.
Why doesn't my Docker container reflect my code changes?
Most often either the image was never rebuilt (docker compose up alone doesn't rebuild) or a volume is mounted over the same path as the application code, so the volume's old contents shadow the newly built image's files at that path.
Should I use --no-cache every time I rebuild?
No — for most edits only the last layer or two actually changed, and --build alone already picks that up correctly and much faster. Reach for --no-cache specifically when you suspect the cache itself is stale or wrong, not as a routine habit.
More from Self-Hosting & Privacy

I pasted a fresh Postgres password straight into environment: on a new compose file, then stopped with my cursor over git add . That password was about to sit in plain text in a repo I push to self-hosted Gitea, not because I was careless, but because environment: is the path of least resistance.

I ran docker compose stop expecting the stack to fully disappear. A few seconds later, docker compose ps still listed every container and the default network was still there, nothing had been removed, just stopped.

The power blipped, the host rebooted, and half the homelab stack came back up on its own — the other half just sat there, exited. Every container had a restart: line; they weren't all the same line.
Stay in the loop
Get the latest articles delivered to your inbox. No spam, unsubscribe anytime.
Docker Compose Down vs Stop vs Kill
I ran docker compose stop expecting the stack to fully disappear. A few seconds later, docker compose ps still listed every container and the default network was still there, nothing had been removed, just stopped.
Continue Reading