Skip to main content

🐳 Lesson 3: Your First Containers

Stop reading about containers and start running them. This is the hands-on lesson where docker run, ps, logs, exec, and the full container lifecycle become second nature.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Run containers with docker run, decoding every flag in -d --name web -p 8080:80 nginx
  • List, inspect, and monitor containers with docker ps, inspect, stats, and top
  • Read logs and get a shell inside a running container using docker logs and docker exec
  • Manage the container lifecycle — stop, start, restart, restart policies — and clean up safely

Estimated Time: 45–55 minutes

Prerequisite: Lessons 1–2 (you understand images vs containers and the Docker engine).

In This Lesson

🚀 The docker run Anatomy

Almost everything you do with Docker starts with one command: docker run. It takes an image, creates a fresh container from it, and starts it. Let's run a real web server — nginx — and dissect every part of the command.

docker run -d --name web -p 8080:80 nginx

That one line packs four decisions. Here is what each piece is telling Docker:

Breakdown of the docker run command flags and arguments docker run -d --name web -p 8080:80 nginx -d = detached Run in the background, return the prompt to you. --name web A friendly handle so you need not memorize the ID. -p 8080:80 Map host port 8080 to container port 80. nginx The image[:tag]. No tag means :latest by default. image[:tag] is always the LAST argument — flags come before it
Figure 1: Every docker run reads as flags first, then the image, then any command to run inside.

Run it, and Docker prints the new container's long ID and hands your prompt straight back:

Output:

Unable to find image 'nginx:latest' locally
latest: Pulling from library/nginx
a2318d6c47ec: Pull complete
Digest: sha256:5c5a0a2f2a0c8f5b8e3d1f6b0e9c2d4a7b1e3f5a9c8d7e6b4a2c1d3e5f7a9b0c
Status: Downloaded newer image for nginx:latest
9f1c2b3a4d5e6f7089a1b2c3d4e5f60718293a4b5c6d7e8f9012a3b4c5d6e7f8

Now open http://localhost:8080 in your browser — you'll see the "Welcome to nginx!" page, served from inside a container that didn't exist ten seconds ago. No install, no config files, no system services.

How port mapping works

The container listens on port 80 in its own private network namespace. That port is invisible to your machine until you publish it. -p 8080:80 tells Docker: "forward traffic arriving at host port 8080 to container port 80." The format is always -p HOST:CONTAINER.

Host port 8080 is forwarded by Docker to container port 80 where nginx listens 🌐 Browser localhost:8080 Your Computer (host) Host port 8080 📦 web nginx Container port 80 -p 8080:80
Figure 2: Without -p, the container's port 80 stays private. Publishing it bridges a host port to the container port.
💡 Tip: The two ports don't have to match. -p 3000:80 would serve the same nginx on localhost:3000. Use different host ports to run several copies of the same image side by side — -p 8081:80, -p 8082:80, and so on.

🎛️ Detached, Foreground & Interactive

A container runs a single main process. How you attach to that process is your choice, and it changes what your terminal does.

Foreground (the default)

Without -d, the container runs in the foreground and streams its output to your terminal. Your prompt is occupied until the process ends (press Ctrl+C to stop it):

docker run -p 8080:80 nginx

Output:

