skip to content

questions

15

A docker build fails and the console shows only the last few lines of the failing step — how do you see that step's full output?

level: juniorimportance: must knowfreq 66%

answer

  1. The default renderer redraws and discards
  2. The output was never lost, just collapsed
  3. One flag switches to append-only logs
  4. An env var makes it CI's default
  5. --progress=plain and BUILDKIT_PROGRESS

basics

~20 s

Re-run the build with --progress=plain. The default progress display collapses each step to its last few lines, while plain mode streams every line of every step, prefixed with the step number and elapsed seconds, and leaves it all on screen.

solid answer

~50 s

BuildKit's default `--progress=auto` renders a live, redrawing view in a terminal: finished steps collapse to one line and a running step shows only a short tail. Re-run with `docker build --progress=plain .` and the builder streams the complete stdout/stderr of every step as ordinary log lines, each prefixed `#<step> <seconds>` — that is where the compiler error or package-manager message actually is. Set `BUILDKIT_PROGRESS=plain` in the environment to make it the default, which is what you want permanently in CI, where the redrawing renderer produces unreadable logs anyway. Read to the bottom for the `ERROR: failed to solve: process "/bin/sh -c ..." did not complete successfully: exit code: N` line — it names the exact command and its exit status. One caveat: steps that hit the build cache print `CACHED` and replay no output, so if the interesting log is above the failure you have to invalidate that stage to see it again.

code

bash · 1 line
bash
docker build --progress=plain -t gateway:dev .

go deeper

for a junior

Memorise the one flag and the reason it exists: the default display redraws and discards, plain keeps every line. Be ready to say where the real error message appears and to read the final failed to solve line aloud.

for a middle

Explain that --progress only selects a renderer over the builder's status stream, so it changes output and nothing else, and that #n prefixes exist because independent steps run concurrently and interleave.

for a senior

Show operational habits: BUILDKIT_PROGRESS=plain set in CI so a failure is legible the first time, awareness that cached steps replay no log, and the fact that plain output puts anything a step echoes into a retained CI log.

for a principal

Own the convention: which progress mode CI uses, how long build logs are retained and who can read them, and whether build output is treated as a secrets-disclosure surface in your pipeline's threat model.

