Devops

Docker Without the Magic

·2 min read

Docker is not lightweight virtualization. There is no hypervisor and no second kernel — a "container" is a normal Linux process with restricted views of the kernel's resources. Understanding the three Linux features that make this work demystifies everything else about containers.

The three kernel features

Namespaces give a process its own view of one piece of system state. There are eight kinds — PID, network, mount, IPC, UTS (hostname), user, cgroup, time. A container is a process running in fresh namespaces of each kind, so it sees its own process tree, network interfaces, filesystem mounts, and so on.

cgroups (control groups) limit and account resource usage — CPU shares, memory caps, block I/O bandwidth — for a group of processes. --memory 512m on a docker run wires up a memory cgroup.

OverlayFS stacks read-only layers (the image) under a thin writable layer (the container). This is why images are deduplicated across containers and why builds are incremental.

The commands you actually use

docker run -it --rm alpine sh                # interactive, auto-remove
docker run -d --name web -p 8080:80 nginx    # detached, named, port mapped
docker ps                                     # running
docker ps -a                                  # all, including stopped
docker logs -f web                            # follow logs
docker exec -it web sh                        # shell into running container
docker stop web && docker rm web              # stop + remove

Images vs containers

An image is the read-only template; a container is a running instance. Build images with a Dockerfile:

FROM golang:1.22-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/myapp ./cmd/myapp

FROM gcr.io/distroless/static
COPY --from=build /out/myapp /
USER nonroot:nonroot
ENTRYPOINT ["/myapp"]

Multi-stage builds — the AS build + second FROM pattern — let you compile in a big image and ship a tiny one.

Layer caching

Each Dockerfile instruction creates a layer. Docker caches layers and reuses them when their inputs haven't changed. The classic optimization: copy your dependency manifests and install dependencies before copying source code. Otherwise every code change busts the dependency cache.

Volumes and bind mounts

# Named volume — Docker manages it
docker run -v myvolume:/data alpine touch /data/hello

# Bind mount — points at a host path
docker run -v /etc/myapp:/etc/myapp:ro alpine cat /etc/myapp/conf

# tmpfs — in-memory only
docker run --tmpfs /tmp alpine

docker-compose for multi-container

services:
  web:
    image: nginx:alpine
    ports: ["8080:80"]
    depends_on: [api]
  api:
    build: ./api
    environment:
      DATABASE_URL: postgres://db/myapp
    depends_on: [db]
  db:
    image: postgres:16-alpine
    volumes: [pgdata:/var/lib/postgresql/data]
    environment:
      POSTGRES_PASSWORD: secret
volumes:
  pgdata:

Things that hurt new users

  • Files written inside a container are gone when the container is removed unless they're on a volume.
  • By default, all images run as root. Use USER nonroot or pass --user.
  • localhost inside a container is the container, not your host. Use host.docker.internal (Docker Desktop) or the container's own network gateway.
  • docker build sends the entire working directory as context. Add a .dockerignore.

Related