Logo

Lab 08: Introduction to Docker — Containers and Images

13 min read
Lesson slides
1 / 13

Lab 08: Introduction to Docker — Containers and Images

Why Docker exists, writing a Dockerfile, and building and running containers

Overview

This lab introduces Docker as a containerization technology. You will learn why Docker was created, what problem it solves, how to write a Dockerfile, and how to build and run Docker images and containers. The demo application is a simple Node.js/Express server used throughout the lab to make every concept hands-on from day one.


Objectives

  • Understand the deployment and environment consistency problem that Docker solves
  • Install and configure Docker Desktop on your local machine
  • Build a simple Node.js/Express application to use as a Docker target
  • Write a Dockerfile with all required instructions
  • Build a Docker image from a Dockerfile
  • Run, inspect, stop, and remove Docker containers
  • Understand the difference between a Dockerfile, an image, and a container
  • Use Docker Hub to find official base images

Prerequisites

  • A computer running Windows, macOS, or Linux
  • Basic familiarity with the terminal/command line
  • Node.js LTS installed (for running the demo app before Dockerizing it)
  • Docker Desktop installed (see Task 1)
  • Visual Studio Code (recommended, with the Docker extension)

Background

The Problem Docker Solves

When a new developer joins a project or when you need to deploy an application to a new server, the typical workflow requires manually installing every dependency the application needs. Consider a project with the following stack: PHP with Laravel, PostgreSQL with the PostGIS spatial plugin, Redis for caching, and Angular for the frontend. To get this running locally, each developer must install compatible versions of every component. Version mismatches — for example, a Laravel version requiring a specific PHP version, or PostGIS requiring a specific Postgres version — frequently produce installation errors that can take hours or days to resolve.

The same problem repeats every time the application moves to a new environment: local development, testing server, staging server, and production. Each environment requires the same setup steps, and each step is a potential failure point.

graph LR
    A[Developer Machine] -->|Manual install of all deps| B[Works on My Machine]
    C[Test Server] -->|Manual install again| D[Version Conflict!]
    E[Production Server] -->|Manual install again| F[More Conflicts!]

What is Docker?

Docker is a tool that allows you to package your application along with everything it needs to run — its runtime, dependencies, configuration — into a portable unit called a container. You can then move that container from one environment to another with a single command, without reinstalling anything.

The Docker logo depicts a whale carrying a stack of containers on its back. This is the core metaphor: just as cargo ships transport standardized containers from port to port regardless of what is inside them, Docker transports your application containers from machine to machine regardless of the underlying operating system.

graph LR
    A[Dockerfile] -->|docker build| B[Docker Image]
    B -->|docker run| C[Container 1: App]
    B -->|docker run| D[Container 2: Database]
    B -->|docker run| E[Container 3: Redis]

With Docker, the entire setup for a new developer becomes:

docker build -t myapp .
docker run myapp

That replaces days of dependency installation and debugging.

Docker Image vs Docker Container

Docker uses a concept similar to Object-Oriented Programming:

OOP ConceptDocker Equivalent
Class / BlueprintDocker Image
Object / InstanceDocker Container

A Docker Image is a read-only template built from a Dockerfile. It contains all the layers needed to run your application. A single image can be used to create multiple containers.

A Docker Container is a running instance of an image. It is an isolated environment where your application actually executes.

flowchart TD
    A[Dockerfile\nInstructions] -->|docker build| B[Docker Image\nTemplate / Blueprint]
    B -->|docker run| C[Container A\nRunning Instance]
    B -->|docker run| D[Container B\nRunning Instance]
    B -->|docker run| E[Container C\nRunning Instance]

Docker Image Layers

Every instruction in a Dockerfile creates a layer in the resulting image. Docker caches these layers. If a layer has not changed since the last build, Docker reuses the cached version instead of rebuilding it. This makes subsequent builds faster.

graph TD
    L1[Layer 1: node:14 base image]
    L2[Layer 2: WORKDIR /app]
    L3[Layer 3: COPY package.json]
    L4[Layer 4: RUN npm install]
    L5[Layer 5: COPY source files]
    L1 --> L2 --> L3 --> L4 --> L5

Docker Hub

Docker Hub (hub.docker.com) is a public registry for Docker images. It is the equivalent of GitHub for Docker images. All official base images — Node.js, PHP, Python, PostgreSQL, Redis, nginx — are stored there. Docker automatically pulls images from Docker Hub when you reference them in a Dockerfile.

