Docker · 35 min

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

Prerequisites

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

  1. 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 version
    

    You should see two blocks: Client (the CLI you type into) and Server / 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 info
    

    Note 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 nginx download 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.

  2. 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
    (like nginx, 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-alpine
    

    Watch the output: Docker downloads several layers and reports Pull complete for 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-alpine and not just nginx

    A tag identifies a version of an image. docker pull nginx implicitly means nginx: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
    -alpine variant is also much smaller, which means faster pulls and a smaller attack surface.

    Verify what you have

    docker images
    

    You should see a row for nginx with tag 1.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 images output showing the nginx image present.

  3. 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-alpine
    

    Read every flag — they matter:

    • -d (detached) → run in the background and return your prompt, instead of hijacking the terminal.
    • -p 8080:80 (publish) → map port 8080 on your host to port 80 inside the container. Format is always HOST: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-alpine
    

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

  4. 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 ps
    

    You'll see web and web2, each with its image, status (Up ...), and the port mapping
    (0.0.0.0:8080->80/tcp). Add -a to also see stopped containers:

    docker ps -a
    

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

    You'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 web
    

    Press Ctrl+C to stop following — this stops following the logs, not the container.

    Get a shell inside

    docker exec runs a command inside an already-running container. Open an interactive shell:

    docker exec -it web sh
    
    • -i keeps input open, -t gives you a terminal — together -it = an interactive session.
    • sh is the shell (the Alpine image doesn't include bash, but has sh).

    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 shell
    

    The cat /etc/os-release is 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 ps output and a copy of a few docker logs web lines.

  5. 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 back
    

    stop asks 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 from docker 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 web2
    

    docker rm deletes 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 here
    

    This 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 empty
    

    Tidy tip

    To reclaim space from all stopped containers, unused networks and dangling images at once:

    docker system prune
    

    It asks for confirmation and only touches unused resources — safe for cleanup, but read what it
    lists before typing y.

    Deliverable for this step: confirm with docker ps -a and docker images that your host is
    clean (no leftover web/web2 containers).

  6. 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.md documenting 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 URL
    

    Submit the repository or gist URL as your lab deliverable.

    Submission criteria (self-check)

    • docker images output shows the pinned nginx image (not latest)
    • docker ps output shows at least one running container with a HOST:80 port mapping
    • You confirmed the nginx welcome page loaded in your browser
    • docker logs output is included, showing real request lines
    • The cat /etc/os-release output shows the container's OS (Alpine), proving you exec'd inside
    • A final docker ps -a / docker images shows 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.