Skip to content

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:

Terminal window
docker run -it ubuntu:24.04 bash

Once 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 bash
8de6c159ca01
PRETTY_NAME="Ubuntu 24.04.5 LTS"
NAME="Ubuntu"
aarch64

Note 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, -it gives 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:

Terminal window
docker run -d --name web -p 8080:80 nginx:1.31-alpine

Flag 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 as elegant_morse.
  • -p 8080:80 — publish ports: map host port 8080 to container port 80. Traffic arriving at localhost:8080 on 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.

To see what is currently running:

Terminal window
docker ps

Real 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 NAMES
c0d6336221c2 nginx:1.31-alpine "/docker-entrypoint.…" 1 second ago Up Less than a second 127.0.0.1:18001->80/tcp docker-b1-web

The 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 web
80/tcp -> 0.0.0.0:8080
80/tcp -> [::]:8080
$ docker ps --format 'table {{.Names}}\t{{.Ports}}\t{{.Status}}'
NAMES PORTS STATUS
web 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp Up Less than a second

Older tutorials show only the 0.0.0.0 line.

Pass the container ID (or name) to docker stop:

Terminal window
docker stop web

Docker 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 cleanly
alpine sleep 600 stop -> 10.16 s exit code 137 # SIGTERM ignored, killed after the timeout
alpine sleep 600 --init stop -> 0.14 s exit code 143 # docker-init forwards SIGTERM, sleep dies from it

Why? 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:

Terminal window
docker ps -a

Real output after a true and an exit 3:

NAMES STATUS
docker-b1-x1 Exited (3) Less than a second ago
docker-b1-x0 Exited (0) Less than a second ago

The 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 web

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-alpine
Show 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-alpine
docker: 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 $?
125

Exit 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):

scripts/start-web.sh
#!/bin/sh
set -eu
docker rm -f web >/dev/null 2>&1 || true
docker run -d --name web -p 127.0.0.1:8080:80 nginx:1.31-alpine

Running 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.

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.

OptionBenefitCost
-it (interactive)See output in real time, easy to debugNeeds a terminal to stay open; the container stops if the session closes
-d (detached)Runs as a background service continuously, does not hold the terminalNeeds docker ps / docker logs to check on it — no immediate output
--rmNo leftover stopped containers or namesExit code and logs vanish with the container, harder to debug a crash
--initSignals forwarded, zombie processes reaped, fast docker stopOne more process; a program that already handles SIGTERM does not need it
  • Forgetting -d and having the container tie up the terminal because it runs in the foreground by accident.
  • Running docker run repeatedly without checking docker ps first, 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 exit in an -it session), then wondering why it disappeared from docker ps (it is in docker ps -a).
  • Treating a 10-second docker stop as normal instead of a sign that PID 1 ignores SIGTERM.
  • “docker stop deletes the container.” It stops it; the container remains in docker ps -a, and its name stays reserved, until docker rm.
  • “docker stop always 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 -m still says aarch64 on an Apple Silicon Mac.
What does the -d flag do in docker run -d?
What does -p 8080:80 mean in docker run -d -p 8080:80 nginx?
Which command lists only the currently running containers?
What signal does docker stop send to the container process first?
docker stop on a container running sleep 600 as PID 1 took about 10 seconds and the exit code was 137. What happened?
A script runs docker run -d --name web nginx:1.31-alpine daily and fails on the second day with Conflict. The container name "/web" is already in use. Why?