We Cut Our Deploy Upload From 523MB to 6MB
Most of what we were uploading on every deploy was never used by the build. Finding that out took one command; the rest of the speedup came from caching the right layer in the right place.

We Cut Our Deploy Upload From 523MB to 6MB 📦
Every deploy started by uploading 523 megabytes to the build service. Not the image — the context, the tarball of our repository that gets sent before any build step runs. On a home connection that is minutes of waiting before the build even begins, every single time.
It is now about 6MB. Nothing about the application changed.
Measure before you optimize, because the answer is unintuitive
The first useful command is the one that tells you what you are actually sending:
# What would be uploaded, largest first?
tar --exclude-from=.dockerignore -cf - . 2>/dev/null | wc -c
du -sh ./* | sort -rh | head -20
# What the build service reports is authoritative — watch the context line.Ours was dominated by marketing video, high-resolution imagery, and 3D model assets — all of which the build genuinely needs in the final image, and none of which the build needs to receive as input on every deploy, because they had not changed in months.
That distinction is the whole trick. The build needs those files to exist in the output. It does not need them to arrive fresh each time.
The three changes, in order of payoff
1. Exclude what the build does not read. The obvious wins first: version control metadata, local dependency trees, previous build output, test fixtures, documentation, editor directories. .git alone is frequently the single largest entry and is almost never needed by a build.
Be precise, because an over-broad rule here is how a needed file silently disappears and produces a confusing error somewhere else entirely.
2. Carry large static assets forward instead of re-uploading them. For asset directories that change rarely and are large, the build can copy them out of the previously published image rather than receive them from your machine:
# Pull unchanged heavy assets from the last published image.
FROM registry/app:latest AS prevassets
FROM node:22-slim AS runtime
COPY --from=prevassets /app/public/video /app/public/video
COPY --from=prevassets /app/public/models /app/public/modelsThis is the change that produced most of the reduction. It has one real hazard, worth stating plainly: the previous image is now a build dependency. Deleting the :latest tag, or a cleanup policy that prunes it, breaks every subsequent build. Whatever retention rules you write must explicitly protect the tags your builds read from.
3. Cache dependency installation in the registry. Use a registry-backed cache with maximum mode so intermediate layers are cached, not only the final one:
docker buildx build \
--cache-to type=registry,ref=registry/app:buildcache,mode=max \
--cache-from type=registry,ref=registry/app:buildcache \
-t registry/app:latest --push .Combined with copying manifests before source, a code-only change reuses the installed dependency layer entirely. This is where the remaining wall-clock time went.
What we would warn about
The cache tag and the latest tag are now infrastructure. Two tags in your registry are load-bearing inputs to every build. Document that. A reasonable-looking cleanup policy that deletes untagged or old images will eventually reach them, and the failure appears as a build error with no obvious connection to a retention setting somebody changed last week.
A smaller context can hide a missing file until much later. If an exclusion removes something the build only reads on an uncommon path, the build succeeds and the artifact is subtly wrong. After trimming, verify the output, not just that the build passed — list the directories you expect in the final image and check them.
Do not optimize the wrong phase. We nearly spent a day on multi-stage layer ordering when the actual cost was upload time on a residential connection. Measure the phases separately — upload, dependency install, compile, push — and work on the largest one. The phase you are most interested in is rarely the phase that is slow.
The general lesson
Build systems make it very easy to send everything and let the build sort it out, and very easy to never notice how much "everything" has become. Nothing warns you; the context grows by a few megabytes per feature until the number is absurd.
Worth checking now, on whatever you deploy most often: what is the context size, and what fraction of it does the build actually read? If those two numbers are far apart, the fix is an afternoon and you will feel it on every deploy for the rest of the project.