Skip to content
Search lessons, topics, tests…
Esc

    ↑ ↓ moveEnter openEsc close

    Guided course · Testing and Production Engineering

    Docker for .NET Developers: An Interview Guide: back to the course

    Module 1 · Container Foundations · Lesson 1 of 20

    Images vs Containers: How Docker Really Works for .NET Developers

    Learning outcome

    You can explain the difference between an image, a container, a registry and the engine without hand-waving. You know what survives docker stop, docker start and docker rm, why "containers are ephemeral" is only half true, and why a container lives exactly as long as its main process. You can show it all with an ASP.NET Core API on .NET 10 and explain how containers differ from virtual machines.

    Intuition

    An image is a compiled, versioned package: read-only files plus metadata that says how to start the app. It works like a NuGet package for a whole runtime environment. A container is one running instance of that package. Each instance gets its own process, its own thin writable scratch layer, its own network identity and its own lifecycle. It works like a process started from an executable: you can start ten processes from one .exe, and deleting a process does not delete the .exe.

    Keep this chain in your head for the rest of the course:

    flowchart LR
        DF["Dockerfile (recipe)"] -->|docker build| IMG["Image (read-only layers + config)"]
        IMG -->|docker push / pull| REG["Registry (Docker Hub, ACR, GHCR)"]
        IMG -->|docker run| C1["Container 1: process + writable layer"]
        IMG -->|docker run| C2["Container 2: process + writable layer"]
        VOL["Volume (outlives containers)"] -.mounted into.-> C1

    Deep dive

    The moving parts

    TermWhat it is.NET developer analogy
    DockerfileText recipe that builds an imageA build script
    ImageRead-only stack of filesystem layers plus config (entrypoint, env, user, ports)A versioned deployment package
    ContainerA process (or process tree) started from an image, isolated by the kernel, plus a writable layerA running process with its own private filesystem view
    RegistryA server that stores and serves imagesA NuGet feed for images
    TagA movable human-readable label such as 1.4.2 or latestA version string, but one that can be repointed
    Digestsha256:... content hash of an image manifest; immutableA content hash; the real identity
    Engine (daemon)The service that builds images and runs containers (dockerd, containerd, runc underneath)The runtime host
    CLIThe docker client that talks to the engine over an APIA client SDK

    The CLI is only a client. If the engine is not running, docker ps fails with "Cannot connect to the Docker daemon" even though the docker executable exists. On Windows and macOS, Docker Desktop runs the engine inside a small Linux VM (WSL 2 on Windows). Your Linux containers therefore always see a Linux kernel, even when you type commands in PowerShell.

    What isolation actually is

    A Linux container is not a tiny VM. It is an ordinary process on the host kernel, fenced in by kernel features:

    • Namespaces give the process its own view of process IDs (it sees itself as PID 1), network interfaces, hostname, mount points and users.
    • cgroups limit and account CPU, memory and I/O (--memory=512m, --cpus=1.5).
    • A union filesystem (overlay2) stacks the image's read-only layers and puts a writable layer on top.
    • Capabilities, seccomp and AppArmor/SELinux profiles reduce what even root inside the container may do.
    ContainerVirtual machine
    KernelShares the host kernelRuns its own guest kernel
    Starts inMilliseconds to secondsTens of seconds
    SizeMBs to hundreds of MBsGBs
    Isolation boundaryKernel features (weaker, but usually enough)Hypervisor (stronger)
    Runs a different OS kernelNo: Linux containers need a Linux kernelYes

    That shared kernel is the reason a Windows Server container cannot run on a Linux host, and it is the reason containers are cheap enough to run one per process.

    The lifecycle and what survives it

    CommandEffect on the containerWritable-layer files
    docker runcreate + start a new containernew, empty writable layer
    docker stopsends SIGTERM to PID 1, waits 10 s, then SIGKILLkept
    docker startstarts the same container againstill there
    docker rm (-f)deletes the containergone
    new image versiondoes nothing to existing containersyou recreate the container from the new image

    "Containers are ephemeral" really means you should be able to delete and recreate a container at any time without losing anything that matters. Stopping a container keeps its writable-layer files, but process memory and tmpfs data do not survive a stop/start. Removing it loses everything that was not in a volume, a bind mount or an external store. Kubernetes, Azure Container Apps and every CI system remove and recreate containers all the time, so design as if every container may be removed at any moment.

    PID 1 decides the container's life

    The container runs the image's ENTRYPOINT/CMD as its main process, PID 1 inside its PID namespace. When that process exits, the container stops, and its exit code becomes the container's exit code. Docker is not "crashing your app". If an ASP.NET Core app throws during startup (bad connection string, missing configuration, port already in use), the process exits, the container stops, and docker logs holds the stack trace. PID 1 also receives the stop signal, which is why the ENTRYPOINT form matters (Lesson 3).

    Code example: one image, two containers, one writable layer each

    The image shop-api:1.0 is the small ASP.NET Core API used throughout this course. It has / (who am I), /files (write and read a file), health endpoints, and Redis and PostgreSQL endpoints that we use later. Its Dockerfile is built in Lesson 3. The session below is a real terminal transcript, captured on Docker Engine 26.1 with the official .NET 10 images.

    Text
    $ docker image ls shop-api
    REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
    shop-api     1.0       a2cbf40904c7   49 seconds ago   234MB
    $ docker run -d --name shop1 -p 8081:8080 shop-api:1.0 >/dev/null; echo started shop1
    started shop1
    $ docker run -d --name shop2 -p 8082:8080 shop-api:1.0 >/dev/null; echo started shop2
    started shop2
    $ docker ps --filter name=shop --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'
    NAMES     IMAGE          STATUS                  PORTS
    shop2     shop-api:1.0   Up Less than a second   0.0.0.0:8082->8080/tcp, :::8082->8080/tcp
    shop1     shop-api:1.0   Up Less than a second   0.0.0.0:8081->8080/tcp, :::8081->8080/tcp
    $ curl -s localhost:8081/
    {"service":"Shop.Api","version":"dev","environment":"Production","machine":"9107afe21aa0","uid":"1654","framework":".NET 10.0.13","os":"Ubuntu 24.04.5 LTS"}
    $ curl -s localhost:8082/
    {"service":"Shop.Api","version":"dev","environment":"Production","machine":"18b868e27562","uid":"1654","framework":".NET 10.0.13","os":"Ubuntu 24.04.5 LTS"}
    
    # Write a file inside shop1's writable layer, then stop and start the SAME container
    $ curl -s -X POST 'localhost:8081/files?path=/tmp/cart.txt&text=3-items'
    {"written":"/tmp/cart.txt"}
    $ docker diff shop1
    C /tmp
    A /tmp/cart.txt
    A /tmp/clr-debug-pipe-1-2330053-in
    A /tmp/clr-debug-pipe-1-2330053-out
    A /tmp/dotnet-diagnostic-1-2330053-socket
    $ docker stop shop1 >/dev/null; echo stopped
    stopped
    $ docker start shop1 >/dev/null; echo started
    started
    $ curl -s 'localhost:8081/files?path=/tmp/cart.txt'
    3-items
    
    # Remove the container and create a new one from the same image
    $ docker rm -f shop1 >/dev/null; echo removed shop1
    removed shop1
    $ docker run -d --name shop1 -p 8081:8080 shop-api:1.0 >/dev/null; echo started a NEW shop1
    started a NEW shop1
    $ curl -s 'localhost:8081/files?path=/tmp/cart.txt'
    {"missing":"/tmp/cart.txt"}
    
    # The container lives as long as its main process (PID 1)
    $ docker run --name crash -e 'ConnectionStrings__Postgres=Host=db;Port=abc' shop-api:1.0 2>&1 | head -2
    Unhandled exception. System.ArgumentException: Couldn't set port (Parameter 'port')
     ---> System.FormatException: The input string 'abc' was not in a correct format.
    $ docker inspect crash --format 'status={{.State.Status}} exit={{.State.ExitCode}}'
    status=exited exit=139
    $ docker top shop2 -o pid,user,args
    PID                 USER                COMMAND
    961840              1654                dotnet Shop.Api.dll

    What to notice:

    • One image, two containers. Both answer from the same 234 MB image, but each has its own hostname (the container ID), its own published port and its own process. The app runs as UID 1654, the non-root app user that the Dockerfile selects.
    • docker diff lists the writable layer: A = added, C = changed. Next to our cart.txt you see the .NET runtime's diagnostic pipes in /tmp. Those are the sockets dotnet-counters and dotnet-trace use, which is why /tmp must stay writable in a hardened container (Lesson 7).
    • Stop/start kept the file. It is the same container with the same writable layer.
    • Remove + run lost it. A new container gets a fresh writable layer. That is the moment data disappears in real incidents, for example after a redeploy.
    • The crash. An invalid connection string makes NpgsqlDataSource.Create throw at startup. The process dies, the container is exited, and the logs hold the reason. The exit code (139 here, which is not the 134 you might expect from an abort; Lesson 15 explains why) only tells you the process died abnormally. Read the logs for the cause, not the number.
    • docker top shows the host's view: a single dotnet Shop.Api.dll process owned by UID 1654. Inside the container that same process is PID 1.

    Failure modes

    • Treating a container like a server. Patching packages or editing config inside a running container with docker exec works until the next recreate. Change the image or the configuration, then recreate.
    • Data in the writable layer. Uploads, SQLite files, Data Protection keys or a database written inside the container vanish on docker rm. Use a volume or an external store (Lesson 10).
    • "Updating the image" and expecting running containers to change. Containers keep the image they were created from. Pull, then recreate.
    • Using latest as a version. The tag moves; two machines can run different code under the same name. Use explicit tags and record digests (Lesson 17).
    • Assuming a VM-level security boundary. Containers share the kernel. Do not run untrusted code as root in a container and call it sandboxed (Lesson 7).
    • Reading exit codes as diagnoses. 137 usually means SIGKILL (often the OOM killer or a stop timeout). Anything else, read the logs first.

    Interview angle

    "What is the difference between an image and a container?" A strong answer is short and precise: "An image is an immutable, layered filesystem plus metadata such as the entrypoint, user and environment, identified by a digest and distributed through registries. A container is a running instance of an image: an isolated process on the host kernel with its own writable layer, network namespace and lifecycle. One image can back many containers. Files written in a container survive stop and start but disappear when the container is removed, so state goes in volumes or external stores." Then add one practical consequence: you deploy by building a new image and replacing containers, never by patching running ones.

    Expect the follow-up "How is a container different from a VM?" Answer: shared kernel versus guest kernel, namespaces and cgroups versus a hypervisor, and the trade-off of speed and density against isolation strength.

    Practice questions

    1. You stop a container, start it again, and a file written earlier is still there. A colleague says containers are ephemeral. Who is right?

    Answer: Both. Stop/start keeps the same container and its writable layer. The file is lost when the container is removed, for example on every redeploy. Ephemeral means "must be safe to remove and recreate".

    1. You pushed a fixed image under the same tag. Why do the running containers still show the bug?

    Answer: Containers keep the image they were created from. You must pull the new image and recreate the containers. Orchestrators do this as a rolling update.

    1. A container exits immediately after docker run. What do you check first?

    Answer: docker ps -a for the exit code and docker logs <name> for the startup exception. The main process exited, so the container stopped.

    1. Why can a Linux container start in under a second when a VM takes much longer?

    Answer: It is a process on an already running kernel, isolated by namespaces and cgroups. There is no guest OS to boot.

    1. What is the difference between a tag and a digest?

    Answer: A tag is a mutable label that can be moved to another image. A digest is the content hash of the manifest and always identifies the same bytes.

    Revision checklist

    • Dockerfile -> image -> container; registry stores images; the engine runs containers; the CLI is a client.
    • Container = process + namespaces + cgroups + writable layer on read-only image layers.
    • Stop/start keeps the writable layer; rm deletes it; volumes outlive containers.
    • PID 1 exit = container exit; read docker logs for the cause.
    • Containers share the host kernel; VMs do not.

    Analogy

    Everyday picture

    The recipe, the boxed meal and the plate

    The Dockerfile is a recipe. The image is a sealed, labelled boxed meal made from it: every box with the same label hash is identical. A container is one plate served from that box. You can serve many plates, each diner can add salt to their own plate (the writable layer), and clearing one plate does nothing to the box. If you want everyone to get a better meal, you change the recipe and box a new batch. You do not walk around salting plates.

    Quick reference

    Lifecycle

    • docker run -d --name api -p 8080:8080 img:tag: create + start.
    • docker ps -a: running and stopped containers with exit status.
    • docker logs -f api: stdout/stderr of PID 1.
    • docker stop api / docker start api: writable layer kept.
    • docker rm -f api: writable layer deleted.
    • docker diff api: what changed in the writable layer.
    • docker top api: processes, as seen by the host.
    Sign in to mark lessons done and keep your place in the course.Sign in