Each image on Docker Hub has multiple tags representing different versions and variants:

TagDescription
node:14Full Node.js 14 image (Debian-based)
node:14-alpineMinimal Node.js 14 image (~5 MB base, Alpine Linux)
node:14-slimReduced Node.js 14 image (fewer packages than full)

Lab Tasks

Task 1: Install Prerequisites

1.1 Install Docker Desktop

Search for "install Docker Desktop" and navigate to docker.com. Install Docker Desktop for your operating system (Windows, macOS, or Linux). Docker Desktop installs Docker Engine, Docker Compose, and Docker Swarm together.

After installation, verify Docker is running by checking your system tray for the Docker icon. You can also open the Docker Desktop dashboard to monitor containers visually.

1.2 Verify the installation

docker --version
docker ps

Expected output for docker ps:

CONTAINER ID   IMAGE   COMMAND   CREATED   STATUS   PORTS   NAMES

An empty table means Docker is running and no containers are active yet.

1.3 Configure Docker Desktop (optional)

In Docker Desktop Settings → General, you can enable "Start Docker Desktop when you login" so Docker starts automatically with your machine.


Task 2: Create the Node.js Demo Application

This application serves as the target for Dockerization throughout the lab.

2.1 Create the project directory

mkdir node-app
cd node-app

2.2 Initialize the Node.js project

npm init -y

This creates package.json. Accept all defaults.

2.3 Install Express

npm install express

2.4 Install nodemon as a development dependency

npm install --save-dev nodemon

nodemon watches for file changes and automatically restarts the server during local development.

2.5 Create index.js

const express = require('express');
const app = express();
 
const PORT = 4000;
 
app.get('/', (req, res) => {
    res.send('<h2>Hello from Docker!</h2>');
});
 
app.listen(PORT, () => {
    console.log(`App is up and running on port ${PORT}`);
});

2.6 Add scripts to package.json

Edit the scripts section:

"scripts": {
    "start": "node index.js",
    "start:dev": "nodemon index.js"
}

2.7 Test the application without Docker

npm start

Expected output:

App is up and running on port 4000

Open http://localhost:4000 in your browser. You should see the HTML response.

Press Ctrl+C to stop the server before proceeding.


Task 3: Write the Dockerfile

Create a file named Dockerfile (no extension) in the root of node-app.

Install the Docker extension in VS Code for syntax highlighting and autocompletion in Dockerfiles.

Complete Dockerfile:

FROM node:14
 
WORKDIR /app
 
COPY package.json .
 
RUN npm install
 
COPY . .
 
EXPOSE 4000
 
CMD ["npm", "start"]

Explanation of each instruction:

InstructionPurpose
FROM node:14Sets the base image. Docker pulls Node.js 14 from Docker Hub. This installs Node and npm inside the container.
WORKDIR /appCreates and sets /app as the working directory inside the container. All subsequent commands run from this path.
COPY package.json .Copies only package.json into the container's working directory before running npm install.
RUN npm installInstalls all dependencies listed in package.json into /app/node_modules inside the container.
COPY . .Copies all remaining application source files into the container's working directory.
EXPOSE 4000Documents that the application listens on port 4000. This is metadata — it does not publish the port to the host machine.
CMD ["npm", "start"]The command Docker runs when a container starts. Runs the start script defined in package.json.

Why copy package.json separately before copying the full source?

Docker caches each layer. If you copy package.json first and run npm install as a separate layer, Docker only re-runs npm install when package.json actually changes. If you change only your source code (index.js, etc.), Docker reuses the cached npm install layer, making rebuilds much faster.


Task 4: Build the Docker Image

docker build -t express-node-app .
  • -t express-node-app names the image express-node-app
  • . tells Docker to look for a Dockerfile in the current directory

Expected output (abbreviated):

[1/5] FROM node:14
[2/5] WORKDIR /app
[3/5] COPY package.json .
[4/5] RUN npm install
[5/5] COPY . .
Successfully built <image-id>
Successfully tagged express-node-app:latest

Docker pulls the node:14 base image from Docker Hub on the first run. Subsequent builds use the cached base layer.

Verify the image was created:

docker image ls

Expected output:

