Chapter 3: Running Containers
Topic 16

exec, attach, and logs

DebugI/O

Once the Driftwood web container is running detached, you need three different ways back to it, and people constantly reach for the wrong one. docker logs reads what the main process has written to stdout and stderr. docker exec starts a new process inside the running container — a debug shell. docker attach reconnects your terminal to the existing PID 1. Confusing attach with exec is how people accidentally kill a production container with a stray Ctrl-C.

The three are not interchangeable, and the difference comes straight from the PID 1 model in the previous topic. logs and exec never touch PID 1; attach wires your keyboard directly to it. Knowing which one you are running is the difference between inspecting a live service and stopping it.

docker logs — Reading the Main Process

The daemon captures the container's stdout and stderr through its logging driver, and docker logs driftwood-web replays them. -f follows the stream in real time the way tail -f does; --tail 100 limits the output to the last hundred lines; --since 10m bounds it by time. This is the first thing you run when a detached container misbehaves — before you shell in, before you attach, you read what it has already said.

Reading and following the web container's output
$ docker logs --tail 50 driftwood-web        # last 50 lines, then return
$ docker logs -f --since 5m driftwood-web    # follow, last 5 minutes onward
[2024-01-15 10:42:07] gunicorn: GET /bookmarks 200
[2024-01-15 10:42:09] gunicorn: POST /bookmarks 500  ← here's the problem

docker exec — A New Process Inside

docker exec -it driftwood-web bash spawns a second process inside the container's namespaces, giving you a shell running next to the app without touching PID 1. You poke around the filesystem, check environment variables, run a one-off query — and when you exit that shell, the app keeps running, because the shell was never the main process. This is what you want 95% of the time you need to be "inside" a live container.

A debug shell alongside the running app, leaving PID 1 untouched
$ docker exec -it driftwood-web sh
/app # env | grep DATABASE_URL
DATABASE_URL=postgresql://db:5432/driftwood
/app # exit          # the app keeps running — you only killed the shell

docker attach — Reconnecting to PID 1

docker attach wires your terminal to the existing main process's stdin, stdout, and stderr. Because you are now connected to PID 1 itself, a Ctrl-C sends SIGINT to PID 1 and can stop the container. attach is for the rare case where you genuinely need the foreground process's own I/O — a REPL that runs as the main process, for instance. To leave without killing it, use the detach-key sequence Ctrl-P Ctrl-Q rather than Ctrl-C, which signals the process instead of detaching from it.

Why logs Depend on stdout/stderr

docker logs only shows what the process wrote to stdout and stderr — that is all the logging driver captures. An app that writes its logs to a file inside the container shows nothing under docker logs, and people burn time wondering why the command "doesn't work." It works fine; the logs are in a file the driver never sees. This is exactly why the twelve-factor convention of logging to stdout exists, and why the Driftwood web app should write to stdout rather than to a file.

Choosing the Right One

The rule is short: logs to read history, exec to run something new alongside the app, attach only to interact with PID 1 itself. There is one wrinkle — for a distroless or scratch image there is no shell to exec into, and alpine has sh but not bash. When the image has no shell, logs and ephemeral debug containers (Chapter 11) carry the debugging load instead.

Three ways back to a running container
exec
Starts a new process inside the container — a debug shell next to the app. Quitting it leaves PID 1 untouched. What you want 95% of the time.
attach
Connects your terminal to PID 1's own stdin/stdout/stderr. A stray Ctrl-C signals PID 1 and can stop the container; detach with Ctrl-P Ctrl-Q.
logs
Replays the captured stdout and stderr the logging driver recorded. Reads history without touching the process at all.
exec vs attach
  • docker exec — starts a new process (a shell, usually) inside the running container; quitting it leaves the app untouched because it was never PID 1. This is what you want 95% of the time you need to inspect a live container.
  • docker attach — connects to the existing PID 1's terminal; Ctrl-C there sends SIGINT to the app and can stop the container. Use it only when you need PID 1's own stdin/stdout, and leave with the detach keys Ctrl-P Ctrl-Q, never Ctrl-C.
Common Mistakes
  • Using docker attach to "check on" the Driftwood web container and pressing Ctrl-C to leave — that SIGINT goes to PID 1 and stops the container; exec, or the detach-key escape, is what you wanted.
  • Expecting docker logs to show anything when the app writes its logs to a file inside the container — the logging driver only captures stdout and stderr, so file logs are invisible there.
  • Running docker logs without --tail on a container that's been up for days and drowning the terminal in gigabytes of history — bound it with --tail or --since.
  • Trying to docker exec -it … bash into a distroless or alpine image and getting "executable not found" — distroless has no shell at all and alpine ships sh, not bash; the image dictates what you can exec.
  • Letting docker logs output grow unbounded because the default json-file driver has no rotation — the JSON log file fills the host disk over weeks (logging drivers and rotation are Chapter 11).
Best Practices
  • Make the application log to stdout and stderr so docker logs and the logging driver see everything, instead of writing log files inside the container.
  • Use docker exec -it … sh (or bash) for live debugging so you never risk signaling PID 1, and exit freely without affecting the app.
  • Reach for docker attach only when you truly need PID 1's own I/O, and leave with the detach keys Ctrl-P Ctrl-Q rather than Ctrl-C.
  • Bound log reads with --tail and --since, and configure log rotation on the daemon (Chapter 11) so the capture file can't fill the disk.
Comparable tools Podman provides identical logs/exec/attach verbs nerdctl over containerd mirrors them kubectl logs · kubectl exec the multi-host analogs that work the same way per container (Ch12) ctr containerd's low-level CLI, exposing the raw task I/O these wrap

Knowledge Check

How does docker exec -it driftwood-web sh differ from docker attach driftwood-web?

  • exec starts a new process you can quit safely; attach connects to PID 1, where Ctrl-C can stop it
  • attach safely starts a brand-new shell while exec dangerously connects you to the existing main process
  • exec works only on stopped containers, while attach works only on containers that are still running
  • exec clones the container into a separate copy while attach shares the original running container

Why does docker logs show nothing for an app that writes its logs to a file inside the container?

  • The logging driver only captures stdout and stderr — a file inside the container never passes through it
  • docker logs reads log files but only from the /var/log directory, and the app wrote them elsewhere
  • The file is encrypted at rest by the container runtime and docker logs has no way to decrypt and read it
  • The file logs are buffered internally and only get flushed out to docker logs once the container exits

You docker attach to a running container to look around, then press Ctrl-C to get out. What happens?

  • Ctrl-C sends SIGINT to PID 1 and can stop the container — you wanted Ctrl-P Ctrl-Q or exec
  • Ctrl-C cleanly detaches your terminal from the session and leaves the container itself running untouched
  • Ctrl-C opens a fresh sub-shell inside the running container so you can keep poking around in it
  • Nothing happens — Docker deliberately ignores Ctrl-C while you are attached in order to protect the container

Why can't you docker exec -it … bash into a distroless image?

  • A distroless image ships no shell at all, so there's no bash or sh to run — debug via logs instead
  • docker exec is deliberately disabled on distroless images for security and supply-chain hardening reasons
  • Distroless renames the bash binary to dash, so you simply have to call the shell by its new name
  • A distroless image has no running PID 1 process, so there's no namespace for Docker to exec a new process into

You got correct