Containers and IaC
Docker
Container, image, volume and network commands for a single Docker daemon; multi-service stacks belong in Docker Compose.
Cheatsheet #
| Task | Command |
|---|---|
| What am I talking to | docker context show |
| Everything, including stopped | docker ps -a |
| Last 100 log lines, follow | docker logs -f --tail 100 myapp |
| Shell inside a container | docker exec -it myapp sh |
| One-off container, no trace | docker run --rm -it alpine sh |
| Publish a port to localhost only | docker run -p 127.0.0.1:8080:80 nginx |
| Why did it exit | docker inspect -f '{{.State.ExitCode}} {{.State.Error}}' myapp |
| Where is my disk | docker system df |
| Resolve a name from inside | docker exec myapp getent hosts db |
| Copy a file out | docker cp myapp:/app/config.yaml . |
| Build with a named tag | docker build -t myapp:1.0 . |
| Image layers and sizes | docker history myapp:1.0 |
Start with a container problem #
For multi-service stacks, see Docker Compose. A container is a process with namespaces and cgroups applied. It exits when PID 1 exits, so a “crashed” container is almost always a process that returned, not Docker losing it.
docker context show # which daemon the CLI targets
docker ps -a # status, exit codes, published ports
docker logs --tail 100 myapp # stdout/stderr of PID 1 only
docker inspect -f '{{.State.Status}} {{.State.ExitCode}} {{.State.OOMKilled}}' myapp| Symptom | Cause to check first |
|---|---|
| Exited (0) immediately | Command finished; no long-running process in CMD |
| Exited (137) | SIGKILL, usually the memory limit — OOMKilled: true |
| Exited (1) with empty logs | Application logs to a file, not stdout |
| Port published but refused | Process bound to 127.0.0.1 inside the container instead of 0.0.0.0 |
| Name resolves nowhere | Containers are on different user-defined networks, or on the default bridge |
Data gone after docker rm | Writes went to the container layer, not a volume |
Prune removes resources for every project on this daemon
docker system prune --volumes deletes unused volumes, which is where databases usually live. Run docker system df -v first and confirm what “unused” covers.
Containers #
docker run is create plus start. Flags that shape the sandbox (network, mounts, limits, user) can only be set at create time — changing them means replacing the container.
docker run -d --name myapp \
-p 127.0.0.1:8080:80 \
-e DB_HOST=db \
-v mydata:/app/data \
--memory 512m --cpus 1.5 \
--restart unless-stopped \
nginx:1.27-alpine
docker run --rm -it alpine:3.20 sh # throwaway shell
docker stop myapp # SIGTERM to PID 1, SIGKILL after 10s
docker stop -t 30 myapp # give it 30s to drain
docker start myapp # same sandbox, same filesystem, new process
docker rm -f myapp # kill and remove
docker update --memory 1g myapp # limits are the exception: changeable in place| Flag | Effect |
|---|---|
-p 8080:80 | Host port 8080 on all interfaces to container port 80 |
--network mynet | Join a user-defined network; enables DNS between containers |
--restart unless-stopped | Restart on daemon start and on failure, but not after a manual stop |
--user 1000:1000 | Run as a non-root UID, overriding the image’s USER |
--read-only --tmpfs /tmp | Immutable root filesystem with writable scratch |
--cap-drop ALL | Drop capabilities; add back only what the process needs |
Images #
An image is an ordered stack of read-only layers plus metadata. Each Dockerfile instruction that changes the filesystem adds a layer, and the build cache reuses a layer only while its instruction and its inputs are unchanged, so ordering decides rebuild time.
docker pull nginx:1.27-alpine
docker images
docker history myapp:1.0 # per-layer size and instruction
docker image inspect -f '{{.Config.Entrypoint}} {{.Config.Cmd}}' myapp:1.0
docker tag myapp:1.0 registry.example.com/myapp:1.0
docker push registry.example.com/myapp:1.0
docker image prune -a # anything not referenced by a containerPull by digest when the deployment must be reproducible: a tag is a moving pointer, nginx@sha256:... is not.
Dockerfile #
Put the instructions that rarely change first and the ones that change every commit last, so dependency layers survive in cache. COPY go.mod go.sum ./ before COPY . . is the whole trick.
FROM golang:1.23-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download # cached until the manifests change
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -o /out/server .
FROM alpine:3.20
RUN apk add --no-cache ca-certificates && adduser -S -u 10001 app
COPY --from=build /out/server /usr/local/bin/server
USER app
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --retries=3 CMD wget -qO- http://127.0.0.1:8080/health || exit 1
ENTRYPOINT ["server"]
CMD ["--port", "8080"]| Instruction | Behaviour worth knowing |
|---|---|
ENTRYPOINT vs CMD | ENTRYPOINT is the executable, CMD supplies default arguments that docker run overrides |
Exec form ["x"] | No shell, so the process is PID 1 and receives signals; shell form wraps it in /bin/sh -c |
EXPOSE | Documentation and Compose hinting only; it publishes nothing |
ARG vs ENV | ARG exists at build time, ENV persists into the running container |
RUN apt-get update && install | Must be one RUN, or a cached update layer feeds stale package indexes to install |
COPY --from=stage | Copies artefacts out of a build stage, leaving toolchains behind |
Secrets passed as ARG or ENV stay readable in image metadata. Use RUN --mount=type=secret with BuildKit, or inject at runtime.
Volumes #
Writes inside a container land in a copy-on-write layer that dies with the container. Named volumes are a directory the daemon manages; bind mounts are a host path grafted in, with host ownership and permissions intact.
docker volume create mydata
docker run -v mydata:/var/lib/postgresql/data postgres:17 # named volume
docker run -v "$PWD/src:/app/src:ro" myapp # bind mount, read-only
docker run --mount type=tmpfs,destination=/tmp myapp # memory-backed scratch
docker volume inspect mydata # Mountpoint on the host
docker volume ls -f dangling=true # not attached to any containerBack up a named volume without stopping to think about its driver:
docker run --rm -v mydata:/data -v "$PWD:/backup" alpine tar czf /backup/mydata.tgz -C /data .Networks #
On a user-defined bridge network the daemon runs an embedded DNS resolver at 127.0.0.11, so containers reach each other by container name or alias. The default bridge network has no such resolution — that alone explains most “works in Compose, fails with docker run” reports.
docker network create mynet
docker run -d --network mynet --name db postgres:17
docker run --rm --network mynet alpine getent hosts db # name resolves to the container IP
docker network connect mynet myapp # attach a running container
docker network inspect mynet -f '{{json .Containers}}'| Mode | Behaviour |
|---|---|
bridge (default) | NAT behind the host; published ports only |
| user-defined bridge | Same, plus DNS by name and container isolation per network |
host | No namespace: binds host ports directly, ignores -p, Linux only |
none | Loopback only |
Published ports bypass the host firewall on Linux because Docker writes its own DOCKER chain in iptables/nftables. Bind to 127.0.0.1 for anything that should not leave the machine.
Logs and debugging #
docker logs reads the daemon’s log file for that container, so it only ever shows what PID 1 wrote to stdout and stderr. Anything the application writes to a file is invisible here.
docker logs -f --since 15m --timestamps myapp
docker exec -it myapp sh # or bash on Debian-based images
docker exec myapp ps -eo pid,comm,rss # what is actually running
docker stats --no-stream # live CPU, memory, I/O
docker top myapp # host-side view of the processes
docker diff myapp # files changed since the image
docker cp myapp:/etc/nginx/nginx.conf . # pull a file outDebug a container with no shell by attaching a toolbox to its namespaces:
docker run --rm -it --pid container:myapp --net container:myapp --cap-add SYS_PTRACE nicolaka/netshootRegistry #
docker login registry.example.com -u username --password-stdin < token.txt
docker manifest inspect nginx:1.27-alpine # digests and platforms, no pull
docker buildx build --platform linux/amd64,linux/arm64 -t registry.example.com/myapp:1.0 --push .Credentials land in ~/.docker/config.json, base64 encoded, not encrypted, unless a credential helper is configured.
System cleanup #
docker system df -v # what is using the space
docker container prune # stopped containers
docker image prune -a --filter "until=168h" # images unused for a week
docker builder prune --keep-storage 10GB # BuildKit cache is often the bulk
docker system prune # all of the above, no volumesOneliners #
# Stop everything running
docker stop $(docker ps -q)
# Remove containers that exited non-zero
docker rm $(docker ps -aq -f status=exited)
# Every container's IP on every network
docker inspect -f '{{.Name}} {{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' $(docker ps -q)
# Images sorted by size
docker images --format '{{.Size}}\t{{.Repository}}:{{.Tag}}' | sort -h -r | head
# Environment of a running container
docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' myapp
# Follow logs of every container at once
docker ps -q | xargs -P0 -I{} docker logs -f --tail 5 {}
# Which container owns port 8080
docker ps --format '{{.Names}}\t{{.Ports}}' | grep 8080
# Wait until a health check passes
until [ "$(docker inspect -f '{{.State.Health.Status}}' myapp)" = healthy ]; do sleep 1; done