DELIVERY GUIDE · UPDATED 11 AUG 2026

Docker: containers as the unit of delivery

Every service I ship is a container image — the same artifact from a developer’s laptop through staging to the production cluster. That single property eliminated an entire category of “works on my machine” incidents.

What containers actually bought us

  • Parity: the image tested in staging is byte-identical to production — configuration is the only variable, injected at runtime.
  • Local realism: docker compose brings up MySQL, RabbitMQ, and an MQTT broker in seconds; new engineers run the full platform on day one.
  • Deployment as replacement: no in-place upgrades, no drift — a deploy is new containers up, old containers drained and gone.
  • Density: right-sized .NET services pack efficiently; the vending platform’s 10+ services share nodes that once ran two VMs.

Image discipline for .NET

  • Multi-stage builds: SDK image compiles, runtime-deps image ships — final images stay under 120MB and patch faster.
  • Non-root user, read-only filesystem where possible — charger-facing services are internet-adjacent; assume hostile input.
  • Pin base image digests and rebuild weekly for CVE patches; “latest” is not a version.
  • One process per container — background workers are separate deployables, not forked processes inside an API container.

Production realities

Containers made deployment boring, and boring is the goal. But they moved complexity rather than deleting it: log collection, resource limits, and graceful shutdown became explicit engineering concerns. The one that bites .NET teams: SIGTERM handling. A charging-session service must finish in-flight OCPP message processing inside the termination grace period — we test shutdown behavior as seriously as startup.

Worked example: the production Dockerfile

Multi-stage, non-root, pinned digest — the pattern every service on the platform ships with. Final image: ~110MB:

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["Sessions.Api.csproj", "."]
RUN dotnet restore                     # cached layer while csproj is unchanged
COPY . .
RUN dotnet publish -c Release -o /app --no-restore

FROM mcr.microsoft.com/dotnet/aspnet:8.0-noble-chiseled AS final
WORKDIR /app
COPY --from=build /app .
USER app                               # never root
EXPOSE 8080
ENTRYPOINT ["dotnet", "Sessions.Api.dll"]

The chiseled base image has no shell and no package manager — a charger-facing service’s attack surface should be exactly its endpoints.

FAQ

Do containers make sense without Kubernetes?

Absolutely — containers on App Service, ECS, or even docker compose on a VM deliver parity and clean deploys. Kubernetes is one way to run containers, not the reason to use them.

How big should a .NET image be?

Under 150MB with multi-stage builds and the aspnet runtime base. If it’s 800MB, the SDK is in the final stage — the most common containerization mistake I review.

Containers for databases too?

In development, always. In production, managed services — the operational cost of self-running stateful workloads exceeds the savings for almost every team.