Run Your First Docker Containers
Pull an official image from Docker Hub and run, inspect and tear down real containers from the command line. Learn the image-vs-container mental model by doing — the foundation every other Docker skill builds on.
Problem
"Works on my machine" is the oldest bug in software. Your app runs locally, then breaks in staging because the Python version, a system library or an environment variable is different. Docker kills this class of problem by packaging an application together with everything it needs into an **image**, and running it in an isolated **container** that behaves the same everywhere. But before you can containerize your own apps, you need fluency with the basic loop: find a trustworthy image, pull it, run it as a container, look inside a running container, read its logs, and clean it all up. Skip this and every later topic (Dockerfiles, Compose, networking, volumes) feels like magic you can't debug. In this lab you will run real containers from official images, expose a web server to your browser, exec into a live container, and manage the full lifecycle — so the vocabulary becomes muscle memory.
Objectives
- Explain the difference between an image (the blueprint) and a container (a running instance)
- Find and evaluate a trustworthy image on Docker Hub, preferring official images and pinned tags over
latest - Pull an image with
docker pulland list what you have withdocker images - Run a container in the background with
docker run, publishing a port so your browser can reach it - Inspect running containers with
docker ps, read their output withdocker logs, and get a shell inside withdocker exec - Manage the full lifecycle: stop, start, remove containers and remove images, leaving a clean host
Prerequisites
- Docker installed and running (Docker Desktop on Windows/macOS, or Docker Engine on Linux). Run
docker version— if it prints a Client and a Server section, you are ready. - A terminal (PowerShell, bash or zsh) and a web browser
- Internet access to reach Docker Hub for the image pulls
- Git installed, to submit your deliverable at the end
- No prior Docker knowledge required — this is the entry point of the Docker track
What you will build
You won't write any application code in this lab. Instead you will run a real web server
(nginx) inside a container, reach it from your browser, look inside the running container, and
then clean everything up — using only the Docker CLI. By the end, the core commands
(pull, run, ps, logs, exec, stop, rm) will feel routine.
The one mental model: image vs container
Almost every Docker confusion traces back to blurring these two words. Keep them sharp:
| Concept | What it is | Analogy |
|---|---|---|
| Image | A read-only, immutable package: app + runtime + libraries + config, built in layers | A recipe, or a class in code |
| Container | A running instance of an image, with its own writable layer, isolated from the host | The cake baked from the recipe, or an object instantiated from a class |
One image can spawn many containers, the same way one class can create many objects. The
image never changes when a container runs — the container gets its own thin writable layer on
top. This is why containers are cheap to create and throw away, and why the same image behaves
identically on your laptop and in production.
Where images come from
Images live in registries. The default public registry is Docker Hub
(hub.docker.com). When you docker pull nginx, Docker fetches it from Docker Hub unless you
tell it otherwise. Other registries exist (Amazon ECR, Google GCR, Azure ACR) for private or
cloud-integrated images, but Docker Hub is where you'll start.
How to work through this lab
Do the steps in order — each command builds on the last. Run every command yourself and read
its real output; don't just skim. Along the way you'll collect the outputs you need for the
submission in Step 6.
Steps
-
Confirm Docker is running and understand the split
Before pulling anything, confirm your engine is alive and lock in the image-vs-container idea.
Check the installation
Run:
docker versionYou should see two blocks:
Client(the CLI you type into) andServer/Engine
(the daemon that actually runs containers). If the Server block is missing or you get a
"cannot connect to the Docker daemon" error, start Docker Desktop (Windows/macOS) or run
sudo systemctl start docker(Linux) and try again.Now run:
docker infoNote the line reporting how many images and containers you currently have — probably zero of
each on a fresh install. You'll watch these numbers change through the lab.The architecture in one sentence
You type commands into the Docker client; it sends them to the Docker daemon, which
pulls images from a registry and runs them as containers.Checkpoint
Answer these before continuing:
- If you run the same image three times, how many images and how many containers exist? (One image, three containers.)
- Does running a container change the image it came from? (No — the image is immutable; the container gets its own writable layer.)
- Where does
docker pull nginxdownload from by default? (Docker Hub, the default registry.)
If any answer felt shaky, re-read "The one mental model" in the overview. Everything below
assumes you can tell an image from a container. -
Pull an official image and pin a tag
Now fetch a real image. You'll use nginx, a popular web server, because it's tiny, official
and gives you something to open in a browser.Choose a trustworthy image
On Docker Hub, prefer images marked Official or Verified Publisher. Official images
(likenginx,postgres,python) are maintained and security-reviewed by Docker or the
upstream project. Community images can be great, but they're unverified — check the last-updated
date, the download count, and whether the source Dockerfile is published before trusting one in
anything serious.Pull it
docker pull nginx:1.27-alpineWatch the output: Docker downloads several layers and reports
Pull completefor each. Those
layers are the image's building blocks — shared and cached, so the next pull of a related image
reuses them instead of re-downloading.Why
1.27-alpineand not justnginxA tag identifies a version of an image.
docker pull nginximplicitly meansnginx:latest,
which is a moving target — it changes whenever the maintainers publish a new release. In real
projects that unpredictability bites you: a rebuild silently picks up a different version.-
nginx:latest→ whatever is newest right now (avoid in anything you want to reproduce) -
nginx:1.27-alpine→ pinned to the 1.27 line, on the small Alpine Linux base (predictable)
Pinning a specific tag is the single most important habit for reproducible containers. The
-alpinevariant is also much smaller, which means faster pulls and a smaller attack surface.Verify what you have
docker imagesYou should see a row for
nginxwith tag1.27-alpine, an image ID, and a size (Alpine keeps it
around ~50 MB). Your host went from zero images to one.Deliverable for this step: the
docker imagesoutput showing the nginx image present. -
-
Run a container and reach it in your browser
An image just sitting there does nothing. Turn it into a running container.
Start nginx
docker run -d -p 8080:80 --name web nginx:1.27-alpineRead every flag — they matter:
-
-d(detached) → run in the background and return your prompt, instead of hijacking the terminal. -
-p 8080:80(publish) → map port8080on your host to port80inside the container. Format is alwaysHOST:CONTAINER. nginx listens on 80 inside; you reach it on 8080 outside. -
--name web→ give the container a friendly name so you don't have to use its random ID. -
nginx:1.27-alpine→ the image to instantiate.
Docker prints a long hex string — the container ID. Your container is now running.
See it in the browser
Open http://localhost:8080 — you should see the "Welcome to nginx!" page. Traffic hit port
8080 on your machine, Docker forwarded it to port 80 in the container, nginx answered. That port
mapping is the whole trick of exposing containerized services.Prove the isolation
Run a second container from the same image on a different host port:
docker run -d -p 8081:80 --name web2 nginx:1.27-alpineOpen http://localhost:8081 — a second, independent nginx, from the same one image. Two
containers, one image: exactly the model from the overview. If you tried to reuse host port 8080
you'd get a "port is already allocated" error, because only one process can own a host port.Deliverable for this step: note the URL that worked (http://localhost:8080) and the fact that
two containers ran from one image. -
-
Inspect a running container: ps, logs, exec
A running container is not a black box. These three commands are how you observe and debug one.
List what's running
docker psYou'll see
webandweb2, each with its image, status (Up ...), and the port mapping
(0.0.0.0:8080->80/tcp). Add-ato also see stopped containers:docker ps -aRead the logs
Every container's logs are whatever its main process wrote to stdout/stderr. Refresh
http://localhost:8080 a couple of times in your browser, then:docker logs webYou'll see nginx's access log lines for the requests you just made. To follow logs live (like
tail -f), add-f:docker logs -f webPress
Ctrl+Cto stop following — this stops following the logs, not the container.Get a shell inside
docker execruns a command inside an already-running container. Open an interactive shell:docker exec -it web sh-
-ikeeps input open,-tgives you a terminal — together-it= an interactive session. -
shis the shell (the Alpine image doesn't include bash, but hassh).
Now you're inside the container. Try:
ls /usr/share/nginx/html # nginx's web root cat /etc/os-release # note it says Alpine Linux, not your host OS exit # leave the container's shellThe
cat /etc/os-releaseis the "aha": the container runs Alpine Linux regardless of what your
host is. That's the isolation Docker gives you — a self-contained environment bundled in the image.Deliverable for this step: the
docker psoutput and a copy of a fewdocker logs weblines. -
-
Manage the lifecycle: stop, start, remove, clean up
Containers are meant to be disposable. Practice stopping and removing them so your host stays clean.
Stop and start
A stopped container still exists — it keeps its writable layer and can be restarted:
docker stop web docker ps # 'web' is gone from the running list docker ps -a # but still listed, with status 'Exited' docker start web # bring it backstopasks the process to shut down gracefully; after a timeout Docker force-kills it. Restarting
reuses the same container (and its writable layer), which is different fromdocker run, which
always creates a new one.Remove containers
To delete a container you must stop it first (or force it):
docker stop web web2 docker rm web web2docker rmdeletes the container and its writable layer permanently. The image is untouched —
confirm:docker ps -a # web and web2 are gone docker images # nginx:1.27-alpine is still hereThis is the payoff of the image/container split: you threw away two containers, but the image (the
reusable blueprint) remains, ready to spawn new containers instantly.Remove the image
When you truly don't need the image anymore:
docker rmi nginx:1.27-alpine docker images # back to emptyTidy tip
To reclaim space from all stopped containers, unused networks and dangling images at once:
docker system pruneIt asks for confirmation and only touches unused resources — safe for cleanup, but read what it
lists before typingy.Deliverable for this step: confirm with
docker ps -aanddocker imagesthat your host is
clean (no leftoverweb/web2containers). -
Submit: document your container run
Prove you ran the full loop by capturing the evidence in a small repository.
Build your submission
Create a folder, initialize git, and write one file named
SUBMISSION.mddocumenting what you did.
Paste the real outputs you collected — not invented text.# Docker Lab 1 — Run Your First Containers ## 1. docker images (after pull) <paste your `docker images` output showing nginx:1.27-alpine> ## 2. docker ps (with web and web2 running) <paste your `docker ps` output> ## 3. Browser check I opened http://localhost:8080 and saw the "Welcome to nginx!" page. (yes / no) I ran a second container on 8081 from the same image. (yes / no) ## 4. docker logs web (a few lines) <paste a few access-log lines> ## 5. Inside the container Output of `cat /etc/os-release` inside `web`: <paste it — it should mention Alpine Linux> ## 6. Clean host <paste `docker ps -a` and `docker images` showing web/web2 are gone> ## Reflection (2-3 sentences) In my own words: the difference between an image and a container, and why I pin a tag instead of using `latest`.Submit
git init git add SUBMISSION.md git commit -m "Docker lab 1 — first containers" # push to a public repo or create a gist, then submit that URLSubmit the repository or gist URL as your lab deliverable.
Submission criteria (self-check)
-
docker imagesoutput shows the pinned nginx image (notlatest) -
docker psoutput shows at least one running container with aHOST:80port mapping - You confirmed the nginx welcome page loaded in your browser
-
docker logsoutput is included, showing real request lines - The
cat /etc/os-releaseoutput shows the container's OS (Alpine), proving you exec'd inside - A final
docker ps -a/docker imagesshows a clean host - Your reflection correctly states, in your words, image vs container and why pinning beats
latest
What's next
You can now run and manage containers from existing images. In the next Docker lab you'll go the
other direction: write a Dockerfile and build your own image, turning your code into a portable
container you control end to end. -