To force Docker Compose to rebuild an image, run docker compose up --build, or docker compose build --no-cache when a cached layer is serving you stale code. The reason "I changed the code but the container runs the old version" is the single most common Docker Compose confusion is that docker compose up alone reuses the existing image without rebuilding it. Compose builds an image once, tags it, and keeps using that tag until you explicitly tell it to rebuild. This post explains the rebuild commands, how the layer cache decides what to reuse, and how to clean up the stale images that accumulate.
up Does Not Rebuild by Default
The mental model that catches people out: docker compose up starts containers from images. If an image for a service already exists, Compose uses it as-is. It does not check whether your source changed. So this sequence runs old code:
docker compose up -d # builds image, starts container
# ... you edit app.py ...
docker compose restart # restarts the SAME image, old code
docker compose up -d # image exists, reused, still old code
To pick up the change you must rebuild:
docker compose up -d --build # rebuild image, then start
--build tells Compose to run the build step before starting, which is what most people actually mean when they type up.
build vs up --build
Two related commands:
docker compose buildbuilds (or rebuilds) images for services that have abuild:section, and stops. Nothing starts.docker compose up --buildbuilds first, then starts everything.
Use build in CI when you want to build and push without running, and up --build in local development when you want the change live immediately. There is also docker compose up --force-recreate, which is different: it recreates containers from the existing image without rebuilding. People confuse --force-recreate with --build constantly; the first replaces the running container, the second replaces the image.
The Layer Cache and Stale Layers
Even with --build, you can get stale results because of the build cache. Docker builds images layer by layer and reuses a cached layer if its inputs have not changed. The classic trap is dependency installation:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt # cached unless requirements.txt changes
COPY . .
RUN pip install -r requirements.txt # WRONG placement, but illustrative
If you bump a dependency but the cache key for the RUN pip install layer has not changed the way Docker computes it, or if you edited a file that a COPY earlier in the file already cached, the build reuses the old layer. When you suspect stale layers, bypass the cache:
docker compose build --no-cache api
--no-cache rebuilds every layer from scratch for that service. It is slower, so reach for it when something is genuinely wrong rather than habitually. A more surgical option is --pull, which forces re-fetching the base image so you are not building on a months-old cached python:3.12-slim.
Ordering Your Dockerfile to Cache Well
The cache is a feature, not just a trap. Structure the Dockerfile so expensive, rarely-changing steps come first and cheap, frequently-changing steps last:
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci # cached until dependencies change
COPY . . # invalidated on every code change, but cheap
CMD ["node", "server.js"]
Copying package.json before the rest of the source means editing application code does not invalidate the npm ci layer. Get this ordering right and you rarely need --no-cache at all.
Cleaning Up Stale Images
Every rebuild that changes a layer leaves the old image layers behind as dangling images. Over weeks of development these consume tens of gigabytes. Remove them:
docker image prune # remove dangling images
docker image prune -a # remove all images not used by a container
docker compose down --rmi local # remove images built by this compose project
docker compose down also has --volumes to drop named volumes, which matters when a stale database volume is the thing serving old data, a subtler cousin of the stale-image problem. If your "old code" is actually old data in a persisted volume, no amount of --build will fix it; you need down -v.
A Reliable Rebuild Workflow
When you are unsure what state you are in and want a clean slate:
docker compose down # stop and remove containers
docker compose build --no-cache app # rebuild the service from scratch
docker compose up -d # start fresh
For everyday development, docker compose up -d --build is enough. Reserve the full teardown for when you genuinely suspect cache or volume staleness, because it is slow.
One more practice worth adopting: rebuild images regularly even without code changes, so the base image and OS packages get the latest security patches. An image built three months ago carries three months of unpatched base-image CVEs. Rebuilding on a schedule and scanning the result in CI keeps that surface small; container scanning as part of the pipeline flags when a rebuild is overdue because the base has known vulnerabilities.
FAQ
How do I force Docker Compose to rebuild an image?
Run docker compose up --build to rebuild then start, or docker compose build to rebuild without starting. If a cached layer is serving stale code, add --no-cache to rebuild every layer from scratch.
Why does my container still run old code after editing files?
Because docker compose up reuses the existing image without rebuilding. You must pass --build so Compose runs the build step before starting. If the code is baked into the image at build time, a plain restart or up will not pick up your edits.
What is the difference between --build and --force-recreate?
--build rebuilds the image from the Dockerfile before starting. --force-recreate recreates the container from the existing image without rebuilding. Use --build when your source changed and --force-recreate when only configuration or environment changed.
How do I clean up old images left by rebuilds?
Use docker image prune for dangling layers, docker image prune -a for all unused images, or docker compose down --rmi local to remove images built by the current project. Add --volumes to down if stale data in a named volume is the real problem.