Inspect, logs & exec
Look inside a running container.
A running container is like a washing machine locked in the utility room. It is working away, you can hear it, but you cannot see the drum, and you would rather not switch it off at the wall only to find out what cycle it is on. Docker gives you three ways to check on a live container without stopping it. You can read the notes it keeps pushing under the door. You can open the door and step inside. Or you can read the metal spec plate bolted to its side. Those three moves are docker logs, docker exec, and docker inspect. Try them in that order, because nine times out of ten the answer is already sitting in the logs.
Get a real container running first, so you have something to check on. The command below starts nginx, a very common web server, which is a program whose whole job is handing out web pages when a browser asks for one. Three flags do the work. -d means detached: run it in the background and give me my prompt back. --name web sticks the label web on the container so you never have to type its long ID. And -p 8080:80 wires port 8080 on your own machine through to port 80 inside the container. A port is a numbered door that network traffic goes through, so that flag is you propping open door 8080 on your laptop and running a corridor from it to door 80 inside.
$ docker run -d --name web -p 8080:80 nginx:1.27Unable to find image 'nginx:1.27' locally1.27: Pulling from library/nginxDigest: sha256:287ff8...9eStatus: Downloaded newer image for nginx:1.273f8a1c9d4e2b7a6f0c5d8e1b2a3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c$ docker psCONTAINER ID IMAGE STATUS PORTS NAMES3f8a1c9d4e2b nginx:1.27 Up 3 seconds 0.0.0.0:8080->80/tcp web
Read what it printed: docker logs
Every program chatters while it runs. nginx prints a short startup banner, then one line for every request it answers. That text goes out on two streams with formal names: standard output and standard error, usually shortened to stdout and stderr. Treat them as the program talking out loud. Docker sits there recording both, word for word, and keeps the recording. That is why docker logs can play it back at any time, even long after the container has stopped. Add -f to follow the log live, so new lines land on your screen as they happen (press Ctrl+C when you want to stop watching). Add --tail to see only the last few lines instead of the whole history. In the example below I use curl, a small command-line tool that fetches a web page the way a browser would, to hit the server once so nginx has a request to log.
$ docker logs web/docker-entrypoint.sh: Configuration complete; ready for start up2025/07/16 10:11:02 [notice] 1#1: nginx/1.27.42025/07/16 10:11:02 [notice] 1#1: start worker processes$ curl -s localhost:8080 > /dev/null$ docker logs --tail 1 web172.17.0.1 - - [16/Jul/2025:10:12:40 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/8.5.0"
Logs really earn their keep when a container refuses to stay up. Here is the trap beginners fall into over and over: one small typo in the command. You start the container detached, the command dies on the spot, and the container is gone before you have looked back at the screen. You run docker ps and there is nothing there, which feels like the container evaporated. It did not. docker ps lists containers that are still running, and nothing else. Add -a (short for all) and the stopped ones show up too. Then read the logs and find out what happened.
$ docker run -d --name greeter alpine:3.20 sh -c "ecoh hello"b4d2e1f09a7c8d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e$ docker ps --filter name=greeterCONTAINER ID IMAGE COMMAND STATUS$ docker ps -a --filter name=greeterCONTAINER ID IMAGE COMMAND STATUSb4d2e1f09a7c alpine:3.20 "sh -c 'ecoh hello'" Exited (127) 4 seconds ago$ docker logs greetersh: ecoh: not found
Exited (127) is your clue. An exit code is the number a program hands back on its way out, like a contractor leaving a note about how the job went, and 0 always means it went fine. 127 is the shell's way of saying it could not find the command you asked for. The log says the rest out loud: ecoh was meant to be echo. Fix the spelling and the container runs.
Step inside: docker exec
Sometimes the notes under the door are not enough and you want to walk in and look around yourself. docker exec starts a new command inside a container that is already running. Pair it with -it, which stands for interactive plus terminal, and Docker hands you a shell inside the container. A shell is a command prompt, the same kind of thing you type into on your own machine. This is not docker run. docker run would spin up a brand new container. exec joins the one that is already up. Type exit when you are done and the container carries on running without you.
$ docker exec -it web sh# whoamiroot# head -3 /etc/nginx/conf.d/default.confserver {listen 80;server_name localhost;# exit$
Get a fact: docker inspect
Logs tell you what the container said. exec lets you wander around inside it. Some things are neither, because they are flat facts Docker holds about the container: the address it was handed, which folders from your own machine are shared into it, the exact code it exited with. That address is an IP address (Internet Protocol address), the number that identifies the container on Docker's private network, the way a house number identifies a house on a street. For facts like these, read the spec plate. docker inspect prints the container's whole configuration and live state as JSON (JavaScript Object Notation, a plain-text way of writing structured data). It is long. Nobody reads all of it.
$ docker inspect web | head -11[{"Id": "3f8a1c9d4e2b7a6f0c5d8e1b2a3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c","Created": "2025-07-16T10:11:01.482613814Z","Path": "/docker-entrypoint.sh","Args": ["nginx","-g","daemon off;"],"State": {
So you pull out the one field you want with -f and a Go template. A Go template is a tiny pattern where a pair of double curly braces means 'print this value right here'. You do not need to know the Go programming language to write one. Find the field in the JSON, wrap its path in braces, and inspect fills in the value. This works on stopped containers too, which is how you confirm that greeter really did exit with 127.
$ docker inspect -f '{{ .State.Status }}' webrunning$ docker inspect -f '{{ .NetworkSettings.IPAddress }}' web172.17.0.2$ docker inspect -f '{{ .State.ExitCode }}' greeter127
When something breaks, the reflex is to open the Dockerfile and rebuild. Hold off for sixty seconds. The three commands answer three different questions, and asking the right one first saves you a whole build. What did the process say on its way down? That is logs. What does the world look like from inside, right now? That is exec. What does Docker itself believe about this container's configuration, network, mounts and state? That is inspect.
docker logs is your first call after an unexpected exit. Get the name or ID from docker ps -a, then read the last handful of lines, because the reason a process died is nearly always the last thing it printed. For a service that keeps running, docker logs -f leaves the tap open the way tail -f does on an ordinary file. One catch is worth knowing early. If the application writes its own log file inside the container instead of printing to stdout, docker logs will look almost empty. Nothing mysterious is going on. The image was packaged in a way that hides its own output, and that is an application packaging problem to fix.
docker exec -it name sh (or bash, if that is what the image ships) opens a shell in a running container. It is the right tool for a yes-or-no question. Did that config file land where you expected? Does that hostname resolve? Is the package you assumed was there actually there? It is a poor tool for repairs. Anything you install or edit through exec vanishes the moment the container is replaced, and containers get replaced constantly. Use exec to work out what is wrong, then write the fix into the Dockerfile or a mount so it survives.
docker inspect is dense, and that is fine. Pipe it through a format string, or through jq (a command-line tool for picking values out of JSON), when you want one fact: an IP address, the list of environment variables, where the mounts land. Knowing the data is in there beats memorising field names. Practise pulling .State.Status and .NetworkSettings.IPAddress until the braces stop looking strange, and the rest of that tree turns into something you browse instead of a wall of text.
Try this
Start a named nginx, read its logs, exec in to see which processes are running inside it, then pull one structured field back out with inspect. Five commands, about a minute of your time.
docker run -d --name peek nginx:1.27-alpinedocker logs peek | tail -n 5docker exec peek ps aux | head -n 5docker inspect -f "{{.State.Status}} {{.Config.Image}}" peekdocker rm -f peek
.../docker-entrypoint.sh: Configuration complete; ready for start up...PID USER TIME COMMAND1 root 0:00 nginx: master process nginx -g daemon off;...running nginx:1.27-alpine
Takeaway
Next time a container disappears on you, type docker ps -a before you touch anything else, then docker logs on the name it shows you. The error line and the exit code are already there waiting. Rebuilding first only means you sit through a build and still do not know why it died.