Containers & Images

Docker Fundamentals — One Implementation of the Model

Dockerfile, build, image, container, registry, volume, network, published port: eight nouns that cover almost everything a team does day to day. They are the vocabulary of one popular toolchain, not the definition of containers.

The question this answers

Infrastructure question

What is the minimum working vocabulary for operating containers, and which parts of it are Docker rather than containers?

Application requirement

A new engineer must be able to build the service, run it locally against a database, reach it from a browser, keep the database's data across restarts, and push the resulting artifact — without a platform team walking them through it.

What it provides

A small, transferable command surface for the whole local lifecycle, and a clear line between the concepts that survive a change of tooling and the flags that do not.

Application RequirementInfrastructure RequirementComputeNetworkStorageIdentityDeploymentScalingReliabilityObservabilitySecurityCostTrade-offs

Eight nouns, and which of them are standardized

containers· Docker CLI used for concreteness. Podman, containerd/nerdctl and Buildah cover the same ground with different flags.

Docker made containers usable and gave the industry its vocabulary, and it is worth being precise about which parts of that vocabulary are portable. The image format, the registry protocol and the runtime interface are open specifications; an image built by any modern builder runs on any modern runtime. The Dockerfile syntax, the CLI flags, Compose, and the local network and volume drivers are one vendor's ergonomics — widely copied, not standardized.

The practical consequence is that migrating from Docker to another builder or runtime is a tooling change, not an artifact change. Your images keep working. What breaks is scripts that assume a Docker daemon, a Docker socket, or Compose-specific behaviour — which is exactly why mounting the Docker socket into a container is both a portability trap and a serious security finding.

TermWhat it actually isPortable?
DockerfileA build recipe: an ordered list of instructions producing layers.Docker-originated syntax; most builders accept it, no standard requires it.
BuildExecuting that recipe to produce layers plus a config document.Concept is universal; the builder and its cache semantics are not.
ImageThe immutable artifact: manifest + layers + config.Standardized (OCI). Runs anywhere.
ContainerA process started from an image, with a disposable writable layer.Standardized runtime interface.
RegistryContent-addressed storage the image is pushed to and pulled from.Standardized distribution protocol.
VolumeStorage mounted into the container that outlives it.Concept is universal; drivers and semantics are platform-specific.
NetworkA virtual network the container attaches to, with name resolution between members.Concept is universal; Docker's bridge/DNS behaviour is its own.
Published portA host port mapped to a container port so traffic from outside can arrive.Local-development shape. In production an ingress or load balancer does this job.
The working vocabulary, and how far each term travels.

The whole loop in eight commands

Almost every local container task is one of these. Read them for the shape rather than the flags: build an artifact, run it with configuration injected from outside, attach storage that survives, publish a port so you can reach it, inspect why it is unhappy, then push it so something other than your laptop can pull it.

Two habits in this block are worth carrying into production. --env-file keeps configuration out of the image, which is the whole of Configuration Belongs Outside the Image. Pushing an immutable tag — ideally recording the digest the registry returns — is what makes Build Once, Promote the Same Bytes possible later.

1# 1. build — tag with something immutable, not just :latest
2docker build -t registry.example.com/checkout:1.14.2 .
3
4# 2. run — config from outside, storage outside, port published to the host
5docker run --rm \
6 --name checkout \
7 --env-file ./local.env \
8 -e DATABASE_URL="postgres://app@db:5432/checkout" \
9 -p 8080:8080 \
10 --network app-net \
11 --read-only --user 10001 \
12 registry.example.com/checkout:1.14.2
13
14# 3. inspect — the four commands that answer "why is it unhappy"
15docker ps -a # exit code and restart state
16docker logs --tail=100 checkout # stdout/stderr, the only log path that survives
17docker exec -it checkout sh # only works if the image still has a shell
18docker history registry.example.com/checkout:1.14.2 # what each layer added, and how big
19
20# 4. publish — the registry returns the digest; that digest is the real identity
21docker push registry.example.com/checkout:1.14.2
22# -> digest: sha256:6b1f… — record this, deploy this
The local lifecycle. The last command is the one that matters for production.

