Docker Without the Magic
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 nonrootor pass--user. localhostinside a container is the container, not your host. Usehost.docker.internal(Docker Desktop) or the container's own network gateway.docker buildsends the entire working directory as context. Add a.dockerignore.