Multi-stage Docker images for .NET + Angular
node_modules inside the image you deploy to production means shipping 800 MB of tooling that will never run at execution time. Multi-stage builds separate what compiles from what runs: you end up with a tiny final image containing only what the runtime strictly needs.
The principle: compile, then throw away
Dockerfile declares several FROM lines. Each FROM opens an isolated stage; only the last stage becomes the shipped image. You selectively copy the build artefacts from a build stage into a runtime stage, and everything else — SDK, sources, caches — is discarded.
Layer caching: order to avoid rebuilding everything
COPY *.csproj then dotnet restore before copying the rest, the restore only re-runs when the .csproj changes — not on every C# edit. The same logic applies on the Angular side, with package.json and npm ci before the source COPY : a code change never re-invalidates the dependency install, which cuts build times tenfold.
A tiny final image
aspnet:9.0-noble-chiseled ) strip out the shell, package manager and superfluous binaries: smaller attack surface, images often under 110 MB, and execution as a non-root user by default. To serve the Angular front end, nginx alpine plays the role of the final stage.
dist/browser folder into the nginx root and add a try_files $uri /index.html to the config for the SPA fallback .
Orchestrate locally with Compose
docker-compose.yml wires up both services and their network:
--target build ) and stage sharing, both handy for isolating a test stage in the CI pipeline.
A production image should contain only what runs. Multi-stage builds make that discipline free: the SDK stays in the build stage, never in what you deploy .