Volumes, networks and published ports — and what replaces them in production

containers· Compose semantics. An orchestrator expresses the same three concerns as services, persistent volume claims and ingress objects.

Locally, a bridge network gives containers name resolution among themselves, a volume gives the database a filesystem that survives docker rm, and -p 8080:8080 punches a hole from the host into the container. That trio is a complete miniature of a production topology, which is why Compose is such a good teaching tool.

It is also where the analogy stops. In production, name resolution is a service discovery mechanism (Service: A Stable Name in Front of Moving Pods or DNS in Cloud Infrastructure), the port is published by a load balancer or ingress terminating TLS (Load Balancers as Infrastructure, Ingress and Gateway: Getting Traffic In), and the volume is a network-attached disk, a managed database or an object store (Persistent Data and Containers). A Compose file is not a deployment; it is a local approximation, and treating it as one is how teams end up running a production database in a container on one host with a bind mount.

Local Compose shape on the left of the boundary; what each piece becomes in production on the right.PROVIDER-NEUTRAL
Developer hostprivate
Bridge network app-netinternal
checkout:1.14.2internal— reachable on the host via -p 8080:8080
postgres:16 containerinternal
Named volume pgdataprivate— survives container removal, not host loss
Production: load balancer :443public— replaces the published port; terminates TLS
Production: managed databaseprivate— replaces the database container and the volume
checkout:1.14.2postgres:16 container· resolves "db" by container name
postgres:16 containerNamed volume pgdata· mount /var/lib/postgresql/data
Production: load balancer :443checkout:1.14.2· what -p becomes in productioncrosses boundary
checkout:1.14.2Production: managed database· what the db container becomes
1services:
2 checkout:
3 image: registry.example.com/checkout:1.14.2
4 ports: ["8080:8080"] # host:container — local only
5 environment:
6 DATABASE_URL: postgres://app@db:5432/checkout
7 STRIPE_KEY: ${STRIPE_KEY} # read from the shell, never committed
8 depends_on: [db]
9 read_only: true
10 user: "10001"
11 db:
12 image: postgres:16
13 volumes: ["pgdata:/var/lib/postgresql/data"] # outlives the container
14volumes:
15 pgdata: {}
The same shape as a Compose file. Note that no secret value appears in it.

Key points

  • Image format, registry protocol and runtime interface are open standards; Dockerfile syntax, CLI flags and Compose are one toolchain's ergonomics.
  • The eight nouns — Dockerfile, build, image, container, registry, volume, network, published port — cover nearly all day-to-day work.
  • Configuration and secrets enter at run, never at build. --env-file and shell-provided variables are the local form of that rule.
  • A published port is a development shape; production replaces it with a load balancer or ingress that terminates TLS.
  • Compose is a local approximation of a topology, not a deployment target. The database container and its bind mount are the parts that do not translate.

The loop, answered

Every field is required, which is why no lesson here can recommend something without saying what it costs and what simpler thing to consider first.

How it works
  • A build sends a context directory to the builder, which executes each instruction, snapshots the filesystem changes into layers and writes a config document.
  • run creates namespaces and a cgroup, mounts the image layers plus a writable layer, applies env, mounts and user, then execs the entrypoint.
  • A user-defined network gives each container an interface and a name that other members resolve — the mechanism is taught in Networking as container networking.
  • -p host:container installs a forwarding rule on the host so external traffic reaches the container's port; without it the port exists but is unreachable from outside.
  • A volume mount replaces a path in the union filesystem with storage managed outside the container, so writes there are not in the disposable layer.
  • push uploads layers the registry does not already have and returns the manifest digest, which is the artifact's immutable name.
What you still own
  • You own the tag discipline. :latest is a mutable pointer and is not a version — see The Container Registry.
  • You own log routing: stdout and stderr are the only outputs the platform collects, and a log file inside the container is a log file you will lose.
  • You own the daemon socket. Mounting it into a container grants that container full control of the host's containers — root, in practice.
  • You own the local/production gap: whatever Compose does for you, something in production must do explicitly.
  • You own build context size. A missing .dockerignore uploads node_modules and .git on every build and quietly slows the whole pipeline.
