Skip to main content

Docker Fundamentals

Volume 3 — Modern Development

Docker packages an application with everything it needs to run — runtime, libraries, config — into a single portable unit that behaves identically on your laptop, in CI, and in production. This chapter covers the mental model and the commands you'll actually use day to day.

1. Containers vs. Virtual Machines

A VM virtualizes hardware and runs a full guest operating system per instance — heavy, slow to start (minutes). A container shares the host machine's kernel and only packages the application and its libraries — light, starts in seconds. This is why you can run a dozen containers on a laptop that would struggle to run three or four full VMs.

2. Images vs. Containers

  • Image — a read-only template: your application, its dependencies, and instructions for how to run it. Built once, stored, reused.
  • Container — a running (or stopped) instance of an image, with its own writable layer on top.

The relationship is the same as a class and an object in code: one image, many containers can run from it simultaneously, each independent.

docker images # list images you have locally
docker ps # list running containers
docker ps -a # list all containers, including stopped ones

3. Writing a Dockerfile

A Dockerfile is the recipe for building an image. Here's a realistic multi-stage build for a .NET API — multi-stage means the final image only contains the compiled output, not the entire SDK used to build it:

# Stage 1: build
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

COPY *.csproj .
RUN dotnet restore

COPY . .
RUN dotnet publish -c Release -o /app/publish

# Stage 2: runtime — much smaller, no SDK included
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
COPY --from=build /app/publish .

EXPOSE 8080
ENTRYPOINT ["dotnet", "RoundTripAPI.dll"]
InstructionPurpose
FROMThe base image to start from
WORKDIRSets the working directory inside the image for subsequent instructions
COPYCopies files from your machine into the image
RUNExecutes a command during the build (installing packages, compiling)
EXPOSEDocuments which port the container listens on — informational, doesn't actually publish it
ENTRYPOINT / CMDWhat runs when a container starts from this image

Copying *.csproj before the rest of the source (rather than one COPY . .) is deliberate: Docker caches each layer, and this ordering means dotnet restore only re-runs when dependencies actually change, not on every single code edit — a meaningfully faster rebuild loop.

4. Building and Running

docker build -t roundtrip-api:latest .
docker run -d -p 5000:8080 --name roundtrip-api roundtrip-api:latest
FlagMeaning
-tTag the image with a name (and optional :version)
-dRun detached, in the background
-p 5000:8080Map host port 5000 to the container's port 8080 (hostport:containerport)
--nameGive the running container a friendly name instead of a random one

5. Everyday Commands

docker logs roundtrip-api # view a container's stdout/stderr
docker logs -f roundtrip-api # follow logs live, same idea as tail -f
docker exec -it roundtrip-api bash # open an interactive shell inside a running container
docker stop roundtrip-api # gracefully stop a running container
docker rm roundtrip-api # remove a stopped container
docker rmi roundtrip-api:latest # remove an image
docker system prune # clean up unused containers, networks, and dangling images

docker exec -it is the single most useful debugging command — it drops you inside the running container exactly as it exists right now, letting you inspect files, check environment variables, or test connectivity from the container's own perspective rather than guessing from outside it.

6. Volumes and Bind Mounts

By default, anything written inside a container disappears when the container is removed — containers are meant to be disposable. Volumes and bind mounts persist data outside that lifecycle.

docker run -v roundtrip-data:/var/lib/data roundtrip-api # named volume, managed by Docker
docker run -v $(pwd)/local-folder:/app/data roundtrip-api # bind mount, maps directly to a folder on your machine
TypeUse case
Named volumePersistent data Docker manages for you — database data files, for example
Bind mountDirectly linking a folder on your machine into the container — the standard way to get live code reloading during local development

7. Networking

docker network ls # list networks
docker network create traxs-local # create a custom network
docker run --network traxs-local ... # attach a container to it

Containers on the same custom network can reach each other by container name as a hostname — this is the mechanism that makes docker-compose.yml service names resolve to each other (covered in the next chapter), rather than each container needing to know the others' IP addresses.

8. OrbStack for Local Traxs Development

OrbStack is the Docker runtime used for local development across Traxs projects (macOS). It's a drop-in replacement for Docker Desktop — the same docker CLI commands, images, and Dockerfiles work identically; OrbStack just runs the underlying virtualization more efficiently on macOS.

# All standard docker commands work exactly the same under OrbStack
docker ps
docker compose up -d

If a container behaves unexpectedly only under OrbStack and not under a teammate's Docker Desktop (or vice versa), that's worth flagging specifically — the two are meant to be compatible, but resource limits (memory/CPU allocated to the VM) differ by default and are worth checking first.

9. Troubleshooting Playbook

SymptomLikely causeWhat to check
docker: command not foundOrbStack/Docker not running, or CLI not on PATHConfirm OrbStack is running; which docker
Container exits immediately after docker runThe main process finished or crashed immediatelydocker logs <name> right after — the exit reason is almost always in there
Can't reach the app at localhost:5000Port mapping missing or wrong, or the app isn't listening on the port you expect inside the containerConfirm -p hostport:containerport matches what the app actually binds to inside the container (check EXPOSE and app startup config)
Changes to code don't show up in the running containerRebuilt without a bind mount, or the image was cachedFor live development, use a bind mount rather than rebuilding on every change; otherwise docker build again
"port is already allocated"Another container or local process already using that host portdocker ps to check running containers; lsof -i :5000 to check local processes (see the Bash Reference chapter)
Container can't reach another container by nameBoth aren't on the same custom Docker networkdocker network ls and docker inspect <container> to confirm network membership
Disk filling up over timeOld stopped containers, dangling images, and unused volumes accumulatingdocker system prune (add --volumes to also remove unused volumes, with care)
permission denied inside the container writing to a bind-mounted folderUID/GID mismatch between the container's user and the host folder's ownershipAdjust the Dockerfile's user, or the host folder's permissions, to align

10. Quick Reference

CategoryCommandPurpose
Builddocker build -t name:tag .Build an image from a Dockerfile
Rundocker run -d -p host:container nameRun a container, detached, with port mapping
Inspectdocker ps -aList all containers, running or stopped
Inspectdocker logs -f nameFollow a container's logs live
Debugdocker exec -it name bashOpen a shell inside a running container
Lifecycledocker stop name / docker rm nameStop / remove a container
Imagesdocker images / docker rmi nameList / remove images
Cleanupdocker system pruneRemove unused containers, networks, dangling images
Storage-v volume:/pathNamed volume, Docker-managed
Storage-v $(pwd)/dir:/pathBind mount, maps a local folder directly
Networkdocker network create nameCreate a custom network for containers to reach each other by name

Part of the Traxs Engineering Handbook — Volume 3: Modern Development. Companion chapters in this volume: Docker Compose, REST APIs, Authentication & Authorization, JSON & YAML, Postman Guide.