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: likeunless-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