Generating PDF…

Preparing…
← Back

Computer Programming 2

Introduction to Docker

What Are We Building?

We're going to build a simple To-Do web app with Flask — step by step.

By the end, your app will:

  • Show a page listing all your to-do items
  • Have a text form to type in a new task
  • Update the list immediately when you add a task

Our approach: We'll build it piece by piece. Try each step yourself first, then look at the solution on the next slide.

todo-app/
├── app.py              ← Flask server (Python)
└── templates/
    └── index.html      ← Web page template (HTML)

Setup: Create Your Project Folder

Start by creating a new folder for the project and moving into it.

  • mkdir creates the folder
  • cd moves into it

Tip: Keep this terminal open — all commands from here run inside the todo-app/ folder.

# Create a new folder called "todo-app"
mkdir todo-app

# Move into the folder
cd todo-app

# Confirm you're in the right place
pwd
# Should show: .../todo-app

Setup: Virtual Environment

A virtual environment keeps Flask isolated to this project only — it won't affect anything else on your computer.

🍎 macOS / Linux
# 1. Create the virtual environment
python3 -m venv .venv

# 2. Activate it
source .venv/bin/activate

# Your prompt will now show:
# (.venv) $   ← means it is active
🪟 Windows (PowerShell)
# 1. Create the virtual environment
python -m venv .venv

# 2. Activate it
.venv\Scripts\activate

# Your prompt will now show:
# (.venv) PS C:\...  ← means it is active

Remember: Every time you open a new terminal for this project, you need to activate the virtual environment again before running anything.

Setup: Install Flask and Create Files

With the virtual environment active, install Flask:

  • Create app.py directly in todo-app/
  • Create a subfolder called templates/ and inside it create index.html

Why templates/? Flask looks for HTML files specifically in a folder called templates. The name is not optional!

# Install Flask (venv must be active first)
pip install Flask

# Verify it installed correctly
pip show flask

# Create the templates folder
mkdir templates

# Now create two empty files:
#   todo-app/app.py
#   todo-app/templates/index.html

# Your structure should look like:
# todo-app/
# ├── .venv/           ← auto-created by venv
# ├── app.py           ← create this (empty for now)
# └── templates/
#     └── index.html   ← create this (empty for now)

✏️ Your Turn — Hello World

Open app.py and try to write a Flask app that:

  • Imports Flask from the flask package
  • Creates an app object with Flask(__name__)
  • Has a route / that returns the text "Hello, To-Do App!"
  • Runs the server with debug=True

Give it a go — then go to the next slide to see the solution.

Solution: Hello World app.py

Every line is explained in the comments. Key things to notice:

  • Flask(__name__)__name__ tells Flask where your app file lives so it can find the templates/ folder
  • @app.route('/') — this decorator registers the function as the handler for the URL /
  • debug=True — the server auto-restarts whenever you save a change to app.py
# app.py

# Step 1: Import the Flask class
from flask import Flask

# Step 2: Create the application object.
# __name__ tells Flask where to look for templates and files.
app = Flask(__name__)

# Step 3: Define a route.
# When someone visits http://127.0.0.1:5000/
# Flask calls this function and returns its result to the browser.
@app.route('/')
def index():
    return "Hello, To-Do App!"

# Step 4: Run the development server.
# debug=True → server restarts automatically when you save changes.
if __name__ == '__main__':
    app.run(debug=True)

Run Your App and Test It

Save app.py, then run it from your terminal. Flask will print a URL — open it in your browser.

What you should see: A plain white page with the text Hello, To-Do App!

Getting "command not found"? Your virtual environment is not active. Run source .venv/bin/activate (Mac) or .venv\Scripts\activate (Windows) first.

# Make sure you are in todo-app/ with venv active, then:
python app.py

# Flask prints something like:
#  * Running on http://127.0.0.1:5000
#  * Debug mode: on

# Open that URL in your browser.
# You should see: Hello, To-Do App!

# To stop the server:  Ctrl + C

Add the To-Do List to app.py

Instead of returning a plain string, let's store tasks in a Python list right below the app line. This list acts as our simple in-memory "database."

In-memory means: the list is stored in RAM while the server runs. If you stop and restart the server, it resets to these two starter items. A real app would use a database to save data permanently.

