Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

By updated

Improve this page

Logo Docker

A container runs on a machine, and nothing on that machine says anything about it: no configuration file, no startup script, no trace in the shell history. This is the normal situation, not a documentation accident. The information you are looking for exists, but it is held by the container engine (the Docker service that creates and supervises containers), which does not publish it on its own: you have to ask for it. This lesson teaches the four questions you ask a running container first, the command that answers each one, and what stopping the container makes disappear for good.

All the outputs on this page were taken on Docker Engine 29.9.2.

What you will learn

  • Read a container's output with docker logs, from its start
  • Get its IP address with docker container inspect, in a form that does not break
  • Tell the container port from the host port with docker port
  • Copy a file out of the container with docker cp, and check that the copy is faithful
  • Recognize what a stop destroys: the address and the port table, but neither the logs nor the files

Prerequisites

A running Docker engine, obtained from the Docker installation guide, and the three basic verbs covered in the Docker command line: start, stop, delete.

  • An account allowed to talk to the engine, through the docker group or through sudo
  • A terminal, and no other tool: everything is done with the docker client

Why a running container leaves almost nothing on the host

A container does not write its configuration on the host, because it has none in the classic sense. What an administrator used to system services would go looking for in /etc exists nowhere: the container was created by a call to the engine, with arguments passed on a command line that vanished along with the shell that typed it. The engine, for its part, kept everything in its own database, under /var/lib/docker, in a format not meant to be read by hand.

This shift is a frequent source of blocking for a beginner. The "I look for the file" reflex returns nothing, and the lack of a result is easily read as a lack of information. It is not: the information is complete, it is simply held by a process rather than by a file system. Changing reflex means going from "where is the file" to "which command returns this value".

During an incident, four questions come up in this order, and each has its command: what did the container say when it started, which address did it get, through which port can it be reached, and what has it written since? The four sources are disjoint: none gives the answer to another, which is why you have to know all four.

Where does each answer live, and which command returns it?

The diagram below reads from left to right. The dotted link to the host's files is the one that returns nothing, and it is the one we take spontaneously. The other four start from the engine, and each ends on a concrete value, taken during a different run from the outputs below: addresses and ports vary from one launch to the next.

Four questions about a running container get nothing from the host's files, and everything from the container engine: its logs via docker logs give the token announced at startup, its full state via docker container inspect gives an address on the default network, its port table via docker port gives host port 32768, and its writable layer via docker cp brings out the file written after startup.nothingits logsits full stateits port tableits write layerFour questions about arunning containerHost files:/etc, /opt, shell historyThe container enginedocker logsdocker container inspectdocker portdocker cpThe token announcedat startupThe container address172.17.0.6The host port32768The file writtenafter startup
The main path starts from the engine: it holds the four answers. Source · SVG
Esc to close, + and − to zoom, scroll to move around. 100 %

The examples that follow all use the same container named courtier, which you can launch yourself. Its script draws a random opening token, announces it on its output, writes an audit file, then waits; the -p 0:80 option asks the engine to publish the container's port 80 on a free host port.

Terminal window
docker run -d --name courtier -p 0:80 alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a sh -c '
jeton=$(head -c 6 /dev/urandom | sha256sum | cut -c1-12)
mkdir -p /var/log/courtier
date +%s > /var/log/courtier/audit.log
echo "courtier: demarrage"
echo "courtier: 2 files ouvertes"
echo "courtier: jeton-d-ouverture=$jeton"
echo "courtier: pret, en attente"
exec sleep infinity'

The container starts in the background; the outputs below show random values (token, address, port) that will be different on your machine, only their shape matters.

What does a container say at startup, and where do you read it?

docker logs returns everything the container wrote to its standard output and its error output since it started. It is the first command to type, because a service almost always says at startup what it read, what it found, and what it is missing.

Terminal window
docker logs courtier
courtier: demarrage
courtier: 2 files ouvertes
courtier: jeton-d-ouverture=cf8f20d6ae43
courtier: pret, en attente

This token appears in no configuration. It is not in the image, nor in a file, nor in the command that launched the container: the process drew it at random at startup and only announced it. The container's output is the only place to read it, which is exactly why you start there.

Two options change how you read a chatty container:

Terminal window
# The last two lines, timestamped by the engine
docker logs --timestamps --tail 2 courtier
2026-11-01T22:45:10.997237672Z courtier: jeton-d-ouverture=cf8f20d6ae43
2026-11-01T22:45:10.997238934Z courtier: pret, en attente

The timestamp comes from the engine, not from the program: it is therefore present even on a service that does not timestamp its own lines, which lets you place an incident in time. --follow follows the output live, and --since 10m returns only the last ten minutes.

With the default logging driver (json-file), the engine keeps these lines in a file it maintains itself on the host, and inspecting the container gives its path:

