Most slow builds aren't slow because of the work they do. They're slow because a cheap layer invalidates an expensive one on every commit.
A ten-minute build for an application that compiles in ninety seconds is almost always a caching problem. Docker invalidates every layer after the first change, so a single misplaced COPY forces a full dependency reinstall on every commit.
The single highest-value fix
Copy your dependency manifests and install dependencies before copying application source. Source changes on every commit; dependencies change weekly. Ordered the wrong way round, every one-line change reinstalls the world. This one reordering routinely takes builds from minutes to seconds.
After that
- Use a build cache that persists across CI runs — ephemeral agents start cold every time otherwise
- Keep a tight .dockerignore; shipping node_modules or .git into the build context slows every step before anything executes
- Multi-stage builds so test tooling never reaches the runtime image
- Invalidate deliberately when you need to — a build arg tied to the source commit gives you a cache bust you control, rather than one that surprises you
The failure mode to watch for
Over-aggressive caching is worse than no caching: a stale layer that silently ships yesterday's application code produces a deploy that appears to succeed and changes nothing. If you have ever redeployed 'the fix' three times and watched it not take effect, this is usually why.