Docker
This activity puts into practice the concepts from the Containerization with Docker lecture. You will investigate how containers actually behave: namespace isolation, ephemeral storage, volumes, image layer caching, port mapping, and multi-service Compose stacks. By the end, you will have run, built, and inspected containers and seen for yourself exactly what isolation they provide and where it ends.
What You Will Need
Section titled “What You Will Need”You need a container runtime installed on your laptop before starting.
On macOS, Docker Engine runs inside a Linux VM managed transparently by the tool you choose. All of the following expose the same docker CLI and are compatible with this activity.
Docker Desktop: Download from docker.com/products/docker-desktop. After installation, start Docker Desktop from your Applications folder and wait for the whale icon in the menu bar to show “Docker Desktop is running.”
OrbStack (lightweight alternative): Download from orbstack.dev. OrbStack runs a lightweight Linux VM with lower memory overhead and faster startup than Docker Desktop. All docker commands work without modification.
Podman Desktop: Download from podman-desktop.io. Provides a Podman-based environment; most docker commands work with podman substituted.
Rancher Desktop: Download from rancherdesktop.io. Free and open-source; bundles either containerd or dockerd.
Verify your installation:
docker versiondocker run --rm hello-worldOn Linux, Docker Engine and Podman run natively against the host kernel with no VM required.
Docker Engine: Follow the official installation guide for your distribution at docs.docker.com/engine/install/. After installing, add your user to the docker group so you can run commands without sudo:
sudo usermod -aG docker $USER# Log out and back in for the group change to take effectPodman (rootless alternative): Available in most package managers:
# Ubuntu/Debiansudo apt install -y podman
# Fedora / AlmaLinux / RHELsudo dnf install -y podman
# Arch Linuxsudo pacman -S podmanPodman runs rootless by default: no daemon, no sudo required.
Rancher Desktop: Download from rancherdesktop.io. Provides a GUI and bundles either containerd or dockerd.
Verify:
docker version # or: podman versiondocker run --rm hello-worldOn Windows, Linux containers require a WSL2-backed Linux VM. All of the following use WSL2.
Docker Desktop: Download from docker.com/products/docker-desktop. Docker Desktop will offer to enable WSL2 during installation if it is not already configured. After installation, open a PowerShell terminal and verify:
docker versiondocker run --rm hello-worldDocker Engine in WSL2 without Docker Desktop: Install a WSL2 Linux distribution (e.g., Ubuntu from the Microsoft Store), then follow the Docker Engine Linux install guide inside that distro. This gives you a full Docker Engine without the Docker Desktop application.
Podman Desktop: Download from podman-desktop.io. Provides a Podman environment using a WSL2-backed VM. All docker commands work with podman substituted.
Rancher Desktop: Download from rancherdesktop.io. Free and open-source; uses WSL2 and bundles either containerd or dockerd.
Pulling and Inspecting Images
Section titled “Pulling and Inspecting Images”Before running anything, let us look at what an image actually is.
-
Check which cgroup version your container runtime is using:
Rancher Desktop docker info | grep -Ei 'Cgroup'Most modern Linux distributions and recent Docker Desktop setups report cgroup v2. If you are using Podman,
podman infoalso reports the cgroup version in its host information. -
Pull two variants of the nginx image:
Docker Pull Nginx:alpine docker pull nginx:alpinedocker pull nginx:latest -
Compare their sizes:
Docker Images Nginx docker images nginxRecord the size of each. The
alpinevariant should be considerably smaller. -
Inspect the layers of the alpine image:
Docker History Nginx:alpine docker history nginx:alpineEach row is one layer. The columns show the instruction that created it, when it was built, and how much disk space it added.
Record:
- How many layers does
nginx:alpinehave? - Which layer is the largest, and what instruction created it?
- Why are some layers listed with size
0B?
- How many layers does
-
Examine the image’s metadata:
Docker Inspect Nginx:alpine docker inspect nginx:alpineIn the JSON output, find:
ExposedPorts: what port does the image declare as its service port?EntrypointandCmd: what command runs when a container starts from this image?Env: what environment variables are set by default?
Inside the Container: Namespace Isolation
Section titled “Inside the Container: Namespace Isolation”A running container is an isolated process with its own view of the system. This section makes that isolation visible.
-
Start an nginx container in the background and then open an interactive shell inside it:
Start nginx Sandbox docker run -d --name sandbox nginx:alpinedocker exec -it sandbox shYou are now inside the container’s namespace.
-
Explore what the container can see:
Hostname # What is this machine's hostname?hostname# Who is the current user?whoami# What processes are running?ps aux# What does the filesystem look like?ls /# What OS is this?cat /etc/os-releaseps auxlists running processes:aincludes processes from all users,ushows the user-oriented format with CPU and memory columns, andxincludes processes not attached to a terminal. Inside the container you should see only a handful of nginx processes. Record each result. -
Exit the container and run the same commands on your host machine:
Exit Container Shell exithostnamewhoamips auxcat /etc/os-releaseThe host
ps auxoutput will be considerably longer than what you saw inside the container. Every daemon, shell session, and background service on your machine is visible here; inside the container you saw only the processes that belong to it.
Container Lifecycle and Configuration
Section titled “Container Lifecycle and Configuration”Now that you have seen what isolation looks like from inside, look at container management from the outside: checking logs, monitoring resource usage, stopping and restarting without discarding, and passing configuration in at runtime.
-
The
sandboxcontainer from the previous section is still running. Generate an HTTP request inside it to produce a log entry, then view the logs from the host:Generate and View nginx Logs docker exec sandbox wget -qO- http://localhost > /dev/nulldocker logs sandboxwgetdownloads a URL from the command line.-qsuppresses progress output;-O-writes the response to standard output instead of saving it to a file.docker logsshows everything the container has written to stdout and stderr since it started. For nginx, that is the access log. Follow logs in real time with-f; press Ctrl+C to stop:View Container Logs docker logs -f sandbox -
Check resource usage across all running containers:
Docker Stats --no-stream docker stats --no-stream--no-streamprints one snapshot and exits instead of staying in interactive mode. You will see CPU percentage, memory usage, and network I/O for each container. -
Stop the container and observe its status:
Docker Stop Sandbox docker stop sandboxdocker psdocker ps -adocker psshows only running containers;sandboxis gone from that list.docker ps -ashows all containers including stopped ones;sandboxappears with statusExited. The container is paused, not erased. -
Restart it:
Docker Start Sandbox docker start sandboxdocker psThe same container is running again. Its writable layer was preserved through the stop and start.
-
Pass configuration into a one-off container at runtime.
-esets an environment variable inside the container;--rmremoves the container automatically when it exits:Run Container docker run --rm -e GREETING="hello from the host" alpine sh -c 'echo $GREETING'You should see
hello from the hostprinted.--rmis useful for short-lived commands that produce output and do not need to persist. -
Clean up:
Docker Stop Sandbox docker stop sandboxdocker rm sandbox
Ephemeral Storage: What Containers Don’t Keep
Section titled “Ephemeral Storage: What Containers Don’t Keep”A container’s writable layer disappears when the container is removed. This section makes that concrete.
-
Run a container and write something to its filesystem:
Run Container docker run -d --name ephemeral nginx:alpinedocker exec ephemeral sh -c 'echo "this will not survive" > /tmp/testfile.txt'docker exec ephemeral cat /tmp/testfile.txtConfirm the file is there.
-
Inspect what changed in the container’s writable layer:
Docker Diff Ephemeral docker diff ephemeraldocker diffprefixes each path with a status:Ameans added,Cmeans changed, andDmeans deleted. In this example you should seeA /tmp/testfile.txt; you may also seeCentries for files nginx touched while running.Dis not used in this step, but you would see it if a file or directory from the container filesystem had been removed. -
Stop and remove the container completely:
Docker Stop Ephemeral docker stop ephemeraldocker rm ephemeral -
Start a fresh container from the same image:
Run Container docker run -d --name ephemeral2 nginx:alpinedocker exec ephemeral2 cat /tmp/testfile.txtThe file is gone.
-
Clean up the second container:
Docker Stop Ephemeral2 docker stop ephemeral2docker rm ephemeral2
Persistent Storage with Volumes
Section titled “Persistent Storage with Volumes”Volumes live outside the container’s writable layer and survive container replacement.
-
Create a named volume:
Docker Volume Create Mydata docker volume create mydatadocker volume ls -
Run a container with the volume attached and write to it:
Run Container docker run -d --name writer -v mydata:/data nginx:alpinedocker exec writer sh -c 'echo "persisted across containers" > /data/record.txt'docker exec writer cat /data/record.txt -
Remove the container and start a completely new one with the same volume:
Docker Stop Writer docker stop writerdocker rm writerdocker run -d --name reader -v mydata:/data nginx:alpinedocker exec reader cat /data/record.txtThe data is still there, even though the original container no longer exists.
-
Find where Docker stores the volume on your host:
Docker Volume Inspect Mydata docker volume inspect mydataLook for the
Mountpointfield. On Linux, you can read the files directly at that path (as root). On macOS and Windows, this path is inside the Linux VM that Docker Desktop manages. -
Clean up:
Docker Stop Reader docker stop readerdocker rm readerdocker volume rm mydata
Building an Image and Observing Layer Caching
Section titled “Building an Image and Observing Layer Caching”This section shows how Docker’s layer cache behaves in practice: both when it helps and when it gets invalidated.
-
Create a working directory and add two files:
Create Directories mkdir mysite && cd mysiteCreate
index.html:index.html <h1>Hello from my container</h1>Create
Dockerfile:Dockerfile FROM nginx:alpineRUN echo "server_tokens off;" > /etc/nginx/conf.d/security.confCOPY index.html /usr/share/nginx/html/index.htmlserver_tokens offis a real nginx security setting: it removes the server version number from response headers. -
Build the image:
Build Docker Image docker build -t mysite:v1 .This is the first build, so nothing is cached. You will see each step execute from scratch. Note the layer IDs in the output.
-
Build again without changing anything:
Build Docker Image docker build -t mysite:v1 .Every step should now say
CACHED. Docker reused all layers from the first build. -
Change only
index.html(edit the file to say something different), then rebuild:Build Docker Image docker build -t mysite:v2 .The
RUNstep saysCACHED. Only theCOPYstep re-runs, because that is the first step whose inputs changed. -
Now change the
FROMline in the Dockerfile toFROM nginx:latestand rebuild:Build Docker Image docker build -t mysite:v3 .FROMnow resolves to a different base image. Both theRUNstep and theCOPYstep must re-run: every cached layer was built on top of the previous base image, so none of them can be reused. -
Run the resulting image and verify it serves your content:
Run Container docker run -d -p 8080:80 --name mysite mysite:v3curl http://localhost:8080docker stop mysite && docker rm mysiteClean up your working directory when done:
cd .. && rm -rf mysite
Port Mapping and Container Networking
Section titled “Port Mapping and Container Networking”Containers have their own network namespace. Port mapping is how services inside that namespace become reachable from outside.
-
Start a container with port mapping and verify you can reach it from your host:
Pattern B docker run -d -p 8080:80 --name webserver nginx:alpinecurl http://localhost:8080curlmakes an HTTP request from the command line and prints the response body. You made a request to your host on port 8080; Docker forwarded it to the container on port 80. -
Inspect the container’s network configuration:
Docker Inspect Webserver docker inspect webserverIn the
NetworkSettingssection, find:IPAddress: the container’s private IP on the Docker bridge networkPorts: the port mapping Docker configured
-
Try to reach the container’s private IP directly from your host:
Ports # Substitute the IP address you found in step 2curl http://<container-ip>:80On Linux with Docker Engine this will work. On macOS and Windows with Docker Desktop it likely will not, because the bridge network is inside a Linux VM.
-
List the Docker networks on your system and inspect the default bridge:
Docker Network Ls docker network lsdocker network inspect bridgeFind the
webservercontainer listed underContainers. Note the bridge network’s subnet. -
Create a user-defined network and run two containers on it:
Docker Network Create Mynet docker network create mynetdocker run -d --network mynet --name server1 nginx:alpinedocker run -d --network mynet --name server2 nginx:alpine# From server2, reach server1 by container namedocker exec server2 wget -qO- http://server1Container DNS resolves
server1to its IP withinmynet. -
Confirm that containers on different networks cannot reach each other:
Confirm That Containers on Different Networks Cannot Reach # webserver is on the default bridge, server1 is on mynetdocker exec server2 wget -qO- http://webserver 2>&1This should fail or time out:
webserveris not onmynet. -
Clean up:
Docker Stop Webserver Server1 Server2 docker stop webserver server1 server2docker rm webserver server1 server2docker network rm mynet
A Multi-Service Stack with Compose
Section titled “A Multi-Service Stack with Compose”Compose wires multiple containers together with automatic networking and a single declarative file. This section uses nginx as a web frontend and Redis as an internal cache, a common real-world pairing. Redis is a fast in-memory data store that should never be exposed directly to the network; only the application that uses it should be able to reach it.
-
Create a working directory and write a
compose.yml:compose.yml mkdir mystack && cd mystackmkdir htmlecho "<h1>Compose stack</h1>" > html/index.htmlcompose.yml:Compose Services services:web:image: nginx:alpineports:- "8080:80"volumes:- ./html:/usr/share/nginx/html:rodepends_on:- cachecache:image: redis:alpineTwo things to notice: the
:rosuffix on the bind mount makes it read-only inside the container, so nginx can read your files but cannot modify them. Thedepends_onentry tells Compose to startcachebeforeweb. -
Start the stack:
Start Compose Stack docker compose up -dWatch the output: Compose starts
cachefirst, thenweb, respecting thedepends_onorder. Confirm this in the startup logs:Docker Compose Logs docker compose logs -
Inspect what is running:
List Compose Services docker compose psLook at the
PORTScolumn.webshows0.0.0.0:8080->80/tcp;cacheshows nothing. That absence tells youcacheis not reachable from outside the Compose network. -
Reach the web service from your host:
Test HTTP Response curl http://localhost:8080 -
Verify the cache service is running.
redis-cliis the Redis command-line client, included in theredis:alpineimage:Ping Redis Cache docker compose exec cache redis-cli pingRedis responds with
PONG. Now store and retrieve a value to confirm it is working:Set and Read Redis Value docker compose exec cache redis-cli set greeting "hello from cache"docker compose exec cache redis-cli get greeting -
Confirm that the
webcontainer can resolve thecachehostname via Compose’s embedded DNS:Resolve Compose Service Name docker compose exec web nslookup cacheYou should see the IP address assigned to the
cachecontainer within the Compose network. Containers on the same Compose network reach each other by service name; the host cannot. -
Upgrade the web service. Open
compose.ymland change thewebimage fromnginx:alpinetonginx:latest, then pull and recreate:compose.yml docker compose pulldocker compose up -dRead the output carefully. You will see something like:
A Multi-service Stack with Compose Output ✔ Container mystack-cache-1 Running✔ Container mystack-web-1 StartedRunningmeans the container was already up and was left alone.Startedmeans it was stopped, replaced with a new container from the updated image, and started again. Compose determines which containers to recreate by comparing the running container’s image digest against what was just pulled. -
Verify the web content is still served:
Test HTTP Response curl http://localhost:8080The HTML is unchanged. It lives in
html/on your host; replacing the web container had no effect on it. -
Stop the stack and confirm what persists:
Stop Compose Stack docker compose downdocker compose ps # containers are gonels html/ # bind-mounted host directory is unchanged -
Clean up:
CD .. && Rm -rf Mystack cd .. && rm -rf mystack
Your Image
Section titled “Your Image”You have explored existing images, observed isolation, built a layered image, and run a Compose stack. Now build an image that carries your identity and applies the best practices from the lecture: a non-root user and a realistic server process.
-
Create a working directory and an HTML file with your ONID:
Create Directories mkdir myapp && cd myappecho "<h1>Built by: ONID@oregonstate.edu</h1>" > index.html -
Create a
Dockerfile:Dockerfile FROM python:3.12-slimRUN useradd --create-home appuserWORKDIR /home/appuser/appCOPY index.html .USER appuserEXPOSE 8080CMD ["python3", "-m", "http.server", "8080"]python3 -m http.server 8080is Python’s built-in static file server. It serves any files in the working directory over HTTP on port 8080. Because it does not need to bind a privileged port, it can run as a non-root user. -
Build and run the image:
Build Docker Image docker build -t myapp:v1 .docker run -d -p 9090:8080 --name myapp myapp:v1 -
Confirm your content is served:
Test HTTP Response curl http://localhost:9090You should see your ONID in the response.
-
Verify the container process is running as a non-root user:
Docker Exec Myapp Whoami docker exec myapp whoamiYou should see
appuser, notroot. -
Tag the image in the format a registry expects. Replace
YOURONIDwith your actual ONID:Docker Tag Myapp:v1 Youronid/myapp:v1 docker tag myapp:v1 YOURONID/myapp:v1docker images myappBoth tags point to the same image ID. The
username/repository:tagformat is what Docker Hub expects; a private registry would useregistry.example.com/repository:tag. No account or login is needed to tag locally. -
Inspect the image layers:
Docker History Myapp:v1 docker history myapp:v1You will see several base layers from
python:3.12-slim, then your own layers on top: theRUN useradd,WORKDIR,COPY,USER, andCMDentries. -
Clean up:
Docker Stop Myapp docker stop myappdocker rm myappcd .. && rm -rf myapp
Going Further
Section titled “Going Further”You have worked through the core mechanics of containers from scratch. The natural next step is to build something real.
The Docker documentation’s Workshop guide is the most direct continuation of this activity. It walks you through containerizing a complete application, adding a database with Compose, and pushing the finished image to Docker Hub. Plan about an hour for it.
If you would rather start from your own code, pick a project in a language you already know and write a Dockerfile for it from scratch. Start with a single-stage build that runs correctly, then improve it step by step: add a multi-stage build to strip the build toolchain from the final image, shrink the base image, add a non-root USER, and add a HEALTHCHECK. Run docker images after each change and watch what happens to the size. The gap between a first-draft image and an optimized one is often an order of magnitude.
Two tools worth knowing once you are writing real Dockerfiles:
- hadolint is a Dockerfile linter that catches common mistakes and anti-patterns, from wrong
COPYordering to unintentional package cache bloat. Run it against any Dockerfile you write. - dive lets you explore an image layer by layer in the terminal and shows exactly which files each instruction added, removed, or changed. It is the fastest way to understand why an image is larger than you expect.