Cloud Tech by Victor
DevOpsBeginner

Docker Fundamentals

How images, containers, volumes, and networks fit together in Docker's runtime model, and the mental shift from "installing software" to "running a packaged image" that trips up newcomers.

Updated 2026-07-247 min read

Overview

Docker packages an application together with everything it needs to run - a base OS layer, a runtime, dependencies, and the app itself - into a single, portable image. Running that image starts a container: an isolated process with its own filesystem view, network namespace, and process tree, sharing the host's kernel rather than virtualizing an entire OS the way a VM does. That's the core mental shift for anyone coming from "installing software": you don't configure a machine to run your app, you build an image once and run it, identically, on a laptop, a CI runner, or a production host. Everything else - volumes for persistent data, networks for container-to-container communication, Compose for running several containers as one unit - exists to manage the boundary between that disposable, reproducible container and the stateful, connected world around it.

Quick Reference

ConceptPurposeLifetime
ImageRead-only, layered filesystem snapshot + metadataImmutable once built
ContainerA running (or stopped) instance of an image, plus a writable layerDisposable, recreated freely
VolumeNamed, Docker-managed persistent storage, mounted into a containerLong-lived, survives container removal
NetworkVirtual network connecting containers, with built-in DNS by nameLong-lived
Compose serviceOne entry in a compose.yaml, describing how to run a containerDeclarative, versioned with the project

The commands below are grouped the way you'd reach for them day to day, not an exhaustive CLI reference (see the official Docker CLI reference for every flag), but the ones that cover almost all daily Docker work.

CommandDescriptionCopy
docker versionShow the Docker client and server (Engine) version.
docker infoShow system-wide information: storage driver, containers, images, resource limits.
docker loginAuthenticate to a registry (Docker Hub by default).
docker logoutRemove locally stored registry credentials.
docker search nginxSearch Docker Hub for images matching a term.

Syntax

dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
yaml
# compose.yaml
services:
  api:
    build: .
    ports:
      - '3000:3000'
    environment:
      DATABASE_URL: postgres://db:5432/app
    depends_on:
      - db
  db:
    image: postgres:16
    volumes:
      - db-data:/var/lib/postgresql/data
volumes:
  db-data:

Examples

bash
docker build -t app:1.0 .
docker run -d -p 3000:3000 --name app app:1.0
docker logs -f app
docker exec -it app sh

Drop into a running container's shell to inspect files or debug a process without stopping it.

docker exec -it app sh

Visual Diagram

Common Mistakes

  • Writing data a container needs to keep into its own writable layer instead of a volume or bind mount; it's gone the moment docker rm runs.
  • Confusing EXPOSE in a Dockerfile with actually publishing a port; EXPOSE only documents intent, -p host:container on docker run is what makes a port reachable from outside.
  • Running containers as the default root user in production images, which turns a container escape into a host-level root compromise.
  • Tagging every build latest and deploying that tag, so "which version is actually running" becomes unanswerable - pin explicit version tags instead.
  • Running docker system prune -a on a shared host without checking what's still in use; it deletes any image not backing a currently running container.

Performance

  • Docker layers a Dockerfile top to bottom and caches each layer; ordering rarely-changing steps (installing dependencies) before frequently-changing ones (copying source code) means most rebuilds only re-run the last few layers.
  • Multi-stage builds (FROM ... AS build, then a second FROM that copies only the compiled output) keep the final image small by discarding build-time tooling that isn't needed at runtime.
  • Smaller base images (alpine, distroless) reduce image pull time and attack surface, but can lack libraries (like glibc) that some binaries assume are present - worth checking before committing to one.
  • Unbounded containers can starve a host of memory or CPU; --memory and --cpus on docker run (or deploy.resources.limits in Compose) cap what a single container can consume.

Best Practices

  • Use multi-stage builds and a .dockerignore file so build context and final images only contain what's actually needed at runtime.
  • Pin base image and dependency versions explicitly; never rely on a moving latest tag for anything beyond local experimentation.
  • Run as a non-root user inside the container (USER node, or an equivalent line for your base image) unless there's a specific reason not to.
  • Keep one primary process per container; if two unrelated services need to scale or fail independently, they belong in two containers, not one with a supervisor process.
  • Reach for volumes (not bind mounts) for data a container owns and manages itself, and bind mounts mainly for local development, where you want host file changes to show up instantly.

Interview questions

What is the difference between a Docker image and a Docker container?

An image is a read-only, layered filesystem snapshot plus metadata (entrypoint, exposed ports, environment); it never changes once built and can be shared through a registry. A container is a running (or stopped) instance of an image: Docker adds a thin writable layer on top of the image's read-only layers and starts a process inside an isolated namespace. You can start many independent containers from the same image, each with its own writable layer and state, the same way many processes can run from the same binary on a normal OS.

Why does data written inside a container disappear when the container is removed?

Anything a container writes lands in its own writable layer, which is deleted along with the container by `docker rm`. That is deliberate; it is what makes containers disposable and reproducible, a fresh container from the same image always starts from the same known state. Anything that actually needs to survive a container's lifecycle (a database's data files, uploaded assets) has to live outside that writable layer, in a named volume or a bind mount, which Docker mounts into the container at a chosen path but manages independently of the container itself.

Why is publishing a port with `-p 8080:80` different from the container just "having" port 80?

A container's ports exist only on its own private network namespace by default; nothing on the host or outside can reach them until Docker explicitly forwards a host port to it. `-p 8080:80` tells Docker's network layer to forward the host's port 8080 to port 80 inside the container's namespace, host port first, container port second. Leaving a port `EXPOSE`d in a Dockerfile only records metadata/documentation, it has no effect on connectivity at all: another container on the same Docker network can already reach any port the first container is listening on, EXPOSE or not. Publishing to the host is the one thing that always requires an explicit `-p`.

References

ShareXLinkedInReddit
Was this page helpful?
Suggest an improvement