A bloated Docker image is a security risk and a slow deployment. Multi-stage builds let you use a full build environment — compilers, node_modules, test runners — without shipping any of it to production.
Structure your Dockerfile with a 'builder' stage and a 'runtime' stage. The builder stage installs all dependencies and compiles assets. The runtime stage starts from a slim base image and copies only the compiled output.
For Python applications, the builder stage installs packages into a virtual environment. The runtime stage copies that venv directory and sets PATH — no pip, no gcc, no build artifacts in the final image.
Order your COPY instructions to maximise cache hits. Copy requirements.txt and run pip install before copying application code. This way, the expensive dependency layer is only rebuilt when requirements change, not on every code change.
Always run the final container as a non-root user. Add a RUN useradd instruction and switch with USER before the CMD. This limits the blast radius if a vulnerability in your application is exploited.
Use .dockerignore aggressively. Exclude .git, __pycache__, .env files, test directories, and local virtual environments. Smaller build contexts mean faster docker build invocations.