🚀 Executive Summary
TL;DR: Docker’s layer caching often prevents static sites from rebuilding with `docker-compose up`, leading to stale content. The article provides solutions ranging from forcing a rebuild with `–build` to a robust multi-stage Dockerfile with named volumes for automated, production-ready deployments.
🎯 Key Takeaways
- Docker’s build process uses layer caching, which can prevent `RUN` commands like `npm run build` from re-executing if preceding `COPY . .` layers are considered unchanged, leading to stale static assets.
- The `docker-compose up –build` flag forces a rebuild of service images, bypassing the cache for local development but is not ideal for automated CI/CD pipelines.
- A robust, production-ready solution involves a multi-stage Dockerfile combined with named volumes in `docker-compose.yml` to cleanly separate the build artifact from the serving container, enabling efficient and cache-aware deployments.
- `docker system prune` is a highly destructive last-resort command to clear Docker’s entire cache, guaranteeing a fresh build but significantly slowing down subsequent operations and posing risks on shared systems.
Quick Summary: Frustrated that Docker Compose isn’t rebuilding your static site after code changes? Learn why Docker’s layer caching is the culprit and discover three practical solutions, from a quick command-line fix to a production-ready, multi-stage build pattern.
Docker Compose Won’t Rebuild Your Static Site? You’re Not Crazy, It’s Caching.
I still remember the night. 2 AM. A “simple” CSS fix for a critical landing page wasn’t showing up on the staging server, `stg-web-01`. The product manager was breathing down my neck, the frontend dev swore they pushed the change, and I was staring at a CI/CD pipeline that was green across the board. I re-ran the deployment script five, maybe six times. `docker-compose down`, `docker-compose up -d`. Nothing. For 30 agonizing minutes, I was convinced I was losing my mind. The problem? A tiny, fundamental misunderstanding of how Docker’s image builder thinks. It’s a rite of passage, I suppose, but one I’d like to help you skip.
The “Why”: Docker’s Caching Is a Feature, Not a Bug
Before we dive into the fixes, you need to understand the root cause. When Docker builds an image from a `Dockerfile`, it executes each instruction line-by-line, creating a layer for each one. If you run the build again and an instruction hasn’t changed (and the files it depends on haven’t changed), Docker uses a cached version of that layer. It’s a brilliant feature for speed.
But here’s the trap. Look at this common `Dockerfile` for a static site:
# Stage 1: Build the assets
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Stage 2: Serve the assets
FROM nginx:stable-alpine
COPY --from=builder /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Let’s say you change a single CSS file in your `src` directory. The `COPY package*.json ./` layer is cached because `package.json` didn’t change. The `RUN npm install` layer is cached. The `COPY . .` layer is also seen as cached if you’re not careful, because Compose might not signal a change in context properly. And if that layer is cached, Docker says, “Hey, no need to run the next step!” and happily skips your `RUN npm run build` command, serving you the same stale assets from the last successful build.
So, let’s fix it. Here are three ways to get your changes deployed, from the quick fix to the proper, long-term solution.
Solution 1: The Quick Fix: Force the Rebuild
This is the simplest, most direct way to tell Docker Compose, “Hey, I know what I’m doing. Ignore the cache and build it again.”
Instead of just running `docker-compose up`, you add the --build flag.
docker-compose up -d --build
This flag forces Compose to rebuild the image for the service, which will pick up your code changes and run the `npm run build` step as you expect. It’s perfect for local development when you’ve made a change and just want to see it reflected.
Pro Tip: This is a manual fix. It’s great for your local machine, but it doesn’t solve the underlying problem in an automated CI/CD pipeline where the script might just be running `docker-compose up`.
Solution 2: The ‘Right’ Way: Isolate Build and Runtime
The most robust, production-ready solution is to treat the build artifact (your static files) as separate from the runtime container. We use a multi-stage `Dockerfile` (like the one above) but manage the data with a named volume. The idea is to have one “builder” service that only runs once, places its output in a volume, and a “server” service that just reads from that volume.
Here’s how your `docker-compose.yml` would look:
version: '3.8'
services:
# This service ONLY builds the code and exits.
builder:
build:
context: .
target: builder # We only run the 'builder' stage from our Dockerfile
volumes:
- static_content:/app/build
# This service serves the files built by the 'builder'
webserver:
image: nginx:stable-alpine
ports:
- "8080:80"
volumes:
- static_content:/usr/share/nginx/html
volumes:
static_content: # Define the named volume
In this setup, your deployment process becomes:
docker-compose build builder(or just `docker-compose build`) – This rebuilds the builder image if the source code has changed.docker-compose run --rm builder– This runs the build, populating the `static_content` volume.docker-compose up -d webserver– This starts the Nginx server, which serves the fresh content from the volume.
This pattern cleanly separates the build from the run. Your `webserver` container never has to be rebuilt; it just serves whatever is in the volume. This is clean, efficient, and how we handle it for our production deployments.
Solution 3: The ‘Nuclear’ Option: Nuke the Cache
I’m including this with a heavy warning. Sometimes, you’re just stuck. It’s late, you’ve tried everything, and you suspect something is deeply wrong with your Docker cache state. This is your break-glass-in-case-of-emergency option.
The command is `docker system prune`.
# WARNING: This is destructive. Read the prompt carefully.
# It will remove all stopped containers, all dangling images, and all unused networks.
docker system prune
# The EVEN MORE NUCLEAR version:
# WARNING: This will also remove all your build cache and unused volumes!
docker system prune -a --volumes
This command forcibly removes unused Docker assets, including layers from your build cache. By running this, you guarantee that the next time you run `docker-compose up`, it will have to build everything from scratch. It *will* solve your problem, but at the cost of nuking your entire cache, which will make subsequent builds much slower.
Darian’s Warning: Never, ever run this on a shared build server or a production machine unless you are absolutely certain you know what you’re doing. You could wipe out important data stored in volumes or disrupt other projects. This is a tool for your local machine when you’re truly desperate.
Comparison of Solutions
| Solution | Pros | Cons |
| 1. The `–build` Flag | Simple, fast, easy to remember. | Manual step, doesn’t fix underlying CI/CD issues, can still be slow if the whole image rebuilds. |
| 2. Multi-Stage & Volumes | Clean separation of concerns, CI/CD friendly, very fast runtime deployments. | More complex `docker-compose.yml`, requires a slightly different deployment mindset. |
| 3. `docker system prune` | Guaranteed to work by starting from a clean slate. | HIGHLY DESTRUCTIVE. Slows down all future builds, dangerous on shared systems. A last resort. |
At the end of the day, that 2 AM incident taught me a valuable lesson: understand your tools, don’t just use them. Docker’s caching is your friend 99% of the time. For that other 1%, now you know how to give it a firm, but polite, nudge in the right direction. Go with Solution 2; your future self will thank you.
🤖 Frequently Asked Questions
âť“ Why isn’t Docker Compose rebuilding my static site after code changes?
Docker’s layer caching mechanism reuses existing layers if instructions and their dependencies haven’t changed. If the `COPY . .` instruction is cached, subsequent build commands like `npm run build` are skipped, resulting in stale static assets being served.
âť“ How do the different solutions for Docker Compose static site rebuilds compare?
The `–build` flag is a simple manual fix for local development. The multi-stage Dockerfile with named volumes offers a robust, CI/CD-friendly solution by separating build artifacts from the runtime. `docker system prune` is a destructive last resort that clears all cache but is slow and risky.
âť“ What is a common implementation pitfall when trying to rebuild static sites with Docker Compose?
A common pitfall is relying solely on `docker-compose up` without understanding Docker’s caching, which can lead to stale content. The solution is to explicitly force a rebuild with `–build` for local development or implement a multi-stage build pattern with named volumes for automated, production environments.
Leave a Reply