Docker & Containers
In short: Docker is a platform to build, ship and run applications in containers.
- A container packages the app with its runtime and dependencies, so it runs identically everywhere ("works on my machine" disappears).
- Benefits: consistent environments across dev/test/prod, fast startup, high density, isolation between apps, easy scaling, and a standard artifact for CI/CD and orchestrators like Kubernetes.
In short: A VM virtualises hardware and runs a full guest OS with its own kernel on a hypervisor — strong isolation, GBs in size, minutes to boot.
- A container virtualises at the OS level: isolated processes sharing the host kernel using namespaces (isolation) and cgroups (resource limits) — MBs in size, starts in seconds, much higher density.
- Containers can run inside VMs (as in most clouds).
In short: An image is a read-only, layered template (filesystem + metadata) built from a Dockerfile.
- A container is a running (or stopped) instance of an image with its own writable layer, network and process.
- One image can start many containers, just like a class and its objects.
In short: FROM (base image), WORKDIR, COPY/ADD, RUN (build-time command), ENV/ARG, EXPOSE, USER, HEALTHCHECK, ENTRYPOINT and CMD.
- ENTRYPOINT defines the executable that always runs; CMD supplies default arguments (or a default command) that
docker runarguments override. - COPY copies local files; ADD also auto-extracts local tar archives and can download URLs — prefer COPY for clarity and security.
In short: A Dockerfile with several FROM stages: an early stage (e.g. .NET SDK) compiles and publishes the app;
- the final stage starts from a small runtime image and copies only the build output with
COPY --from=build. - Result: much smaller images without compilers or source code, faster deployments, smaller attack surface — all in one Dockerfile.
In short: Volumes are managed by Docker (stored under Docker's data directory), portable, easy to back up and the preferred way to persist container data like databases.
- Bind mounts map a specific host path into the container — great for live-editing source code in development but tied to the host's folder structure and permissions.
- Data in the container's writable layer disappears when the container is removed.
In short: A tool for defining and running multi-container applications with a YAML file (compose.yaml): services, images/builds, ports, environment, volumes, networks, dependencies and health checks.
docker compose upstarts everything.- Used for local development environments (API + SQL Server + Redis + RabbitMQ), integration tests and simple single-host deployments.
- For production clusters, use Kubernetes or Azure Container Apps.
In short: Through Docker networks.
- On a user-defined bridge network (or Compose's default network), containers resolve each other by container/service name via Docker's embedded DNS (
http://api:8080,Server=sql). - The default
bridgenetwork doesn't provide name resolution. - Published ports (
-p) are only needed for access from the host or outside, not between containers.
In short: Use multi-stage builds;
- small base images (aspnet runtime instead of SDK, chiseled/Alpine variants, runtime-deps with self-contained/AOT);
- a
.dockerignore; - combine RUN commands and remove caches in the same layer;
- don't install unnecessary tools;
- publish trimmed/AOT apps where suitable.
- Check layers with
docker historyor Dive.
In short: Never bake secrets into images (ENV, ARG, or copied files persist in layers and history).
- Supply them at runtime: environment variables from a secure source, mounted secret files (Docker/Compose secrets, Kubernetes Secrets), or fetch from Azure Key Vault using managed identity.
- For build-time secrets (private NuGet feed tokens), use BuildKit
RUN --mount=type=secret. - Keep
.envfiles out of Git.
In short: Each instruction produces a layer; Docker reuses a cached layer if the instruction and its inputs haven't changed, and invalidates all layers after the first change.
- Put rarely changing steps first: copy only
.csprojfiles and rundotnet restorebefore copying the rest of the source, so code changes don't re-download packages. - Use BuildKit cache mounts for package caches.
In short: 0 = normal exit;
- 1 = application error;
- 137 = killed with SIGKILL (128+9) — often out-of-memory (OOMKilled) or forced stop after timeout;
- 143 = terminated by SIGTERM (128+15) — normal graceful stop;
- 139 = segmentation fault.
- Check
docker inspect(State.OOMKilled) and logs.
In short: Linux namespaces isolate what a process can see — PID, network, mount, UTS (hostname), IPC, user and cgroup namespaces.
- cgroups limit and account resources (CPU, memory, I/O).
- Union/overlay filesystems provide layered images with copy-on-write.
- Security layers: capabilities, seccomp profiles, AppArmor/SELinux, user namespaces/rootless mode. containerd and runc (OCI runtime) actually create the containers.
In short: Kubernetes 1.24 removed dockershim, the adapter that let the kubelet talk to Docker Engine.
- Kubernetes now talks to CRI-compatible runtimes (containerd, CRI-O) directly.
- Images built with Docker are OCI images and run unchanged.
- It only matters if something relied on the Docker socket inside nodes (e.g. Docker-in-Docker builds) — use BuildKit/Kaniko/ACR Tasks instead.
In short: Is the port published (-p 8080:8080)?
- Which port does the app listen on — .NET 8+ images default to 8080 (
ASPNETCORE_HTTP_PORTS), older ones to 80. - Is the app bound to
0.0.0.0/+rather thanlocalhostinside the container? - Did the container crash (
docker ps -a,docker logs)? - Is HTTPS redirection forcing 443 without a certificate?
- On Windows, is another process using the port?
- Check
docker inspectfor port bindings and health status.
In short: Inspect with docker history/Dive.
- Typical fixes: switch to a multi-stage build so the SDK and source aren't in the final image;
- use
mcr.microsoft.com/dotnet/aspnet(or chiseled) as the final base; - add
.dockerignore(bin/obj/.git/node_modules); - remove build tools and caches in the same RUN layer;
- don't copy test projects;
- consider trimming/AOT.
- Target size: ~100–250 MB for a typical ASP.NET Core API.
In short: It's being killed — most often out of memory: the container exceeded its memory limit (docker inspect shows OOMKilled: true, or Kubernetes shows OOMKilled).
- Investigate memory usage (
docker stats, dotnet-counters), raise the limit appropriately, fix leaks, and make sure .NET respects container limits (it does by default; - tune
DOTNET_GCHeapHardLimitor Server/Workstation GC if needed). - It can also be a failing liveness probe or a forced stop after SIGTERM timeout.
In short: Write a compose.yaml with the API (built from its Dockerfile), SQL Server (named volume + health check), Redis and RabbitMQ (management image), a shared network, and environment variables pointing the API at service names;
- put passwords in a
.envfile excluded from Git with a.env.exampletemplate; - add a seed/migration step (EF migration bundle container or init script).
- Onboarding =
git clone && docker compose up -d. - Alternatively use .NET Aspire's AppHost to orchestrate the same dependencies.