Our Docker images got smaller once we stopped overthinking them
A big, slow Docker image is usually a sign of a few habits, not one big mistake. Here is how we get ours small and our builds quick, without anything fancy.

For a while our Docker images were quietly awful. A gigabyte and a half for an app that was a few hundred lines of code. Builds that took long enough to go make tea. Nobody set out to make them that way. It just happened, one reasonable-looking line in the Dockerfile at a time.
Fixing it was not one clever trick. It was undoing a handful of small habits. If your images feel heavier than they should, it is probably these.
Build in one place, ship from another
The biggest single win was splitting the build from the thing we actually run. You need a lot of stuff to build an app: compilers, dev tools, the full set of packages. You need almost none of it to run the finished thing.
A multi-stage build does exactly that. One stage installs everything and builds the app. The final stage starts fresh from a small base and copies over only the finished result. All the build tools, the caches, the extra packages, none of it comes along. That change alone took our images from "embarrassing" to "reasonable."
Start from a smaller base
It is easy to reach for the full, general-purpose base image out of habit. It comes with a whole operating system's worth of things you will never use. The slim and alpine variants give you the same runtime with a fraction of the weight. Unless you have a specific reason to need the big one, start small. You can always add the one thing you are missing.
Order the file so the cache actually helps
Docker builds in layers, and it reuses a layer if nothing above it changed. The mistake we kept making was copying all our code in early, before installing dependencies. That meant every tiny code change threw away the cached dependency install and did the whole slow thing again.
The fix is just order. Copy in the dependency list and install first. Copy the rest of the code after. Now changing a line of code reuses the cached dependencies, and the build that used to take minutes takes seconds. Same result, completely different feel to work with.
Do not ship things you do not need
A .dockerignore file is the quiet hero here. Without one, Docker hands your whole folder to the build, including the local dependency folder, the git history, environment files and whatever else is lying around. That makes the build slower and can even leak things you did not mean to include. List the junk in .dockerignore and it stays out.
Keep it boring
That is genuinely the whole list. Split the build from the runtime, start from a small base, order the file so the cache works, and ignore the things you do not need. No exotic tooling, no clever hacks. Our images went from a headache to something we do not think about, and the builds got fast enough that nobody wanders off to make tea anymore.
Deploys feeling slow or heavier than they should? Reach out and we will help you tidy it up.