skip to content

Why does raw output from the Docker Engine API's /containers/{id}/logs look corrupted?

level: middleimportance: nice to knowfreq 16%

answer

  1. Nothing is corrupted, something is wrapped
  2. Two streams, one connection
  3. A fixed-size header before each chunk
  4. A TTY makes the framing disappear
  5. 8 bytes: stream number plus big-endian length

basics

~20 s

It is framed, not corrupted. A container created without a TTY has stdout and stderr multiplexed into one stream, each chunk preceded by an 8-byte header holding the stream number and payload length. The docker CLI demultiplexes it.

solid answer

~50 s

It is not corrupted, it is framed. When a container is created with `Tty` false — the default unless you pass `-t` — stdout and stderr still have to stay distinguishable inside a single HTTP response body, so the daemon frames them: byte 0 is the stream number (0 stdin, 1 stdout, 2 stderr), bytes 1-3 are zero padding, bytes 4-7 are a big-endian 32-bit payload length, then that many payload bytes, repeated. Piping that straight from `curl` shows a stray control byte and some NULs every few lines. With `Tty` true there is no framing at all — a pseudo-terminal has already merged the two streams into one, so you get raw bytes and can no longer tell stderr from stdout. The same rule governs the attach and exec-start responses. The Go SDK demultiplexes with `stdcopy.StdCopy`; the docker CLI uses the same code, which is why `docker logs` looks clean.

code

bash · 7 lines
bash
# 8-byte header before each chunk when the container has no TTY
curl -s --unix-socket /var/run/docker.sock \
  'http://localhost/v1.43/containers/pdfsign/logs?stdout=1&stderr=1' \
  | head -c 96 | xxd

# The same logs, demultiplexed for you
docker logs pdfsign

go deeper

for a junior

You are unlikely to be asked this. Knowing that docker logs prints cleanly because the CLI decodes something the raw API does not is already a good answer at this level.

for a middle

Be able to describe the 8-byte header — stream number, padding, big-endian length — and say that it disappears when the container has a TTY because a pseudo-terminal merges stdout and stderr.

for a senior

Show the consequence rather than the trivia: allocating a TTY for a service destroys the stdout/stderr distinction for everything downstream, so keep TTYs for interactive use and leave service containers unframed-by-default.

for a principal

Frame it as a contract between the engine and every tool that reads container output, and be ready to say why a platform standardises on non-TTY service containers so that stream separation survives to the log pipeline.

### The symptom You pull a container's logs straight off the Engine API and the output has junk in it: ``` curl -s --unix-socket /var/run/docker.sock \ 'http://localhost/v1.43/containers/pdfsign/logs?stdout=1&stderr=1' ``` Every few lines there is an unprintable byte, a run of NULs and a few more odd bytes. Nothing is broken. You are looking at Docker's multiplexed stream format, and `docker logs` looks clean only because the CLI decodes it before printing. ### Why the framing exists A container has two output streams, stdout and stderr, and the API returns them over one HTTP response body. Something has to mark where one ends and the other begins, so the daemon wraps each chunk in a small header: - **byte 0** — the stream: `0` stdin, `1` stdout, `2` stderr - **bytes 1-3** — zero padding - **bytes 4-7** — the payload length, a big-endian unsigned 32-bit integer - **then** exactly that many bytes of payload and repeats. A reader that wants the two streams apart reads eight bytes, switches on the first, reads the length, then reads exactly that many bytes before looking for the next header. This is a length-prefixed framing scheme, not a text protocol: the payload can contain anything, including newlines and bytes that look like headers, which is why you cannot recover the streams by scanning for delimiters. ### The TTY case is different The framing appears only when the container was created with `Tty` false. Ask for a TTY — `docker run -t`, or `Tty: true` in the create body — and the daemon allocates a pseudo-terminal for the process. A pty has *one* output side by construction: the process's stdout and stderr are both connected to it, and by the time the daemon sees the bytes there is nothing left to distinguish. So the response is a raw byte stream with no headers, and requesting `stderr=1` cannot separate anything, because the separation was lost inside the container. That is a real tradeoff, not a detail. Consider a PHP-FPM application image for a PDF-signing service that writes access lines to stdout and errors to stderr. Run it without a TTY and a log pipeline can route errors differently from access lines, because the frames say which is which. Run it with `-t` because the colours look nicer interactively, and every error is now indistinguishable from an access line for everything downstream. ### Where else the rule applies The same choice governs `GET /containers/{id}/attach`, its websocket variant, and the response to `POST /exec/{id}/start`. In each case the stream is framed if the container (or the exec instance) has no TTY, and raw if it does. Those endpoints hijack the connection after the response headers, so a client reads the frames directly off the socket. On recent API versions the response's `Content-Type` tells you which you are getting — a raw-stream media type for the TTY case and a multiplexed-stream media type otherwise — which is the robust way for a client to decide, rather than remembering how the container was created. ### How clients handle it The Go SDK ships `stdcopy.StdCopy(dstOut, dstErr, src)`, which reads the frames and writes each payload to the matching destination writer; the docker CLI uses exactly that, which is why `docker logs` and `docker logs 2>/dev/null` behave the way a shell user expects. Other SDKs expose an equivalent demultiplexing option. If you are writing a client by hand the decoder is a dozen lines, and writing it is worth doing once because it makes the format concrete. The wrong fixes are worth naming. Stripping non-printable bytes with `tr` destroys binary-safe payloads and still leaves the length bytes. Splitting on newlines and dropping short lines corrupts multi-line output. Requesting only `stdout=1` does remove the interleaving but still returns framed data — the framing is a property of the stream, not of how many streams you asked for. ### What an interviewer is testing This is a curiosity question rather than a screening one, and it separates people who have written a container-driving tool from people who have only used the CLI. The good answer names the header, explains why it exists, and knows that a TTY removes the framing at the cost of merging stderr into stdout for good.

  • How does the stream change if the container was created with a TTY?
    It becomes a raw byte stream with no headers. The daemon gave the process a pseudo-terminal, which has a single output side, so stdout and stderr were merged before the daemon ever saw them. Asking for `stderr=1` separates nothing in that case, and any downstream routing that depends on telling errors from normal output is lost.
  • Which other Engine API endpoints use this same framing?
    Attach and the exec-start response. Both hijack the connection after the response headers and then carry the container's streams directly, framed when there is no TTY and raw when there is. A client that already demultiplexes logs can reuse the same decoder for an interactive exec session.

saying these in an interview costs you the question

  • Says the log output or the log file is corrupted
  • Thinks the eight bytes are a timestamp
  • Assumes stdout and stderr use separate connections
  • Believes a TTY container's stream is also framed
  • Suggests stripping non-printable bytes with tr
  • Thinks requesting only stdout removes the framing

context