# app.py
from flask import Flask

app = Flask(__name__)

# Our in-memory to-do list.
# Pre-loaded with two starter tasks so the page isn't empty.
todos = ["Learn Flask", "Build a To-Do App"]

@app.route('/')
def index():
    # We'll update this soon to show the list on a proper page
    return "Hello, To-Do App!"

if __name__ == '__main__':
    app.run(debug=True)

A Quick Intro to HTML

Before we create our template, let's look at HTML. HTML stands for HyperText Markup Language — the language that describes the structure of every web page.

Think of it as the skeleton of a website. It uses tags to mark up content:

  • Tags come in pairs: <p> (open) and </p> (close)
  • Everything between the tags is the content
  • Tags can be nested inside each other

Analogy: HTML is like Word headings and bullet points — it gives content meaning and structure.

HTML Tags We'll Use

Here are the tags that appear in our to-do app:

  • <h1> — main heading (big, bold text)
  • <ul> — unordered list (a bulleted list container)
  • <li> — list item, goes inside <ul>
  • <form> — container for user input
  • <input> — a text box
  • <button> — a clickable button
<!-- A heading followed by a bulleted list -->
<h1>My Shopping List</h1>
<ul>
    <li>Milk</li>
    <li>Bread</li>
    <li>Cheese</li>
</ul>

<!-- This renders as a bulleted list in the browser -->

HTML Forms

A <form> lets users send data to the server. The key attributes:

  • action="/add" — which URL receives the data when submitted
  • method="post" — sends data in the request body (not visible in the URL)
  • name="username" on <input> — the key our Python code uses to read the value

The name attribute on the input is critical — without it, Python can't find what the user typed.

<!-- A "Leave a Review" form -->
<form action="/submit-review" method="post">

    <!-- Text input  →  Python: request.form.get('username') -->
    <label>Your name:</label>
    <input type="text" name="username" required>

    <!-- Dropdown    →  Python: request.form.get('rating') -->
    <label>Rating:</label>
    <select name="rating">
        <option value="5">⭐⭐⭐⭐⭐ Excellent</option>
        <option value="3">⭐⭐⭐    Average</option>
        <option value="1">⭐        Poor</option>
    </select>

    <!-- Textarea    →  Python: request.form.get('comment') -->
    <label>Comment:</label>
    <textarea name="comment" rows="3"></textarea>

    <button type="submit">Submit Review</button>

</form>

What is Jinja2?

Flask uses Jinja2 — a templating engine — to make HTML dynamic.

It lets us embed Python-like instructions directly inside HTML so we can:

  • Insert Python variables into the page
  • Use for loops to display a list of items
  • Use if/else to show or hide parts of the page

One HTML file becomes a "template" that works with many different data values — we just change what we pass in from Python.

Analogy: A mail-merge letter — same template, different names filled in each time.

Jinja2 Syntax

Two special delimiters to remember:

{{ ... }} — Output a value
Prints the value of a variable onto the page. Like a placeholder that gets replaced.

{% ... %} — Control flow
Used for loops and if statements. Does not print anything itself.

Important: Every {% for %} must be closed with {% endfor %}.

<!-- {{ }} prints the value of the variable 'name' -->
<h1>Hello, {{ name }}!</h1>

<!-- {% %} controls the loop — this line itself prints nothing -->
<ul>
    {% for item in item_list %}
        <!-- {{ item }} prints each item from the list -->
        <li>{{ item }}</li>
    {% endfor %}
</ul>

✏️ Your Turn — Write index.html

Open templates/index.html and write an HTML page that:

  • Has an <h1> heading saying "My To-Do List"
  • Has a <ul> that loops through todos using Jinja2
  • Prints each todo inside an <li>

Hint: use {% for todo in todos %} and {{ todo }}

Give it a go — solution is on the next slide.

Solution: index.html — The List

The {% for %} loop runs once per item in todos. For each item it creates one <li>. This is the most common Jinja2 pattern you will use:

  • Open with {% for item in list %}
  • Write what to repeat between open and close tags
  • Close with {% endfor %}

Note: We use todos in the template because that's the name we'll pass from Python: render_template('index.html', todos=todos)