How it fails
  • The service starts, the logs look fine, and nothing answers — the port was exposed in the image but never published to the host.
  • docker exec … sh fails with executable-not-found because the minimal base has no shell, exactly when someone urgently needs to look inside.
  • A bind mount shadows the application directory, so the container runs the developer's working tree instead of the image and "the fix is not deployed".
  • The database container is recreated without its volume and the data is gone, with no error anywhere — the container did exactly what it was told.
  • A build runs for minutes with no output because the context includes a multi-gigabyte directory the .dockerignore never excluded.
How it scales
  • Nothing here scales beyond one host. Compose has no scheduler, no rescheduling, no rollout and no health-driven replacement — that is Why Orchestration Exists.
  • Published ports collide: two containers cannot share a host port, which is the first wall teams hit when packing services onto one machine.
  • Local volumes are host-local, so a workload using one cannot move to another host — the constraint that makes Stateful Workloads: Databases Are Not Stateless APIs hard.
  • Build throughput becomes the bottleneck long before runtime does, and the fix is cache reuse rather than a bigger builder.
Security
  • Run as a non-root user and with a read-only root filesystem where the workload permits; both are one flag and both remove entire classes of finding.
  • Never mount the container daemon socket into a workload. It is equivalent to handing that workload root on the host.
  • Secrets belong in --env-file, a mounted file, or a secret manager reference — never in a build argument, which is recorded in image metadata.
  • A published port on 0.0.0.0 on a machine with a public address exposes the container to the internet. On a laptop that is a nuisance; on a cloud VM it is an incident.
Cost shape
  • Local tooling has no direct cost; the meters it teaches you to touch — registry storage, image transfer, build minutes — do.
  • Build context and cache misses drive CI minutes, the most commonly ignored line item in a delivery pipeline.
  • Running a production-shaped Compose stack on one large VM is cheap right up to the moment it needs redundancy, at which point none of it transfers.
What to watch
  • docker ps -a exit codes: 137 is a kill (usually OOM), 143 is a clean SIGTERM, 0 on an entrypoint that was supposed to keep running is a configuration bug.
  • Container logs on stdout/stderr, because that is the only stream anything downstream will collect.
  • docker history and image size per build, tracked so growth is noticed before it costs a deployment.
  • The signal that lies: "the container is running". Running means the process exists, not that it is serving — which is what Health Checks is for.
Simpler alternatives
  • Podman or containerd with nerdctl — the same images and workflow without a privileged daemon, which removes the socket-exposure problem entirely.
  • A buildpack or platform builder if nobody on the team wants to own Dockerfiles; you trade control for a maintained base and sensible defaults.
  • For a single service with no dependencies, running the process directly during development is faster and simpler than any container. Containers earn their keep when the dependency graph does.
  • For local multi-service development against real cloud dependencies, a remote development environment often beats reproducing the whole stack on a laptop.
What adopting this costs
  • A local environment that mirrors production buys confidence and costs maintenance of a second, subtly different system.
  • Docker's ergonomics buy fast onboarding and cost a daemon that runs as root and is a genuine attack surface.
  • Compose buys a working multi-service stack in one file and costs the illusion that it resembles a deployment.
  • Standardizing on the toolchain buys team velocity and costs some portability in scripts, though never in the artifacts themselves.

What people believe, and what is true

Claim

Docker is containers.

Reality

Docker is one implementation. The portable parts — image, registry, runtime — are specifications that several other tools implement.

Claim

EXPOSE in the Dockerfile makes the port reachable.

Reality

It is documentation in the image metadata. Reachability comes from publishing the port or from a service in front of the container.

Claim

Compose files describe a deployment.

Reality

They describe one host's worth of containers with no scheduling, no rollout and no failure handling. The gap is what an orchestrator fills.

Apply it