Your First Container
Measured on Docker Desktop 4.81.0 / Engine 29.6.1, macOS on Apple Silicon (linux/arm64), 2026-09-30.
Interactive container: getting a shell inside Ubuntu
Section titled “Interactive container: getting a shell inside Ubuntu”The -it flag combination is how you open an interactive terminal session inside a container. Run:
docker run -it ubuntu:24.04 bashOnce Docker pulls the image and starts the container, your prompt changes to something like:
root@8de6c159ca01:/#You are now inside the container. The hostname shown (8de6c159ca01) is the container ID. You have a fully isolated Ubuntu userland — its own filesystem, its own process tree — running on the engine’s kernel. What you see inside (captured by piping commands to docker run -i, so no TTY):
$ printf 'hostname\ncat /etc/os-release | head -2\nuname -m\nexit\n' | docker run -i ubuntu:24.04 bash8de6c159ca01PRETTY_NAME="Ubuntu 24.04.5 LTS"NAME="Ubuntu"aarch64Note aarch64: on an Apple Silicon Mac Docker pulled the linux/arm64 variant of the multi-platform ubuntu image. An image that only exists for amd64 runs under emulation — for example docker run --platform linux/amd64 alpine:3.24 uname -m prints x86_64 here, slower, and replaces the local alpine:3.24 with the amd64 variant until you pull the arm64 one again (docker pull --platform linux/arm64 alpine:3.24).
When you are done, type exit to return to your host shell. The container stops the moment the bash process exits.
What the flags mean:
-i— keep stdin open so you can type commands into the container.-t— allocate a pseudo-TTY so the shell prompt renders correctly and handles terminal control sequences.- Together,
-itgives you an interactive shell session inside the container.
The lesson pins ubuntu:24.04 rather than bare ubuntu so the sample output matches what you get; bare ubuntu means ubuntu:latest, which moves.
Detached web server: running Nginx in the background
Section titled “Detached web server: running Nginx in the background”Not every container needs an interactive session. Long-running services such as web servers and databases are started in detached mode with -d, so they run in the background without holding your terminal:
docker run -d --name web -p 8080:80 nginx:1.31-alpineFlag breakdown:
-d— detached mode: the container runs in the background and Docker prints only the container ID.--name web— a name you choose, instead of a random one such aselegant_morse.-p 8080:80— publish ports: map host port 8080 to container port 80. Traffic arriving atlocalhost:8080on your machine is forwarded to port 80 inside the container, where Nginx is listening.
After running this command, open http://localhost:8080 in your browser and you will see the Nginx welcome page. From a terminal, curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080 printed 200 here.
Listing running containers with docker ps
Section titled “Listing running containers with docker ps”To see what is currently running:
docker psReal output (the container was published on 127.0.0.1:18001 in the test, so the host part differs from -p 8080:80):
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESc0d6336221c2 nginx:1.31-alpine "/docker-entrypoint.…" 1 second ago Up Less than a second 127.0.0.1:18001->80/tcp docker-b1-webThe columns tell you everything at a glance: the container ID, which image it was started from, the command running inside it, how long it has been up, its status, the port mapping, and its name.
With a plain -p 8080:80 (no address) Engine 29 publishes on both IPv4 and IPv6, and docker ps and docker port show two entries:
$ docker port web80/tcp -> 0.0.0.0:808080/tcp -> [::]:8080$ docker ps --format 'table {{.Names}}\t{{.Ports}}\t{{.Status}}'NAMES PORTS STATUSweb 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp Up Less than a secondOlder tutorials show only the 0.0.0.0 line.
Stopping a container
Section titled “Stopping a container”Pass the container ID (or name) to docker stop:
docker stop webDocker sends SIGTERM to the main process inside the container (PID 1), giving it up to 10 seconds to shut down gracefully. If the process has not exited by then, Docker sends SIGKILL to force-terminate it. The exit code tells you which happened: 128 + signal number, so 143 is SIGTERM (15) and 137 is SIGKILL (9).
How long that takes depends entirely on PID 1. Three real stops, timed with time docker stop:
nginx:1.31-alpine stop -> 0.31 s exit code 0 # nginx handles SIGTERM and exits cleanlyalpine sleep 600 stop -> 10.16 s exit code 137 # SIGTERM ignored, killed after the timeoutalpine sleep 600 --init stop -> 0.14 s exit code 143 # docker-init forwards SIGTERM, sleep dies from itWhy? PID 1 in a container is special: the kernel gives it no default action for signals it has not installed a handler for. sleep installs none, so as PID 1 it simply ignores SIGTERM and Docker waits the full 10 seconds before SIGKILL. nginx installs a handler, so it exits at once. With --init, Docker runs docker-init as PID 1, which forwards signals to your process (sleep is no longer PID 1, so the default action applies). The Dockerfile module explains how CMD form affects this. You can shorten or lengthen the grace period with docker stop -t <seconds>.
Listing all containers (including stopped ones)
Section titled “Listing all containers (including stopped ones)”Once stopped, a container no longer appears in docker ps. Add the -a flag to see everything, including exited containers:
docker ps -aReal output after a true and an exit 3:
NAMES STATUSdocker-b1-x1 Exited (3) Less than a second agodocker-b1-x0 Exited (0) Less than a second agoThe number is the exit code of the main process. This is useful for inspecting exit codes, recovering logs (docker logs), or cleaning up old containers with docker rm. A stopped container still exists and still owns its name — which is the source of the bug below.
docker run -d --name web -p 8080:80 nginx:1.31-alpine
docker ps
docker stop web
docker ps -a
docker rm webReading AI-generated code
Section titled “Reading AI-generated code”An AI-written “start the dev server” helper, run by developers many times a day:
🐛 Spot the bug
#!/bin/sh# AI: "start (or restart) the local web server"docker run -d --name web -p 8080:80 nginx:1.31-alpineShow the answer
The first run works. Every later run fails, whether or not the container is still running, because the name web is still owned by the old container (a stopped container is not removed). Proven on this Mac (the real container name was longer; shortened to web here):
$ docker run -d --name web -p 8080:80 nginx:1.31-alpine # first time: ok$ docker stop web$ docker run -d --name web -p 8080:80 nginx:1.31-alpinedocker: Error response from daemon: Conflict. The container name "/web" is already in use by container "365d43a0…". You have to remove (or rename) that container to be able to reuse that name.$ echo $?125Exit code 125 means the Docker daemon (not the container) failed. docker ps -a shows Exited (0) — the leftover. Make the script idempotent by removing any previous container first (-f also stops a running one):
#!/bin/shset -eudocker rm -f web >/dev/null 2>&1 || truedocker run -d --name web -p 127.0.0.1:8080:80 nginx:1.31-alpineRunning the fixed script twice left exactly one Up container. If you never need the stopped container afterwards, docker run --rm removes it automatically when it exits (proven: docker stop then docker ps -a -q printed nothing), but --rm alone does not fix a re-run while the first one is still up: the name is still taken.
Mental Model
Section titled “Mental Model”docker run = create a container from an image + start its main process. docker stop = send that process SIGTERM, wait, then SIGKILL. The container’s life is the life of PID 1: when it exits the container stops, and the stopped container (with its name, logs and exit code) stays on disk until you docker rm it. Everything about stopping fast or slow is about how PID 1 treats signals.
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
-it (interactive) | See output in real time, easy to debug | Needs a terminal to stay open; the container stops if the session closes |
-d (detached) | Runs as a background service continuously, does not hold the terminal | Needs docker ps / docker logs to check on it — no immediate output |
--rm | No leftover stopped containers or names | Exit code and logs vanish with the container, harder to debug a crash |
--init | Signals forwarded, zombie processes reaped, fast docker stop | One more process; a program that already handles SIGTERM does not need it |
Common mistakes
Section titled “Common mistakes”- Forgetting
-dand having the container tie up the terminal because it runs in the foreground by accident. - Running
docker runrepeatedly without checkingdocker psfirst, ending up with several containers running the same service — or, with--name, a name conflict on the second run. - Forgetting that a container stops the moment its main process exits (for example, typing
exitin an-itsession), then wondering why it disappeared fromdocker ps(it is indocker ps -a). - Treating a 10-second
docker stopas normal instead of a sign that PID 1 ignores SIGTERM.
Common Misconceptions
Section titled “Common Misconceptions”- “
docker stopdeletes the container.” It stops it; the container remains indocker ps -a, and its name stays reserved, untildocker rm. - “
docker stopalways waits 10 seconds.” It waits up to 10 seconds. nginx stopped in 0.31 s; only a PID 1 that ignores SIGTERM takes the full timeout. - “Exit code 137 means the app crashed.” 137 is 128 + 9: something (here Docker after the timeout, elsewhere the OOM killer) sent SIGKILL.
- “Ubuntu in a container is a different machine.” It is the Ubuntu userland on the engine’s kernel;
uname -mstill saysaarch64on an Apple Silicon Mac.