<!-- templates/index.html -->
<!DOCTYPE html>
<html>
<head>
    <title>My To-Do List</title>
</head>
<body>

    <h1>My To-Do List</h1>

    <ul>
        <!-- Loop over every item in the 'todos' list -->
        {% for todo in todos %}
            <!-- {{ todo }} prints the current item's text -->
            <li>{{ todo }}</li>
        {% endfor %}
    </ul>

</body>
</html>

✏️ Your Turn — Use render_template

Update app.py so the index() function renders your HTML template instead of returning a plain string.

  • 1 Add render_template to the existing import line
  • 2 Replace return "Hello..." with return render_template('index.html', todos=todos)

The second argument todos=todos passes our Python list into the template.

Solution: render_template in app.py

render_template('index.html', todos=todos) does two things:

  • Finds templates/index.html and processes all Jinja2 tags
  • Passes our todos list so the template can access it

After saving, refresh your browser. You should see the two starter tasks listed on the page!

# app.py

# Add render_template to the import line
from flask import Flask, render_template

app = Flask(__name__)

todos = ["Learn Flask", "Build a To-Do App"]

@app.route('/')
def index():
    # render_template finds templates/index.html,
    # fills in the Jinja2 placeholders, and returns finished HTML.
    #
    # todos=todos:
    #   left side  → the variable name the template uses: {{ todos }}
    #   right side → the Python list we defined above
    return render_template('index.html', todos=todos)

if __name__ == '__main__':
    app.run(debug=True)

✏️ Your Turn — Add the Form

Below the list in index.html, add a form that lets users type in a new task.

  • The form uses method="post" and sends to action="/add"
  • Contains a text input with name="todo_item"
  • Has a submit button labelled "Add Task"

The name="todo_item" is how our Python code will read what the user typed.

Solution: The Form in index.html

When the user clicks "Add Task", the browser sends a POST request to /add with the text they typed.

The name="todo_item" attribute on the input is how Python retrieves the value: request.form.get('todo_item')

<!-- templates/index.html -->
<h1>My To-Do List</h1>

<ul>
    {% for todo in todos %}
        <li>{{ todo }}</li>
    {% endfor %}
</ul>

<hr>
<h2>Add a new task</h2>

<!-- action="/add" → sends the data to the /add route in app.py -->
<!-- method="post" → data goes in the request body, not the URL  -->
<form action="/add" method="post">

    <!-- name="todo_item" → Python reads this with request.form.get('todo_item') -->
    <input type="text" name="todo_item" placeholder="Enter a task..." required>

    <button type="submit">Add Task</button>

</form>

✏️ Your Turn — Handle /add in app.py

Add a new route to app.py that handles the form submission.

  • 1 Add request and redirect to the import line
  • 2 Create @app.route('/add', methods=['POST'])
  • 3 Read the value: request.form.get('todo_item')
  • 4 Append it to todos, then redirect('/')

Solution: The /add Route

Why do we redirect instead of rendering the template directly?

If we rendered the template on a POST, refreshing the browser would re-submit the form and add the same task again. Redirecting to / makes the browser do a fresh GET request instead.

This pattern is called Post/Redirect/Get and is a standard best practice in web development.

# app.py

# Add request and redirect to the import line
from flask import Flask, render_template, request, redirect

app = Flask(__name__)
todos = ["Learn Flask", "Build a To-Do App"]

@app.route('/')
def index():
    return render_template('index.html', todos=todos)

@app.route('/add', methods=['POST'])
def add():
    # Read the value typed in the form field named 'todo_item'
    new_todo = request.form.get('todo_item')

    # Only append if the user actually typed something
    if new_todo:
        todos.append(new_todo)

    # Redirect the browser back to the homepage (GET /)
    # This prevents the same item being added again on page refresh
    return redirect('/')

if __name__ == '__main__':
    app.run(debug=True)

Run and Test Your Finished App

Run the server and try these four things to confirm everything works:

  1. The page loads and shows "Learn Flask" and "Build a To-Do App"
  2. Type a new task and click "Add Task" — it appears in the list
  3. Add several more tasks — they all appear
  4. Stop the server (Ctrl+C), restart it, and notice that any added tasks are gone — the in-memory list has reset

Congratulations! You built a working web app with Python and Flask. Next up: Docker — a way to package and ship apps like this anywhere in the world.