## Two renderers, one build When you run `docker build`, the builder (BuildKit, the default engine since Docker Engine 23.0) and the CLI are separate things: the builder executes steps and emits a status stream, and the CLI *renders* that stream. The `--progress` flag chooses the renderer, and it changes nothing about how the image is built. - `--progress=auto` (the default) picks the TTY renderer when stdout is a terminal. It draws a live, redrawing table: each step is one line with a spinner and a duration, a running step shows only a short tail of its output, and when the step finishes that tail is thrown away and the line collapses. - `--progress=tty` forces that renderer. - `--progress=plain` renders the same stream as plain, append-only log lines with no cursor tricks. Every line the step wrote to stdout or stderr is printed and stays printed. Recent buildx versions also accept `--progress=rawjson` for machine-readable output, which is useful when a tool, not a human, is reading the build. ## What plain output looks like Each line carries two prefixes: ``` #14 [build 4/6] RUN go build -o /out/gateway ./cmd/gateway #14 12.03 # command-line-arguments #14 12.03 ./cmd/gateway/main.go:41:9: undefined: newIngestPipeline #14 ERROR: process "/bin/sh -c go build -o /out/gateway ./cmd/gateway" did not complete successfully: exit code: 2 ``` `#14` is the step's number, matching the step header line, and `12.03` is seconds elapsed **since that step started** — not since the build started. Because BuildKit runs independent steps concurrently, the `#n` prefix is what lets you untangle interleaved output from two steps running at once; grepping for `^#14 ` gives you one step's log in isolation. Steps served from the build cache print a single `#7 CACHED` line and no body, because nothing was executed to produce output. At the very bottom, a failed build prints the summary error: `ERROR: failed to solve: process "/bin/sh -c <command>" did not complete successfully: exit code: 2`. Read it before theorising — it tells you the literal shell command BuildKit ran (shell-form `RUN` is wrapped in `/bin/sh -c`) and the exit status, which is often enough on its own. ## Making it the default The environment variable `BUILDKIT_PROGRESS=plain` sets the renderer without touching the command line. Export it in CI: the TTY renderer's escape sequences turn a CI log into a wall of control characters, and CI is exactly where you cannot re-run interactively to look again. Many teams set it globally in the pipeline environment and never think about it again. ## Things that trip people up **There is no `docker logs` for a build.** `docker logs` reads a *container's* log; a build step's container is created and destroyed inside the builder and is never a `docker ps` container, so its output exists only in the build's status stream you are looking at right now. If you close the terminal, it is gone — which is the practical reason to run with plain progress from the start on a build you expect to fail. **Re-running does not always reproduce the log.** Everything above the failing step is now cached and will print `CACHED`. That is usually what you want (the failure reproduces in seconds), but if the message you need came from an *earlier* step, you must invalidate it — `--no-cache-filter=<stage>` for one named stage, or `--no-cache` for the whole build if you are willing to pay for it. **Plain output is a disclosure surface.** It prints everything the step wrote, so a `RUN` that echoes a token, or a package manager that prints a URL with credentials in it, lands in your CI log and stays there for the log's retention period. That is an argument for keeping credentials out of the command line, not an argument against plain progress. **There is no `-v`.** `docker build` has no verbosity flag; `--progress` is the knob. Candidates who reach for `-v`, `--debug` or `docker logs` are signalling they have never actually had to read a failing build.

  • You re-run with --progress=plain and every step above the failure now prints CACHED with no output. How do you get those earlier logs back?
    A cached step executes nothing, so it has no output to replay. Force it to run again: `--no-cache-filter=<stage>` re-runs one named stage and leaves the rest of the cache alone, while `--no-cache` re-runs the whole build and costs you the full build time. Pick the narrowest one that covers the step whose log you need.
  • In plain progress output, what do the two prefixes on a line such as `#14 12.03 ...` mean?
    `#14` is the step number, matching the `#14 [build 4/6] RUN ...` header, and `12.03` is seconds elapsed since that step started, not since the build began. BuildKit runs independent steps concurrently and interleaves their lines, so the `#n` prefix is how you separate them; the elapsed figure also shows you cheaply which step is the slow one.
  • Why is `docker logs` not an option for a failed build step?
    `docker logs` reads the log of a container in the engine's container list. A build step runs in a short-lived container the builder creates and destroys itself; it never appears in `docker ps -a` and has no log to read afterwards. The build's status stream, rendered by `--progress`, is the only copy — which is why you capture it during the run.

saying these in an interview costs you the question

  • Says docker logs shows the build's step output
  • Reaches for a -v or --debug flag on docker build
  • Thinks the collapsed output is permanently lost
  • Always adds --no-cache just to see the log
  • Guesses at the cause from the collapsed tail
  • Confuses the elapsed seconds with total build time

context

open as a page

A container vanishes from `docker ps` seconds after `docker run` — what do you check first?

level: juniorimportance: must knowfreq 86%

basics

~20 s

Run docker ps -a: the container is still listed, with a STATUS such as Exited (1) 20 seconds ago that says whether it ran and died or never started. Then docker logs on that container shows what it printed.

open as a page

Why does docker exec -it <id> sh fail on a scratch-based image, and what can you do instead?

level: juniorimportance: must knowfreq 65%

basics

~20 s

docker exec starts a process from the container's own filesystem, and a scratch or distroless image ships no /bin/sh to start. Bring tools from outside instead: nsenter from the engine host, a toolbox container sharing its namespaces, or a copied static binary.

open as a page

Why do `free` and `top` inside a Docker container report the host's memory rather than the container's limit?

level: middleimportance: must knowfreq 68%

basics

~10 s

