When you're deploying to Railway (or any Docker-based platform), you'll eventually hit this: your Dockerfile runs a fresh build, but a dependency you just fixed isn't there. Docker cached the old layer. You re-deploy. Same result.
Here's what's actually happening — and two concrete fixes.
The Problem: Docker Caches Too Aggressively
Docker builds images layer by layer. Each RUN, COPY, or ADD instruction produces a layer. If nothing in that layer's inputs changed, Docker reuses the cached version.
This is great for speed. It's terrible when you need a layer to actually re-run — for example, when:
- You fixed a version or config in the same
RUNthat installed a large dependency - A
curl-installed tool (likeyt-dlp) needs to pull fresh - You're debugging and want a guaranteed clean state
Fix 1: Merge Related RUN Commands
This is the one that surprises developers most.
Don't do this:
RUN pip install yt-dlp
RUN curl -fsSL https://deno.land/install.sh | sh
Docker caches these as two separate layers. If yt-dlp is already cached, it won't re-run even if deno fails or you tweak the curl command. Worse, changes to one don't invalidate the other.
Do this instead:
RUN pip install yt-dlp \
&& curl -fsSL https://deno.land/install.sh | sh \
&& deno --version
Now they're one atomic layer. Either everything runs, or nothing does. This is especially important when tools depend on each other's side effects (env vars, PATH changes, shared files).
Rule of thumb: If two RUN commands are logically coupled — one sets up something the other uses — merge them.
Fix 2: The ARG CACHEBUST Trick
Sometimes you need to force a cache miss without changing anything else in your Dockerfile. This is where ARG CACHEBUST comes in.
Add this to your Dockerfile:
ARG CACHEBUST=1
RUN echo "Cache bust: $CACHEBUST" \
&& pip install yt-dlp \
&& curl -fsSL https://deno.land/install.sh | sh
Then pass a different value at build time:
docker build --build-arg CACHEBUST=$(date +%s) -t myapp .
Because ARG values are part of the layer's cache key, changing CACHEBUST forces Docker to invalidate that layer and everything after it.
On Railway, you can set CACHEBUST as a build variable and rotate it whenever you need a clean build — no Dockerfile changes required.
When to Use Each
| Situation | Fix |
|---|---|
| Two RUN commands that must run together | Merge them |
Cached layer has stale data (e.g. pip install cached old version) |
ARG CACHEBUST at that layer |
| Everything downstream is stale |
ARG CACHEBUST early in the file |
| Just want speed improvements | Split unrelated things into separate RUN layers |
Bonus: Validate Your Install in the Same RUN
When you merge commands, add a quick validation at the end. This makes build failures obvious instead of silent:
RUN pip install yt-dlp \
&& curl -fsSL https://deno.land/install.sh | sh \
&& yt-dlp --version \
&& deno --version
If either install silently fails (returns exit 0 but produces no binary), the --version check will catch it immediately and fail the build — not fail silently at runtime.
One More Thing: RUN Order Matters for Caching
Put COPY and dependency installation steps as early as possible, and things that change often (like your app source code) as late as possible.
# ✅ Good — deps cached separately from source
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
# ❌ Bad — source changes bust the npm ci cache every time
COPY . .
RUN npm ci
Docker layer caching is one of those things that feels magical when it works and maddening when it doesn't. Understanding the cache key mechanics — and having ARG CACHEBUST in your toolkit — turns it from a mystery into a tool you control.
Top comments (0)