# Run from inside todo-app/ with venv active
python app.py

# Open in your browser:
# http://127.0.0.1:5000/

# Test checklist:
# ✓ Page loads with two starter todos
# ✓ Add a task via the form → appears in list
# ✓ No page reload needed — redirect handles it
# ✓ Restart server → manually added tasks are gone (in-memory)

What is Docker?

Docker is an open-source platform for the containerization of applications.

It solves the classic problem: "It works on my machine, but not on the server."


The Shipping Container Analogy

Think of it like a real-world shipping container. Before them, shipping was a mess. You had different-sized boxes, barrels, and sacks. Now, everything goes into a standard container that can be moved by any crane, ship, or truck in the world.

A Docker container does the same thing for software. It packages everything an application needs into a single, standard unit that runs the same way everywhere.

Why Use Containers? The Practical Example

A container includes everything required to run your application in a predictable way:

  • Source Code (e.g., your Python script)
  • Runtime (e.g., Python 3.9)
  • Libraries & Dependencies (e.g., Flask, NumPy, requests)
  • Configuration Files & Environment Variables

Example Scenario: The Python Web App

The Problem (Without Docker):

  • Your Laptop: You build an app using Python 3.9 and Flask 2.0. It works perfectly.
  • The Server: The server has Python 3.7 and an older version of Flask installed for another project. When you deploy your code, it crashes due to version conflicts.

The Solution (With Docker):

  • You package your app, Python 3.9, and Flask 2.0 into a Docker container.
  • You give this single container to the server.
  • Docker runs the container. The app works perfectly because it's running in the exact same environment you built it in, completely isolated from what's on the host server.

Containers vs. Virtual Machines (VMs)

Virtual Machines

App A
Bins/Libs
Guest OS
App B
Bins/Libs
Guest OS
Hypervisor
Host Operating System
Infrastructure (Server)
  • Each VM includes a full guest OS.
  • Heavier, larger (GBs), slower to start.

Containers

App A
Bins/Libs
App B
Bins/Libs
Docker Engine
Host Operating System
Infrastructure (Server)
  • Share the host OS kernel.
  • Lightweight, smaller (MBs), start in seconds.

Installation

There are two primary ways to install Docker:

  1. Docker Desktop:
    • Recommended for local development on Windows, macOS, and Linux.
    • Provides a graphical user interface (GUI) to manage containers, images, and volumes.
  2. Docker Engine:
    • The command-line interface (CLI) version.
    • Ideal for servers that may not have a GUI.

For this course, we will primarily use Docker Desktop and the command line.

Core Docker Concepts

The three main components you'll interact with in Docker are:

  • Images: A read-only blueprint or template for creating containers. It contains the application and all its dependencies.

  • Containers: A runnable, isolated instance of an image. This is your application actually running.

  • Volumes: A mechanism for persisting data generated by and used by Docker containers. Data in volumes survives even after the container is deleted.

What is Docker Hub?

Docker Hub is a registry for Docker images.

  • It's the default public registry used by the Docker command line.
  • Think of it like GitHub for code, but for Docker images.
  • You can find official images for popular software like Python, Postgres, NGINX, etc.
  • You can also push your own custom images to share with others.

Pulling an Image

The `docker pull` command downloads an image from a registry (Docker Hub by default) to your local machine.

Let's pull a simple "hello-world" image.

# Pull the 'hello-world' image from Docker Hub
docker pull hello-world

Running a Container

The `docker run` command creates and starts a new container from a specified image.

When you run `hello-world`, it creates a container, prints a message, and then exits.

The `--rm` flag is useful as it automatically removes the container after it exits.

# Run a container from the hello-world image
docker run hello-world

# Run and automatically remove the container on exit
docker run --rm hello-world

Pull and Run Combined

If you try to `docker run` an image that you don't have locally, Docker will automatically try to `pull` it from Docker Hub first.

You can also specify a command to execute inside the container.

# If the python image is not local, Docker will pull it first.
# Then it runs the container and executes the python command inside it.
docker run --rm python:3.12-slim python -c "print('Hello from a container!')"

Image Tags and Layers

