Learning paths / Containers / Running containers

Logs, exec and restart behaviour

Reading · 6 min · Module 3, lesson 2 of 426 min left in this module

Module 3 · Running containersLesson 2 of 4

Goal: Debug a running container with its logs, a command run inside it, and a restart policy.

Key idea

A container has no window to look into. You debug it three ways: logs show what the app printed, exec runs a command inside it while it runs, and a restart policy decides what Docker does when the app exits. Start with the logs; they answer most questions.

Logs: what the app printed

Docker keeps everything the app writes to standard output and standard error:

docker logs hello-api
2026/09/29 01:49:18 sample-api 1.0.0 listening on :8080

-f follows new lines as they arrive (Ctrl+C stops following, not the container), --tail 20 shows only the last 20, and -t adds a timestamp to each line. An app that writes its logs to a file inside the container shows nothing here, which is why containerized apps print to standard output.

Exec: run a command inside

docker exec runs a second program in a container that's already running, with the same files, variables and network as the app. To open a shell in hello-api:

docker exec -it hello-api sh

-it connects your keyboard and screen, so you'd get a shell prompt inside; exit leaves it and the container keeps running. For a single command, drop -it: docker exec <name> env lists a container's variables.

Here, though, Docker answers:

exec: "sh": executable file not found in $PATH

The lab images, learn-hello-web and learn-sample-api, are distroless: the app and almost nothing else, not even a shell or env. That's deliberate: less inside means less for an attacker to use. Debug them through their logs; in the Try it lesson you'll open a shell in an Alpine container, which has one.

Restart behaviour: when the app exits

By default a container that exits stays stopped: docker ps -a lists it as Exited with the app's exit code. A restart policy changes that:

  • --restart no, the default: never restarted.
  • --restart on-failure:3: restarted when it exits with an error, up to 3 times.
  • --restart unless-stopped: restarted whenever it exits, until you stop it yourself.
  • --restart always: like unless-stopped, and also started again when Docker itself restarts.
docker run -d --name crashy --restart on-failure:3 alpine:3.22 sh -c 'echo starting; exit 1'

docker logs crashy then prints starting four times: the first run and three restarts. A restart doesn't fix a crash; it only buys time. The logs show why it crashed. docker rm -f crashy removes it when you're done.

Check yourself

Your app runs in a container from a distroless image and returns errors. docker exec -it app sh fails. What do you check first?

In the docs