Why "Just Add a Dockerfile" Goes Wrong
The first Dockerfile most developers write looks like this:
This works. It also produces a 1.8 GB image, includes your entire source history via , potentially bundles files with secrets, runs as root, and rebuilds all dependencies every time any file changes.
Let's do better.
Multi-Stage Builds: The Core Pattern
Multi-stage builds separate the build environment (where you compile/install) from the runtime environment (where you run). The final image only contains what's needed to execute:
Result: 280 MB image instead of 1.8 GB. No build tools in the runtime. Non-root user.
Optimising for Build Cache
Docker layer caching means a layer only rebuilds if that layer or a layer above it changes. The most common mistake: copying all source code before installing dependencies.
For a FastAPI app with 60 dependencies, this difference is 90 seconds vs. 3 seconds on most CI runners.
Next.js Multi-Stage Build
Next.js adds complexity because is enormous and the build output is separate from the source:
This requires in your . The standalone output includes a minimal Node.js server and all necessary files — the final image is typically 200–300 MB instead of 1.5+ GB.
Secrets: What Never Goes in a Dockerfile
Never use or for secrets in a Dockerfile. These values are permanently stored in the image layer history and can be extracted with .
The right approaches:
Runtime environment variables (for non-build-time secrets):
Docker secrets (for sensitive files in Swarm/Compose):
BuildKit secret mounts (for build-time secrets, e.g., private PyPI):
The secret is available only during that RUN step and is never written to any layer.
.dockerignore Is Not Optional
Without , sends your entire git history, all node_modules, and any files to the Docker build context. For a typical Next.js project, this can mean sending 500 MB to the Docker daemon just to start the build.
The optimised Dockerfile reduces our FastAPI image from 1.8 GB to 280 MB, our Next.js image from 1.5 GB to 245 MB, and build time from 4 minutes to 45 seconds (with warm cache).