First hands-on practice: running hello-world and ubuntu containers, understanding the interactive options (-i, -t, --rm, --name), managing containers with ps, stop, start, and rm, interacting via exec and logs, and port mapping to connect containers to the outside world.

After dissecting Docker's architecture in episode 2 — client, daemon, containerd, runc, and the four core components (Image, Container, Volume, Network) — in this episode we get straight to practice. Time to get our hands dirty and run our first container.
Don't underestimate this episode. docker run, docker ps, docker exec, docker logs, and port mapping are commands you'll type hundreds of times a day in the workplace. Engineers who've used Docker for years still live on these commands — the difference is, they understand the meaning behind each option rather than just memorizing them. This episode builds that habit: every command is run while understanding what's happening behind the scenes — following the architecture map we built in episode 2.
In this episode we'll run our first containers (hello-world and ubuntu), understand the interactive options -i, -t, --rm, --name, manage containers with ps, stop, start, rm, interact from inside the container with exec and logs, and close with port mapping that connects containers to the outside world.
Open a terminal and run:
docker run hello-worldIf this is the first time, Docker will automatically pull the hello-world image from Docker Hub, then run it. Notice the following output:
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
[...]
Hello from Docker!
This message shows that your installation appears to be working correctly.Two important things happen here:
hello-world image isn't in the local cache, so the daemon downloads it from Docker Hub, then runs it. On the second execution, the image is already cached — the pull is skipped and the container runs immediately. This is the same flow for every image: "use image → check cache → pull if missing".hello-world is a small binary that only prints text and then stops. Its final status is Exited — and that's normal, not an error. A container always stops when its main process finishes.Also note the image naming convention: hello-world:latest consists of the image name (hello-world) and the tag (latest, the default when none is specified). Tags select the image version — we'll dig deeper in episode 12.
Containers aren't always "print then exit". Many images — like ubuntu — contain a complete system with no meaningful default command. To use them interactively:
docker run -it --rm --name my-ubuntu ubuntu bashLet's break down these options — this is what beginners most often misunderstand:
| Option | Function | Why |
|---|---|---|
-i (interactive) | Keeps STDIN open | Lets you type into the container |
-t (tty) | Allocates a pseudo-terminal | Displays clean output, a shell prompt, color support |
--rm | Auto-removes when the container stops | Prevents "zombie" containers from piling up in ps -a |
--name | Names the container | Replaces the random ID; makes stop/logs/exec easier |
Inside that shell, you're in an isolated Ubuntu environment — with PID 1 as the bash process, your own filesystem, and only minimal packages. Try cat /etc/os-release, ls /, and exit to leave. Because we used --rm, the container is cleaned up as soon as the shell closes.
Tip
The -it combination is always used together for interactive sessions: -i alone without -t produces output without a comfortable prompt, while -t alone without -i makes input feel "stuck". For an interactive shell, both must go together. However, to run a one-off command, just docker run ubuntu echo "hai" — without -it.
Now let's leave the interactive session and learn to manage containers running in the background. Start two containers:
docker run -d --name my-web nginxThe -d (detached) option runs the container in the background — the terminal returns immediately. To see running containers:
docker ps
docker ps -aCONTAINER ID IMAGE COMMAND PORTS NAMES
1f3a9c7b2d5e nginx "/docker-entrypoint.…" 80/tcp my-webThe difference between docker ps and docker ps -a: the first only shows running containers (status Running), while the second shows all containers including stopped ones (Exited). You'll often find Exited containers of unclear origin — those are what we'll clean up in episode 4.
The most basic lifecycle management:
docker stop my-web
docker start my-web
docker stop my-web
docker rm my-webdocker stop sends a SIGTERM signal (with a grace period, 10 seconds by default) so the main process stops cleanly — it doesn't force-kill.docker start brings an existing container back to life (its state is preserved).docker rm deletes a container that has already stopped. To delete a running container, add -f (force) — Docker sends SIGKILL and then removes it.$ docker rm my-web
Error response from daemon: You cannot remove a running container ...The more containers you manage, the more important filtering becomes. Two docker ps options you must master:
docker ps -a --filter "status=exited"
docker ps --filter "name=my-"
docker ps -q--filter "status=exited" shows only stopped containers — a good starting point for cleanup.--filter "name=my-" searches containers by name prefix — useful when you forget the exact name.-q (quiet) shows only container IDs, one per line. This is the raw material for scripting: imagine docker rm $(docker ps -aq --filter "status=exited") — one line that deletes all stopped containers at once. This $(...) pattern will become a close friend in the episodes ahead.One key concept: a container lives only as long as its main process (PID 1 inside the container) is running. When you run docker run -d --name my-web nginx, the nginx image has a default command that runs the nginx server in the foreground — as long as nginx is alive, the container is Up. If that process finishes (or crashes), the container immediately goes Exited. This is why docker run -d ubuntu bash appears to "die instantly": bash without interactive arguments exits right away, and with it the container stops. The main process is the heart of the container.
A detached container can still be "entered" and read from the outside — without disturbing its main process. Two commands you'll use most:
1. docker exec — run a new command inside a running container.
docker run -d --name my-web nginx
docker exec -it my-web bashNote the difference from docker run: run creates a new container; exec enters a container that's already running — just like knocking on the container's door and walking in. Inspect the files inside, for example ls /usr/share/nginx/html, then exit. The nginx image is Debian-based, so bash is available; for the slimmer Alpine images, use sh.
2. docker logs — read the container's output (stdout/stderr).
docker logs my-web
docker logs --tail 20 my-web
docker logs -f my-web--tail N: only the last N lines — saves output for long logs.-f (follow): follows the log in real time, like tail -f — very useful when debugging.Warning
An important rule of thumb: don't install SSH inside a container. A container is designed for a single main process; entering via docker exec is the correct and far lighter approach. If you feel the need for SSH, you're using the wrong pattern — a container that's "like a server" is usually a sign of poor design.
Containers run on an internal network with their own IP that can't be reached directly from the host. To connect them to the outside world, you must map ports with the -p host_port:container_port option:
docker run -d --name web -p 8080:80 nginxNow access it from the host:
curl -I http://localhost:8080HTTP/1.1 200 OK
Server: nginx/1.27.3
Content-Type: text/htmlPort mapping analogy: the container is a restaurant serving its food from a kitchen numbered 80; you install a service door numbered 8080 on the main road, and every order coming in through door 8080 is forwarded to kitchen 80. Open http://localhost:8080 in your browser — you'll see the nginx welcome page. Without -p, the container still runs but can't be reached from the host — a fact that often confuses beginners.
Note how the mapping works: host ports must be unique — you can't have two containers mapping -p 8080:80 at the same time. The port is already allocated error appears when they clash; the fix is to change the host port (e.g. -p 8081:80) or stop the container using it.
A summary of all the approaches above in one code-group — pick according to your needs:
docker run -it --rm --name app-dev ubuntu bashThese three patterns cover almost every need: foreground for interactive experiments and debugging, detached for services that must keep running, one-shot for short jobs like running a utility without leaving a trace. Choose the pattern based on what you're doing, not out of habit.
Forgetting -it for interactive sessions. docker run ubuntu bash appears to "hang" — in reality bash runs without open input. Always use -it for an interactive shell.
Forgetting port mapping. docker run -d nginx succeeds, but curl localhost:80 fails. The container runs on an internal network — without -p, there's no way in from the host.
Using a clashing host port. docker run -p 8080:80 nginx fails with port is already allocated. Check with ss -tlnp or change the host port.
Deleting a running container. docker rm refuses active containers. Stop it first (docker stop), or use -f deliberately.
"Big" images during demos. docker run ubuntu pulls hundreds of MB because the Ubuntu image is a full OS. For light experiments, images like alpine (a few MB) are much faster.
| Mistake | Symptom | Solution |
|---|---|---|
Missing -it | Shell "hangs" without a prompt | Use -it for interactive sessions |
Missing -p | Container runs but can't be reached | Map -p host:container |
| Port clash | port is already allocated | Change the host port / stop the user |
rm on an active container | Error "cannot remove running" | stop first, or rm -f |
Note
Layered debugging: if a service doesn't respond, check in logical order — is the container still running? (docker ps) → is the main process running? (docker exec -it web bash then ps aux) → is the port actually listening? (docker logs web and ss -tlnp on the host). This sequence solves 90% of "can't reach the container" problems.
In episode 3 you've run your first container and mastered the Docker CLI fundamentals: docker run hello-world with the automatic pull flow, docker run -it ubuntu bash with an understanding of the -i, -t, --rm, and --name options, container management with docker ps / ps -a, stop, start, rm (including -f), interaction from within via docker exec and docker logs (-f, --tail), and port mapping -p host_port:container_port that connects containers to the outside world.
The core takeaways:
docker run = pull if needed + create + run; a container stops when its main process exits.-i (input stays open) + -t (pseudo-terminal) = interactive session; --rm for disposable containers; --name for identity.docker ps = active ones; docker ps -a = all; stop (SIGTERM) ≠ kill (SIGKILL).exec enters a running container; logs reads its stdout/stderr.-p, a container runs but can't be reached from the host.These commands are your "hands" — and you've just trained them. In the next episode, episode 4, we'll go deeper into the container lifecycle and state management: the state machine from Created → Running → Paused → Stopped → Exited → Dead, the docker create, pause/unpause, restart, kill commands, inspecting JSON metadata with docker inspect, real-time monitoring with top, stats, and events, and system cleanup with docker system prune. See you in episode 4!