REPOSITORY          TAG       IMAGE ID       CREATED         SIZE
express-node-app    latest    <id>           X seconds ago   960MB

Task 5: Run a Docker Container

5.1 Run in the foreground (for initial testing)

docker run --name express-node-app-container express-node-app

Expected output:

App is up and running on port 4000

The terminal is now attached to the container. Press Ctrl+C to stop it.

5.2 Run in detached mode with port forwarding

docker run -d --name express-node-app-container -p 4000:4000 express-node-app
FlagMeaning
-dDetached mode — runs in the background, returns control to your terminal
--name express-node-app-containerAssigns a human-readable name to the container
-p 4000:4000Port forwarding: maps port 4000 on your host machine to port 4000 inside the container

Why is port forwarding required?

Containers run in an isolated network environment. Even though the application inside listens on port 4000, and even though EXPOSE 4000 is declared in the Dockerfile, the port is not accessible from your host machine until you explicitly forward it with -p. The format is -p <host_port>:<container_port>.

5.3 Verify the container is running

docker ps

Expected output:

CONTAINER ID   IMAGE              COMMAND        CREATED        STATUS        PORTS                    NAMES
<id>           express-node-app   "npm start"    5 seconds ago  Up 4 seconds  0.0.0.0:4000->4000/tcp   express-node-app-container

5.4 Test in the browser

Open http://localhost:4000. You should see the HTML response from inside the container.


Task 6: Manage Containers and Images

List running containers:

docker ps

List all containers (including stopped ones):

docker ps -a

Stop a running container:

docker stop express-node-app-container

Remove a stopped container:

docker rm express-node-app-container

Force-stop and remove a running container in one command:

docker rm -f express-node-app-container

List all images:

docker image ls

Remove an image:

docker rmi express-node-app

You cannot remove an image while a container (even a stopped one) is using it. Remove the container first.

View container logs:

docker logs express-node-app-container

Summary of essential commands:

CommandAction
docker build -t <name> .Build an image from the Dockerfile in the current directory
docker image lsList all local images
docker run -d -p <host>:<container> --name <name> <image>Run a container in the background with port forwarding
docker psList running containers
docker ps -aList all containers
docker stop <name>Stop a running container
docker rm <name>Remove a stopped container
docker rm -f <name>Force remove a running container
docker rmi <image>Remove an image
docker logs <name>View container output logs

Task 7: Explore Docker Hub

7.1 Browse to Docker Hub

Open https://hub.docker.com and search for node.

7.2 Read the official Node.js image page

The first result with the official badge is the Docker-maintained Node.js image. Scroll down to see all available tags. Notice the different variants: full image, slim, alpine. Alpine-based images are significantly smaller because they are built on Alpine Linux (~5 MB base).

7.3 Choose the right base image

For production use cases where image size matters, prefer:

FROM node:14-alpine

For development or when you need full system tools, the full image is acceptable.

7.4 Try a different base image variant

Change your Dockerfile's first line to:

FROM node:14-alpine

Rebuild and compare the image size:

docker build -t express-node-app-alpine .
docker image ls

The Alpine-based image should be significantly smaller than the full node:14 image.


Summary

ConceptKey Takeaway
The ProblemMoving an application between environments requires reinstalling all dependencies every time, which is error-prone and time-consuming
DockerA tool that packages your application and its dependencies into portable containers that run consistently anywhere
Docker HubA public registry at hub.docker.com where official and community Docker images are stored
DockerfileA text file with step-by-step instructions telling Docker how to build an image for your application
FROMSpecifies the base image — the starting point that provides the runtime (e.g., Node.js, PHP, Python)
WORKDIRSets the working directory inside the container
COPYCopies files from your local machine into the container's filesystem
RUNExecutes a command during the image build step (e.g., npm install)
CMDThe command that runs when a container starts (e.g., npm start)
EXPOSEDocuments which port the application listens on (does not publish the port)
Docker ImageThe built template (blueprint/class) produced by docker build
Image LayersEach Dockerfile instruction creates a cached layer; unchanged layers are reused on rebuilds
Docker ContainerA running instance of an image (like an object instantiated from a class)
Port Forwarding (-p)Maps a host port to a container port so the application is accessible from outside the container
-d flagRuns the container in detached (background) mode
Caching optimizationCopy package.json and run npm install before copying source files to avoid reinstalling dependencies on every code change