Tags:

  • Tags are used to specify different versions or variants of an image.
  • Example: `python:3.12-slim` requests version 3.12 in its "slim" (lightweight) variant.

Layers:

  • Images are built in layers. Each instruction in a Dockerfile creates a new layer.
  • Images can be based on other images. For example, the `python:3.12-slim` image is built on top of a `debian:12-slim` base image. This makes builds efficient and images reusable.

Listing Resources

You can list your local images and running containers from the command line.

docker image ls shows all images on your machine.

docker ps shows only the currently running containers.

To see all containers, including stopped ones, use the `-a` flag.

# List all local images
docker image ls

# List running containers
docker ps

# List all containers (running and stopped)
docker ps -a

Managing Containers

You can interact with containers using their ID or auto-generated name.

docker logs shows the log output of a container.

docker stop gracefully stops a running container.

# First, find the container ID with docker ps
docker ps

# View logs for a specific container
docker logs <container_id_or_name>

# Stop a running container
docker stop <container_id_or_name>

Cleaning Up

Over time, you can accumulate many stopped containers.

The `docker container prune` command is a convenient way to remove all stopped containers at once.

It will ask for confirmation before deleting.

# Remove all stopped containers
docker container prune

Interacting with a Running Container

You can get an interactive shell inside a running container using `docker exec`.

This is extremely useful for debugging and inspecting the container's environment.

The `-it` flags stand for interactive and TTY (which allocates a pseudo-terminal).

# Start a container in the background (-d for detached)
docker run -d --name my-busybox busybox sleep 3600

# Execute a shell command inside the running container
docker exec -it my-busybox sh

The Problem: Data is Temporary

By default, any data created inside a container's filesystem is tied to the life of that container.

If you create a file, then stop and remove the container, that file is gone forever.

How can we save data permanently, like in a database?

Solution: Docker Volumes.

Using Volumes for Persistent Data

Volumes are managed by Docker and exist outside the container's lifecycle.

You can create a volume and then "mount" it to a specific path inside one or more containers.

The `-v` flag maps `volume-name` to `/path/in/container`.

# 1. Create a named volume
docker volume create my-data-volume

# 2. Run a container and mount the volume
#    Maps 'my-data-volume' to the '/data' directory inside the container
docker run -d --name my-container -v my-data-volume:/data busybox sleep 3600

Containerizing Our Own Application

So far, we've only used pre-built images from Docker Hub.

The real power of Docker comes from packaging your own applications.

To do this, we create a special file called a `Dockerfile`.

A `Dockerfile` is a text file that contains all the commands, in order, needed to build a given image.

The `Dockerfile` - Part 1

Let's look at some core instructions for a Python Flask app.

  • FROM: Specifies the base image to start from.
  • WORKDIR: Sets the working directory for subsequent commands.
  • COPY: Copies files from your local machine into the image.
# Use an official Python runtime as a parent image
FROM python:3.12-slim

# Set the working directory in the container to /app
WORKDIR /app

# Copy the requirements file into the container at /app
COPY requirements.txt .

The `Dockerfile` - Part 2

  • RUN: Executes a command during the image build process (e.g., installing dependencies).
  • EXPOSE: Informs Docker that the container listens on the specified network ports at runtime (documentation).
  • CMD: Provides the default command to execute when the container starts.
# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

# Copy the rest of the application code
COPY . .

# Make port 5000 available to the world outside this container
EXPOSE 5000

# Define the command to run the app
CMD ["python3", "app.py"]

Building the Image

Once your `Dockerfile` is ready, you use the `docker build` command to create the image.

  • The `-t` flag lets you "tag" the image with a name and optional version (e.g., `flask-app:latest`).
  • The `.` at the end specifies that the build context (where Docker looks for files like the `Dockerfile` and your source code) is the current directory.
# Build the image from the Dockerfile in the current directory
docker build -t flask-app:latest .

Running Our Custom Container

To run our custom-built image, we use `docker run` as before.

The crucial addition is the `-p` (publish) flag. It maps a port from your host machine to a port inside the container.

Syntax: `-p <host_port>:<container_port>`.

Now you can access `http://localhost:5000` in your browser to see the app.

# Run the container, mapping port 5000 on the host
# to port 5000 inside the container.
docker run --rm -p 5000:5000 flask-app:latest