/docker-entrypoint.sh: Configuration complete; ready for start up
2024/11/18 14:02:11 [notice] 1#1: nginx/1.27.2
2024/11/18 14:02:11 [notice] 1#1: start worker processes
172.17.0.1 - - [18/Nov/2024:14:02:30 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/8.5.0"

Detached (-d)

Add -d and the container runs in the background. Docker prints the container ID and returns your prompt so you can keep working. This is how you run long-lived services like web servers and databases.

Interactive (-it)

Sometimes you want a shell inside a container to poke around. The -it combo does that: -i keeps STDIN open (interactive) and -t allocates a pseudo-TTY (a proper terminal). Pair it with a shell command like bash:

docker run -it ubuntu bash

Output:

root@a3f5d2c81b4e:/# cat /etc/os-release | head -1
PRETTY_NAME="Ubuntu 24.04.1 LTS"
root@a3f5d2c81b4e:/# whoami
root
root@a3f5d2c81b4e:/# exit
exit

You dropped into a root shell inside an isolated Ubuntu environment, ran commands, and exit ended the shell — which stopped the container, because bash was its main process. When the main process exits, the container stops.

Auto-clean with --rm

Every container you run leaves behind a stopped container record (you'll see them with docker ps -a). For throwaway experiments, add --rm so Docker deletes the container automatically the moment it stops:

docker run --rm -it ubuntu bash

📖 Flag cheat sheet

  • -ddetached, run in the background
  • -itinteractive + TTY, for a shell you can type into
  • --rmauto-remove the container when it stops
  • --name — give the container a memorable name
  • -p HOST:CONTAINERpublish a port
  • -e KEY=value — set an environment variable inside the container
graph TD A["docker run ⟨image⟩"] --> B{"Which flags?"} B -->|"-d"| C["Detached: runs in background, prompt returns"] B -->|"no -d"| D["Foreground: logs stream, Ctrl+C stops"] B -->|"-it + shell"| E["Interactive shell inside the container"] B -->|"--rm"| F["Container auto-deleted when it stops"] style A fill:#bfdbfe,color:#1e293b

🔍 Listing, Inspecting, Logs & Exec

Once containers are running, you need to see them, understand them, and reach inside. This is your day-to-day toolkit.

Listing: docker ps

docker ps lists running containers. Add -a to include stopped ones too:

docker ps

Output:

CONTAINER ID   IMAGE     COMMAND                  CREATED         STATUS         PORTS                  NAMES
9f1c2b3a4d5e   nginx     "/docker-entrypoint.…"   2 minutes ago   Up 2 minutes   0.0.0.0:8080->80/tcp   web
docker ps -a

Output:

CONTAINER ID   IMAGE     COMMAND                  CREATED         STATUS                     PORTS                  NAMES
9f1c2b3a4d5e   nginx     "/docker-entrypoint.…"   2 minutes ago   Up 2 minutes               0.0.0.0:8080->80/tcp   web
a3f5d2c81b4e   ubuntu    "bash"                   5 minutes ago   Exited (0) 3 minutes ago                          nostalgic_khorana

Notice the exited ubuntu container — that's the leftover from our interactive session. Docker auto-generates a whimsical name (nostalgic_khorana) when you don't supply --name.

Inspecting: docker inspect

docker inspect dumps everything Docker knows about a container as JSON — its IP address, mounts, environment, restart policy, and more. Use --format to pull out just what you need:

docker inspect --format '{{.NetworkSettings.IPAddress}}' web

Output:

172.17.0.2

Monitoring: docker stats and docker top

docker stats is a live, top-style stream of CPU, memory, and network usage per container:

docker stats --no-stream

Output:

CONTAINER ID   NAME   CPU %   MEM USAGE / LIMIT   MEM %   NET I/O       BLOCK I/O   PIDS
9f1c2b3a4d5e   web    0.00%   3.9MiB / 7.6GiB     0.05%   1.2kB/0B      0B/0B       3

docker top shows the processes running inside a container, straight from the host:

docker top web

Output:

UID    PID     PPID    C   STIME   TTY   TIME       CMD
root   14231   14210   0   14:02   ?     00:00:00   nginx: master process nginx -g daemon off;
101    14290   14231   0   14:02   ?     00:00:00   nginx: worker process

Logs: docker logs

A detached container still writes to STDOUT/STDERR — Docker captures it. docker logs replays it, and -f ("follow") streams new lines live, just like tail -f:

docker logs -f web

Output:

2024/11/18 14:02:11 [notice] 1#1: start worker processes
172.17.0.1 - - [18/Nov/2024:14:05:44 +0000] "GET / HTTP/1.1" 200 615 "-" "Mozilla/5.0"
172.17.0.1 - - [18/Nov/2024:14:05:44 +0000] "GET /favicon.ico HTTP/1.1" 404 555 "-" "Mozilla/5.0"

Press Ctrl+C to stop following — that only detaches your log view; the container keeps running.

Getting a shell: docker exec

Here's the one you'll reach for constantly. docker exec runs a new command inside an already-running container. Combine it with -it and a shell to jump inside a live container and look around — without disturbing its main process:

docker exec -it web bash

Output:

root@9f1c2b3a4d5e:/# ls /usr/share/nginx/html
50x.html  index.html
root@9f1c2b3a4d5e:/# nginx -v
nginx version: nginx/1.27.2
root@9f1c2b3a4d5e:/# exit
exit
⚠️ exec vs run: docker run creates a brand-new container from an image. docker exec reaches into an existing, running container. Exiting an exec shell does not stop the container — you only ended the extra process you started.

♻️ Lifecycle, Restart Policies & Cleanup

A container moves through a predictable set of states. Learn the verbs that move it between them and you're in full control.

stateDiagram-v2 [*] --> Created: docker create Created --> Running: docker start Running --> Paused: docker pause Paused --> Running: docker unpause Running --> Stopped: docker stop Stopped --> Running: docker start Running --> Stopped: main process exits Stopped --> Removed: docker rm Removed --> [*]

The lifecycle verbs

CommandWhat it does
docker stop webGraceful shutdown — sends SIGTERM, then SIGKILL after a grace period (10s)
docker start webRestart a stopped container, keeping its config and data
docker restart webStop then start in one step
docker pause webFreeze all processes (they stay in memory, using no CPU)
docker unpause webResume a paused container exactly where it left off
docker rm webDelete a stopped container permanently
docker rm -f webForce-remove a container even while it's running (stop + remove)
docker stop web
docker start web
docker restart web

Output:

web
web
web

Each command echoes the name (or ID) it acted on. To remove a container you must stop it first — or use -f to do both at once:

docker rm -f web

Restart policies: keep it running

For real services you want the container to come back automatically after a crash or a host reboot. The --restart flag sets that policy at run time. The most useful for a personal server is unless-stopped:

docker run -d --name web --restart unless-stopped -p 8080:80 nginx
PolicyBehavior
noDefault — never restart automatically
on-failure[:max]Restart only if it exits with a non-zero code (optionally cap the retries)
alwaysAlways restart, including after a Docker daemon / host reboot
unless-stoppedLike always, but respects a manual docker stop — won't come back until you start it

Cleanup: reclaim disk safely

Stopped containers pile up. docker container prune removes all of them in one sweep (it asks for confirmation first):

docker container prune

Output:

WARNING! This will remove all stopped containers.
Are you sure you want to continue? [y/N] y
Deleted Containers:
a3f5d2c81b4e9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b

Total reclaimed space: 12.4kB
⚠️ Danger: docker system prune -a is the nuclear option. It deletes all stopped containers, all unused networks, and every image not currently used by a running container — including base images you'll have to re-download. Read the confirmation prompt before typing y; on a busy machine it can wipe out gigabytes you meant to keep.

🔎 Modern tooling note

As your projects grow you'll manage multiple containers together with docker compose (Compose v2 — always spelled as a space, never the old docker-compose hyphenated binary). And to check an image for vulnerabilities, today's command is docker scout cves nginxdocker scan was retired. We cover both in later modules; for now, just know the current names.

🏋️ Hands-On Lab

Time to wire it all together. This lab has you run a web server, verify it, step inside it, read its logs, and clean up — the exact loop you'll repeat thousands of times.

🏋️ Exercise: Run, reach into, and retire a container

Objective: Practice the full run → inspect → exec → logs → stop → remove cycle.

Instructions:

  1. Run nginx detached, named lab, mapping host port 8088 to container port 80.
  2. Confirm it's running with docker ps.
  3. Hit it from the command line with curl http://localhost:8088 (or open it in a browser).
  4. Get a shell inside with docker exec and confirm the nginx version.
  5. View the access log entry your curl request produced.
  6. Stop the container, then remove it.
💡 Hint

Remember the flag order: options first, image last. For step 5, the request you made in step 3 shows up in docker logs. You can collapse the last two steps into one with docker rm -f lab.

✅ Worked Solution
# 1. Run nginx detached, named, with a port mapping
docker run -d --name lab -p 8088:80 nginx

# 2. Confirm it is up
docker ps

# 3. Request the page from the terminal
curl http://localhost:8088

# 4. Get a shell inside and check the version
docker exec -it lab bash
#   root@...:/# nginx -v
#   nginx version: nginx/1.27.2
#   root@...:/# exit

# 5. Read the logs — your curl request is there
docker logs lab

# 6. Stop and remove (or: docker rm -f lab)
docker stop lab
docker rm lab

What you should observe: curl returns the nginx welcome HTML; the exec shell reports the nginx version and exiting it leaves the container running; docker logs shows your GET / HTTP/1.1 200 line; and after rm, the container no longer appears in docker ps -a. That end-to-end loop is the heartbeat of working with Docker.

Quick Quiz

Question 1: In docker run -d --name web -p 8080:80 nginx, what does -p 8080:80 do?

Question 2: You need a shell inside a container that is already running. Which command do you use?

Question 3: Which restart policy brings a container back after a crash or host reboot, but respects a manual docker stop?

Summary

🎉 Key Takeaways

  • docker run reads flags-first, image-last-d detaches, --name labels, -p HOST:CONTAINER publishes a port, and the image (with an implied :latest) comes at the end.
  • Choose your attachment mode: foreground streams logs, -d runs in the background, -it gives you a shell, and --rm auto-cleans throwaway containers.
  • See and reach inside: docker ps -a lists everything, inspect/stats/top reveal details, logs -f follows output, and exec -it … bash drops you into a running container.
  • Command the lifecycle: stop, start, restart, pause, and rm move a container between states; --restart unless-stopped keeps services alive; container prune cleans up — and treat system prune -a with respect.

📚 Additional Resources

🚀 What's Next?

You can now run and manage containers built from existing images. Next, you'll learn to build your own images with a Dockerfile — turning your application into a portable container you control from the ground up.

🎉 Lesson 3 complete!

Running containers is now muscle memory. Time to build your own images!