Terminal window
docker container inspect courtier --format '{{.LogPath}}'
/var/lib/docker/containers/7a3cb00b8e3a5c1203ab4098f7e11c02f1b409b8b2d43fb4258e9dfa83bc2312/7a3cb00b8e3a5c1203ab4098f7e11c02f1b409b8b2d43fb4258e9dfa83bc2312-json.log

This file can be read, but it belongs to root: an account that is a member of the docker group gets a Permission denied error on it while docker logs answers it just fine. The path remains useful when you want to measure the size of the logs, or read them from a backup of the host.

Which address did the engine give the container?

The engine assigns an IP address to the container when it creates it, and only inspecting the container says which one. This address is neither in the image nor in the launch command, and it is reassigned at every creation: it is not memorized, it is asked for.

docker container inspect returns a full JSON document, several hundred lines long, holding the state, the requested configuration, the mounts and the network. You do not read it whole: the --format option applies a template (the template syntax of the Go language) and returns only the value you want.

Terminal window
docker container inspect courtier \
--format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
172.17.0.2

The range goes through the networks the container is attached to, and returns the address on each. On a container attached to the default network, there is only one, hence a single line. To see the network name at the same time, which becomes necessary as soon as a container has several:

Terminal window
docker container inspect courtier \
--format '{{range $reseau, $conf := .NetworkSettings.Networks}}{{$reseau}} {{$conf.IPAddress}}{{end}}'
bridge 172.17.0.2

Here is what changed, measured on the same machine. NetworkSettings now carries only four keys, SandboxID, SandboxKey, Ports and Networks: the old flat fields IPAddress, Gateway and MacAddress were moved under Networks.<network name>. Engine 29.9.2 serves API 1.56: asking for API 1.51 through the socket, NetworkSettings.IPAddress is still present, and it disappears at API 1.52. An older engine, whose API predates 1.52, therefore still accepts both forms.

The variant that names the network, .NetworkSettings.Networks.bridge.IPAddress, only answers on the default network, called bridge: on a network you created, the name changes and the value is missing. This is why the range, which goes through all the networks without naming them, remains the form to remember.

The rest of the inspection is read with the same mechanism, and two values are worth knowing right away:

Terminal window
# The container state, and the PID of its main process on the host
docker container inspect courtier --format '{{.State.Status}} {{.State.Pid}}'
running 6806

This number is a PID, the process identifier assigned by the host: the container's process is an ordinary process of the system, visible in ps. It is the number seen from the host: inside the container, that same process is PID 1. The lesson Namespaces and cgroups: measure a container's isolation details this separation. docker top gives the same information in a more readable form:

Terminal window
docker top courtier
UID PID PPID C STIME TTY TIME CMD
root 6806 6782 0 22:45 ? 00:00:00 sleep infinity

Which host port is the container published on?

A published container exposes one of its ports on a host port, and the two numbers have no reason to be the same. This is a common confusion, and it has a simple consequence: a container listening on its port 80 is not necessarily reachable on port 80 of the machine, the host-side number being either chosen by you or drawn by the engine.

Terminal window
docker port courtier
80/tcp -> 0.0.0.0:32768
80/tcp -> [::]:32768

You read from left to right: 80/tcp is the port of the container, 32768 is the port of the host. Two lines appear because publishing happens on both address families, IPv4 then IPv6, with the same host-side number. To query a single mapping:

Terminal window
docker port courtier 80/tcp
0.0.0.0:32768
[::]:32768

This high number is no accident. The container was launched with -p 0:80, which asks the engine to pick a free port in the system's ephemeral range itself: nobody wrote it, and it changes at every creation. The same value is read in the port table of the inspection, which is handy in a script:

Terminal window
docker container inspect courtier \
--format '{{(index .NetworkSettings.Ports "80/tcp" 0).HostPort}}'
32768

And it is confirmed from the system side, without going through the engine, since it is indeed the host that listens:

Terminal window
ss -ltn | grep 32768
LISTEN 0 4096 0.0.0.0:32768 0.0.0.0:*
LISTEN 0 4096 [::]:32768 [::]:*

Three paths, a single value: that is the sign the measurement is good. Port publishing, its pitfalls and its listening restrictions are the subject of the lesson on publishing a port.

How do you get a file the container wrote out of it?

docker cp copies a file between the container and the host, in both directions, without stopping the container. It is the gesture you make to take away a log, a fingerprint or a dump before destroying a container, because everything the container wrote outside a volume (a storage space managed separately from the container) disappears with it.

The command also works on a stopped container, in both directions. A file copied out of an exited container arrives intact, and a file copied into it is still there at its next start. Files are therefore among the answers that a stop does not destroy.

You first spot the files born after startup, because they are in no image:

Terminal window
docker diff courtier
C /var
C /var/log
A /var/log/courtier
A /var/log/courtier/audit.log

A marks an addition, C a change, and D a deletion. This list is that of the container's writable layer, that is the modifiable layer the engine places on top of the image's read-only layers, and the only one that belongs to it alone: it says exactly what has moved since launch. The copy is then done by naming the container, a colon, then the internal path, the destination here being the host's current directory:

Terminal window
docker cp courtier:/var/log/courtier/audit.log audit.log

A silent transfer is not proof, and the check fits on one line: you read the file again in the container and compare it to the copy.

Terminal window
docker exec courtier cat /var/log/courtier/audit.log | diff - audit.log

An empty output, hence a zero return code, means the two contents are identical byte for byte. docker exec here runs an extra command inside the running container, without disturbing it: the docker exec -it courtier sh command opens a shell, useful to explore, useless to retrieve a file.

What stopping a container makes disappear

Stopping a container to dig into it destroys two of the four answers. The engine keeps neither the IP address nor the port table of a stopped container: these values belong to its active network, which is torn down with it. The measurement is done on a second container, temoin, which also publishes its port 80 on a free host port and does nothing but wait:

Terminal window
docker run -d --name temoin -p 0:80 alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a sleep infinity

While it runs, the inspection returns its address (here [172.17.0.3]) and docker port temoin 80/tcp a host port (here 0.0.0.0:32773 then [::]:32773). You then stop it with docker stop temoin, and ask the same questions:

Terminal window
docker container inspect temoin \
--format '{{range .NetworkSettings.Networks}}[{{.IPAddress}}]{{end}}'
[invalid IP]
Terminal window
docker port temoin 80/tcp
no public port '80/tcp' published for temoin

The inspection does show a network entry, but its address is empty: docker container inspect temoin --format '{{json .NetworkSettings.Networks}}' reads "IPAddress":"" there. The template, for its part, does not handle that string but the engine's IP address type, whose zero value prints as invalid IP instead of printing nothing. The port table is an empty object ({}), and docker port exits with code 1. docker start brings the container back, but the engine then reassigns these values, and nothing guarantees they are the old ones. Measured here, on the default bridge: the host port changed (32773 then 32774), the address came back identical (172.17.0.3) because nobody had taken it in the meantime, and it changed (172.17.0.4) when another container had been launched during the stop. Noting an address and then restarting therefore means holding a value that may be stale.

The logs, however, survive the stop: docker logs still answers on a stopped container, since the engine wrote those lines to its own file as they came. The container's files survive too, since docker cp still copies them out of a stopped container. The rule to remember fits in one sentence: you query a live container, you do not stop it to dig into it.

Troubleshooting: the four common errors

Each row links a symptom met with the commands of this lesson to its cause and to the gesture that fixes it.

SymptomCauseSolution
docker logs returns nothingThe program writes to a file, not to its standard outputdocker exec <name> cat <path>, after spotting the file with docker diff
map has no entry for key "IPAddress"The flat field was removed from NetworkSettings on recent enginesSwitch to {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}
The service does not answer on the expected portThe container port was mistaken for the host portdocker port <name> and read the number on the right of the arrow
docker port exits with an error, the address is empty or prints invalid IPThe container was stopped before being querieddocker start <name>, then read all the values again

How do you clean up after the measurements?

Two containers remain, courtier running and temoin stopped, and a file lingers in the current directory: docker rm -f courtier temoin deletes the former, rm audit.log the latter. The -f option stops a running container before deleting it, which saves a prior docker stop, and the file copied by docker cp is an ordinary host copy, which the engine never deletes on its own.

Terminal window
docker rm -f courtier temoin
rm audit.log

The docker rm command prints the names of the deleted containers (courtier then temoin), and docker ps -a --filter name=courtier --filter name=temoin then returns only the table header.

Practice

The lab puts you in front of a container delivered by a contractor who left without documentation: it runs, and that is all that is known about it. The validation does not look at any typed command. It queries the engine at grading time, recomputes the four expected values, then compares them to what you wrote: the token announced at startup, the address given by the engine, the host port, and the file written after startup, copied out of the container, which must be identical byte for byte to the one it holds. The four values are drawn at random at each run, so nothing can be copied from a previous attempt, and a container stopped in order to dig into it costs two checks out of four.

Key points

  • A running container leaves almost nothing on the host: the information is held by the engine, and you get it by asking with a command.
  • docker logs returns everything the container wrote to its outputs since startup, and it is often the only place where a startup value can be read.
  • docker container inspect --format extracts a precise value from the state document. For the IP address, the portable form is {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}: the flat field .NetworkSettings.IPAddress has disappeared on Docker 29.9.2.
  • docker port tells the container port, to the left of the arrow, from the host port, to the right. The two can differ, and with random publishing they always differ.
  • docker cp copies a file out of the container without stopping it, and docker diff tells which ones were written since launch.
  • A stop destroys the address and the port table, which the engine does not keep, whereas the logs and the files survive. A restart reassigns these values: the port changes, the address may change, and the old readings are worthless.

Knowledge check

Check that the essentials of this guide stuck. The questions cover only what has just been explained here.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn