home / writing / docker-from-the-ground-up

Docker From the Ground Up

Docker packages an application and its dependencies into a portable image that runs the same way everywhere. This guide covers the architecture, image layering, Dockerfile habits, networking, storage and Compose.

Architecture

Docker uses a client/server model. The docker CLI talks over a socket or HTTP API to the Docker daemon (dockerd), which manages images, containers, networks and volumes. The daemon delegates the actual running of containers to a lower-level runtime (containerd and runc), which uses Linux kernel features to isolate processes.

  • Namespaces give each container its own view of processes, network interfaces, mounts and hostname.
  • Control groups (cgroups) limit and account for CPU, memory and I/O.
  • Union filesystems stack read-only image layers with a thin writable layer on top.

A container is therefore not a virtual machine: it is an ordinary process on the host kernel, started from an image. It starts quickly and carries no guest operating system, which is why you can run many of them on one host.

Images and layers

An image is a read-only template made of ordered layers. Each filesystem-changing instruction in a Dockerfile produces a layer, identified by a content hash. When you run a container, Docker adds one writable layer on top; anything the container writes goes there and disappears when the container is removed.

Layers are shared. If ten images start from the same base, the base layers are stored once and pulled once. Layers are also cached during builds: if an instruction and everything before it is unchanged, Docker reuses the cached result. This is the single most important fact for fast builds.

You can inspect the stack with docker history <image> and see metadata, including layer digests, with docker inspect <image>.

Dockerfile best practices

A Dockerfile is a recipe of instructions. A small, cache-friendly example for a Node service:

FROM node:24-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:24-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
USER node
EXPOSE 8080
CMD ["node", "dist/server.js"]
  • Order for caching. Copy dependency manifests and install first, copy the rest of the source afterwards. Source changes then do not invalidate the dependency layer.
  • Multi-stage builds. Compile in one stage, copy only the artifacts into a slim final stage. The result is smaller and leaves build tools behind.
  • Small, pinned bases. Prefer slim or Alpine variants and pin a specific tag rather than relying on latest.
  • Run as non-root. Add a USER instruction.
  • Use .dockerignore. Exclude .git, local dependencies and build output so the build context stays small and secrets do not leak in.
  • Combine related shell steps in one RUN and clean package caches in the same step.

CMD supplies default arguments and is easily overridden; ENTRYPOINT fixes the executable. Use the JSON (exec) form so signals reach your process directly.

Never bake secrets into an image. Every layer is recoverable, even if a later instruction deletes the file. Pass secrets at runtime or use build-time secret mounts.

Everyday commands

TaskCommand
Builddocker build -t myapp:1.0 .
Run detacheddocker run -d --name web -p 8080:80 nginx
Listdocker ps (add -a for stopped)
Logsdocker logs -f web
Shell insidedocker exec -it web sh
Stop and removedocker stop web && docker rm web
Inspectdocker inspect web
Clean updocker system prune

The -p host:container flag publishes a container port on the host. -e NAME=value sets environment variables, --rm removes the container on exit, and -it attaches an interactive terminal. Both docker ps and docker inspect accept --format with a Go template, handy for scripting, such as extracting a container's IP address.

Networking

Docker networking is driver-based, and the driver decides how containers reach each other and the outside world.

  • bridge is the default on a single host. Containers get private addresses and reach the outside through NAT. On a user-defined bridge, containers resolve each other by name via built-in DNS; on the default bridge they do not.
  • host shares the host network stack, with no isolation and no port mapping needed.
  • none disables networking entirely.
  • overlay spans multiple hosts, used by Swarm for service-to-service traffic.
  • macvlan gives a container its own MAC address on the physical network.
docker network create app-net
docker run -d --name db --network app-net postgres:16
docker run -d --name api --network app-net myapp:1.0
docker network inspect app-net

Here api can reach the database simply at the hostname db. Create a user-defined network per application rather than using the default bridge.

Volumes and storage

The writable container layer is ephemeral. To keep data, mount storage from outside the container.

  • Named volumes are created and managed by Docker (docker volume create data) and are the best choice for databases and other persistent state.
  • Bind mounts map a host path into the container, ideal for live-editing source during development.
  • tmpfs mounts live in memory only, useful for scratch data you do not want on disk.
docker run -d -v data:/var/lib/postgresql/data postgres:16
docker run -d --mount type=bind,source="$(pwd)",target=/app myapp:1.0

The --mount syntax is more explicit and errors if a bind source is missing, whereas -v silently creates a directory. Check what your host uses with docker info; the default storage driver is overlay2 on modern Linux.

Docker Compose

Compose describes a multi-container application in one YAML file and manages it as a unit.

services:
  api:
    build: .
    ports:
      - "8080:8080"
    environment:
      DB_HOST: db
    depends_on:
      - db
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: changeme
    volumes:
      - dbdata:/var/lib/postgresql/data
volumes:
  dbdata:

Compose creates a dedicated network for the project, so services find each other by service name. Common commands:

  • docker compose up -d builds if needed and starts everything.
  • docker compose logs -f api follows one service.
  • docker compose up -d --scale api=3 runs several replicas (do not publish a fixed host port for a scaled service).
  • docker compose down stops and removes containers; add -v to remove volumes too.

Note that depends_on controls start order, not readiness. Add a healthcheck and a condition if the app needs the database actually accepting connections.

For multi-host clustering, Docker's built-in Swarm mode offers manager and worker nodes, replicated services and overlay networks, though most teams today use Kubernetes for orchestration at scale.

Registries and security

A registry stores and distributes images. Name images as registry-host/namespace/name:tag. To publish:

docker login registry.example.com
docker tag myapp:1.0 registry.example.com/team/myapp:1.0
docker push registry.example.com/team/myapp:1.0

Use docker save and docker load to move an image as a tarball when no registry is reachable. Tags are mutable pointers, so use immutable version tags or digests for production deployments.

Security habits worth adopting:

  • Run as a non-root user and drop capabilities you do not need.
  • Use read-only root filesystems where the app allows it.
  • Set memory and CPU limits (--memory, --cpus) so one container cannot starve the host.
  • Scan images for known vulnerabilities and rebuild regularly on updated bases.
  • Never expose the Docker socket to untrusted containers; access to it is effectively root on the host.