Why PreviewDrop Previews Deploy in About a Minute: Build Caching, Explained
You push a branch, and sixty seconds later a live URL is sitting in your pull request. Your teammate pushes a one-line CSS fix to the same branch, and the preview refreshes in half that time. Nobody configured a cache. Nobody wrote a Dockerfile. It just works.
The reason it works is a build cache — and it's the single most important piece of infrastructure in the whole preview pipeline. This post explains how that cache works, why it makes warm redeploys fast, and why most hand-rolled preview pipelines never get this right.
Why "just build the image" is slow
Every preview environment has the same job: take a Git branch, turn it into a running container, and hand back a URL. The naive way to do that is to build a fresh Docker image from scratch on every push.
That means, on every single deploy:
- Pull every base image (
node:20-alpine,python:3.12-slim, whatever your stack needs). - Run your dependency install from zero —
npm ci,pip install,bundle install,go mod download. - Recompile everything, even the parts that didn't change.
For a Rails app with native extensions, or a Node project with a heavy node_modules, that's minutes of work per deploy — and you pay it on every push, even when the diff is a single line.
The fix is obvious in hindsight: don't rebuild what didn't change. That's what a build cache does. It's also the thing most teams skip when they hand-roll a preview pipeline, because it's fiddly to get right and easy to get subtly wrong.
How Docker layer caching works
Docker builds images in layers, and each instruction in a Dockerfile (or an equivalent build plan) produces one layer. The key insight is that Docker caches layers by their inputs:
COPY package.json package-lock.json ./produces a layer keyed on the contents of those two files.RUN npm ciproduces a layer keyed on the previous layer plus the command.COPY . .produces a layer keyed on the entire source tree.
If the inputs to a layer haven't changed, Docker reuses the cached layer instead of rebuilding it. That's the entire mechanism. The trick is ordering your build so that the expensive, slow-changing steps (dependency install) come before the cheap, fast-changing steps (copying your source code).
A well-ordered build looks like this:
# 1. Copy only the dependency manifest first
COPY package.json package-lock.json ./
# 2. Install dependencies — cached unless the manifest changes
RUN npm ci
# 3. Copy the source — cheap, changes every push
COPY . .
# 4. Build
RUN npm run build
When you push a one-line change, only steps 3 and 4 re-run. The npm ci layer — often the slowest part — is reused as-is. That's the difference between a 30-second redeploy and a 4-minute one.
What PreviewDrop does differently
PreviewDrop builds your image for you, without a Dockerfile. It detects your stack from the repo — package.json for Node, requirements.txt or pyproject.toml for Python, Gemfile for Ruby, go.mod for Go — and generates a build plan with the cache-friendly ordering baked in.
That ordering is the part most people get wrong. The generated build plan always copies your dependency manifest first, runs the install, and then copies the source. The result is that the expensive install step is cached across every deploy to the same branch, and across branches that share the same manifest.
Here's what that means in practice. The first deploy to a branch is a cold build — every layer gets built from scratch. That's the slow path, and it's why a brand-new branch takes longer than a redeploy. But every subsequent push to that branch is a warm build: the base image is already pulled, the dependency layer is already cached, and only the application code layer rebuilds.
The generated build is deterministic, too. The same package.json, Gemfile, or requirements.txt produces the same layer hashes every time. That determinism is what makes the cache reliable — there's no timestamp or random value leaking into the layer key that would invalidate the cache for no reason.
Where the seconds actually go
A deploy spends its time in a handful of stages, and the build dominates all of them. Here's the rough breakdown of a warm redeploy:
- Shallow clone — a few seconds, and only the branch you pushed.
- Framework detection — near-instant; it reads the manifest files, not the whole tree.
- Image build — the expensive part, dominated by whatever layers actually changed.
- Container start — a few seconds to boot the process and bind the port.
- Stability poll — a short window where PreviewDrop confirms the app is responding before marking it ready.
On a warm redeploy, step 3 collapses to just the application code layer. The base image and the dependency layer come straight from the cache. That's how a small Express app goes live in about 30 seconds, and a FastAPI app in under two minutes.
Cold deploys — the first push to a new branch, or a brand-new stack — are slower, because there's no cache to lean on yet. A Rails app with native extensions takes longer than a Go binary. That's the honest trade-off, and it's worth stating plainly: the cache makes repeat deploys fast, not the first one.
Why the cache matters more than you'd think
The obvious benefit of a fast build is that your preview URL appears sooner. But the cache does more than save seconds.
It makes iteration cheap. When a reviewer leaves feedback and you push a fix, the preview redeploys in under a minute. Same URL, same password, fresh code. The loop from "feedback" to "re-check" is short enough that reviewers actually do it — instead of approving blind because re-testing takes too long.
It removes the incentive to batch. When every deploy costs four minutes, engineers start bundling unrelated changes into one big PR to avoid paying the cost repeatedly. Big PRs are harder to review. A fast cache removes that pressure — push small, push often, and the preview keeps up.
It keeps the bill flat. PreviewDrop charges flat per workspace, not per build-second, so a fast cache doesn't reduce your cost — it reduces our infrastructure load, which is what makes flat pricing sustainable at $19/month. The cache is the reason we can offer unlimited deploys on every paid plan without a metered bill.
What we sacrificed to keep it simple
No architecture post is honest without the trade-offs. The build cache lives on the worker's local disk, which means it's fast but not yet distributed. A cache hit on one worker is a miss on a second worker that hasn't built that stack before. For teams running dozens of concurrent previews, that's a real limitation — and a shared, distributed cache layer is the obvious next step.
We also chose a single build queue with an in-memory job list rather than a Redis-backed, horizontally-scaled one. That's a deliberate simplicity play: it caps us at a concurrency ceiling we're comfortable with, and it keeps the system easy to reason about. When the ceiling becomes the bottleneck, we'll move to distributed workers — and the cache strategy will need to move with it.
None of this is visible to you when you push a branch. It's the kind of infrastructure that only shows up in the one number that matters: how long until the URL is live.
The bottom line
A preview environment is only as useful as it is fast. If a reviewer has to wait four minutes for a URL, they'll skip the preview and approve the diff blind — and the whole point of the tool is lost.
The build cache is what makes PreviewDrop's previews fast enough to actually use. It reuses Docker layers across deploys, orders the build so the expensive steps cache first, and turns a one-line fix into a sub-minute redeploy. It's not glamorous, but it's the difference between a preview you click and a preview you wait for.
Want to see it for yourself? Push a branch and watch the build cache do its thing — the free tier gives you two concurrent previews across three projects, no credit card required.
See also: How PreviewDrop deploys a branch in about a minute · Preview environments under the hood · Pricing — flat, from $19/month
Want previews on your own pull requests? Start free — 2 concurrent previews, no credit card.