Software Engineering Wiki

Containers and IaC

Docker

Container, image, volume and network commands for a single Docker daemon; multi-service stacks belong in Docker Compose.

Cheatsheet #

TaskCommand
What am I talking todocker context show
Everything, including stoppeddocker ps -a
Last 100 log lines, followdocker logs -f --tail 100 myapp
Shell inside a containerdocker exec -it myapp sh
One-off container, no tracedocker run --rm -it alpine sh
Publish a port to localhost onlydocker run -p 127.0.0.1:8080:80 nginx
Why did it exitdocker inspect -f '{{.State.ExitCode}} {{.State.Error}}' myapp
Where is my diskdocker system df
Resolve a name from insidedocker exec myapp getent hosts db
Copy a file outdocker cp myapp:/app/config.yaml .
Build with a named tagdocker build -t myapp:1.0 .
Image layers and sizesdocker 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
SymptomCause to check first
Exited (0) immediatelyCommand finished; no long-running process in CMD
Exited (137)SIGKILL, usually the memory limit — OOMKilled: true
Exited (1) with empty logsApplication logs to a file, not stdout
Port published but refusedProcess bound to 127.0.0.1 inside the container instead of 0.0.0.0
Name resolves nowhereContainers are on different user-defined networks, or on the default bridge
Data gone after docker rmWrites 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
FlagEffect
-p 8080:80Host port 8080 on all interfaces to container port 80
--network mynetJoin a user-defined network; enables DNS between containers
--restart unless-stoppedRestart on daemon start and on failure, but not after a manual stop
--user 1000:1000Run as a non-root UID, overriding the image’s USER
--read-only --tmpfs /tmpImmutable root filesystem with writable scratch
--cap-drop ALLDrop 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 container

Pull 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"]
InstructionBehaviour worth knowing
ENTRYPOINT vs CMDENTRYPOINT 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
EXPOSEDocumentation and Compose hinting only; it publishes nothing
ARG vs ENVARG exists at build time, ENV persists into the running container
RUN apt-get update && installMust be one RUN, or a cached update layer feeds stale package indexes to install
COPY --from=stageCopies 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 container

Back 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}}'
ModeBehaviour
bridge (default)NAT behind the host; published ports only
user-defined bridgeSame, plus DNS by name and container isolation per network
hostNo namespace: binds host ports directly, ignores -p, Linux only
noneLoopback 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 out

Debug 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/netshoot

Registry #

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 volumes

Oneliners #

# 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

Last updated 15 September 2026 · Edit this page