A while back I inherited a service whose Docker image was 1.2GB. Pulls were slow, the CI cache was useless, and the deploy step took long enough that people context-switched away and forgot about it. I got it down to about 180MB without changing a line of application code. Here's exactly what moved the needle, roughly in order of impact.

1. Multi-stage builds (the big one)

The single biggest win. The original Dockerfile built the app and shipped the entire build toolchain along with it — compilers, dev headers, the full package cache. None of that is needed at runtime.

# build stage — has all the heavy tooling

FROM node:20 AS build