Because /proc is not namespaced. free and top read /proc/meminfo, which is host-wide, so a --memory limit is invisible to them. Read the container's cgroup files (/sys/fs/cgroup/memory.max and memory.current) or docker stats instead.

open as a page

What do the columns of `docker stats` show for a running container, and where do those numbers come from?

level: juniorimportance: should knowfreq 60%

basics

~20 s

docker stats streams a live per-container line: CPU %, MEM USAGE / LIMIT, MEM %, NET I/O, BLOCK I/O and PIDS. The daemon computes them from the containers' cgroup counters on the host, not from tools running inside the containers.

open as a page

Under BuildKit, why is there no intermediate image to run after a failed build step, and how do you get a shell at that point?

level: middleimportance: should knowfreq 57%

basics

~20 s

BuildKit commits no image per instruction, so a failed build leaves no id to run — the classic-builder trick is gone. Instead cut a stage boundary just before the failing instruction, build with --target that stage, and run it interactively.

open as a page

Which `docker inspect` fields explain why a container exited, and how do you print just them?

level: middleimportance: should knowfreq 61%

basics

~10 s

Read .State.ExitCode, .State.OOMKilled, .State.Error, .State.StartedAt and .State.FinishedAt, plus the top-level .RestartCount — RestartCount sits outside State. Print them with docker inspect --format and a Go template instead of paging the whole JSON.

open as a page

How do you enter a running Docker container's namespaces with nsenter from the engine host?

level: middleimportance: should knowfreq 46%

basics

~20 s

Read the container's host PID with docker inspect -f '{{.State.Pid}}', then as root on the engine host run nsenter -t <pid> -n -p. Omitting -m keeps the host's mount namespace, so host tools still work while you see the container's network and processes.

open as a page

A docker build that takes 11 minutes dies at step 14 in CI but passes on your laptop — how do you triage it?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Capture the real failure first with plain progress in CI, then shorten the loop before theorising: --target to stop just above the failing step, --no-cache-filter to re-run only that stage, and a shell there to run the command by hand.

open as a page

`docker logs` prints nothing for a container that exits instantly — how do you get evidence?

level: seniorimportance: should knowfreq 47%

basics

~20 s

Empty output has three causes: the process never started, it logged to a file instead of stdout, or the log driver cannot be read back. Check the status first, then open the image with docker run --entrypoint sh.

open as a page

A Docker container's CPU usage sits below its limit yet p99 latency spikes. How do you confirm CPU throttling?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Read the container's cgroup cpu.stat twice a minute apart and diff it. A rising nr_throttled against nr_periods, plus growing throttled_usec, proves the CPU quota is being exhausted inside periods even when average usage looks low. docker stats cannot show this.

open as a page

How do you debug a shell-less Docker container from a toolbox container that shares its namespaces?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Start a throwaway container with tools using --pid=container:<target> and --network=container:<target>, so it sees the target's processes and network while running its own shell. The mount namespace is not shared, so read the target's files at /proc/1/root.

open as a page

A Docker container has restarted 47 times overnight — what evidence about the earlier runs survives?

level: seniorimportance: nice to knowfreq 31%

basics

~20 s

A restart policy reuses the same container, so docker logs still holds every earlier run's output, separated only by timestamps. docker inspect describes just the latest run apart from RestartCount, and log rotation may have deleted the first failure.

open as a page

Why does `nproc` in a Docker container report the host's core count when the container was run with `--cpus`?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

nproc reports the CPUs in the process's scheduling affinity mask. --cpus limits CPU bandwidth through the cgroup and leaves affinity untouched, so all host CPUs remain visible. --cpuset-cpus does change affinity, so nproc reflects that one.

open as a page

How do you get a static busybox running inside a Docker container that has no shell, and what breaks?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

Place a statically linked busybox binary into the container's writable layer and exec it directly, for example docker exec -u 0 -it app /busybox sh. It fails if the binary is dynamically linked, built for another architecture, or the destination is read-only or mounted noexec.

open as a page