Computer Programming 2

Introduction to Docker

Building a Simple To-Do App

We are going to create a very simple web application with the following features:

  • A main page that displays a list of to-do items.
  • A form to add new items to the list.
  • The list will update immediately after a new item is added.

Step 1: Project Setup

First, let's set up our project. We need one Python file for our Flask logic and a folder to hold our HTML templates.

Install Flask if you haven't already:

python -m venv .venv
# on macOS/Linux
source .venv/bin/activate  
# On Windows use `.venv\Scripts\activate`
pip install Flask

Then, create the following folder and file structure:


todo-app/
├── app.py
└── templates/
    └── index.html
      

Step 2: The Basic Flask App

Let's start with a minimal "Hello World" Flask application in our app.py file.

This code imports Flask, creates an application instance, defines a single route /, and makes the app runnable.

# app.py
from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return "Hello, To-Do App!"

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

Step 3: Storing To-Dos

For this simple app, we won't use a database. We'll store our to-do items in a simple Python list.

This list will act as our in-memory "database". Note that it will reset every time you restart the server.

# app.py
from flask import Flask

app = Flask(__name__)

# Our in-memory "database"
todos = ["Learn Flask Basics", "Build a To-Do App"]

@app.route('/')
def index():
    return "Hello, To-Do App!"

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5000)

A Quick Intro to HTML

Before we create our template, let's talk about HTML. HTML stands for HyperText Markup Language. It's the standard language for creating web pages.

It describes the structure of a web page using elements, which are represented by "tags". Think of it as the skeleton of a website.

  • Tags usually come in pairs like <p> and </p>.
  • The first tag is the start tag, the second is the end tag.

Common HTML Tags

Here are some basic tags we will use in our to-do app:

  • <h1>: A main heading.
  • <ul>: An "unordered list" (a bulleted list).
  • <li>: A "list item" that goes inside a <ul>.

Together, they create a structured list on the page.

<!-- Example of a simple list -->
<h1>My Shopping List</h1>
<ul>
    <li>Milk</li>
    <li>Bread</li>
    <li>Cheese</li>
</ul>

HTML Forms

To get input from a user, we use the <form> tag. Key parts of a form are:

  • <form>: The container.
    • action: The URL where the data is sent.
    • method: How the data is sent (e.g., `post`).
  • <input>: A field for user input. Its `name` attribute is crucial for the server to identify the data.
  • <button>: Submits the form.
<!-- Example of a simple form -->
<form action="/submit-data" method="post">
    <label>Your Name:</label>
    <input type="text" name="username">
    <button type="submit">Submit</button>
</form>

What is Jinja2?

Flask uses a powerful tool called Jinja2. It's a "templating engine."

A templating engine lets us embed Python-like code directly into our HTML files. This is how we make our web pages dynamic. Instead of writing static HTML, we can:

  • Insert variables from our Python code into the page.
  • Use loops to display lists of items.
  • Use if/else conditions to show or hide content.

This allows us to use one HTML file as a "template" for many different pieces of data.

Basic Jinja2 Syntax

Jinja2 has two primary delimiters that you'll see in your HTML templates:

{{ ... }} for Expressions:

This is used to print a variable or the result of an expression to the template. Think of it as a placeholder that gets filled in.

{% ... %} for Statements:

This is used for control flow, like for loops or if statements. It controls the structure of the template.

<!-- Jinja2 Syntax Examples -->

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

<!-- A for loop to create list items -->
<ul>
{% for item in item_list %}
    <li>{{ item }}</li>
{% endfor %}
</ul>

Step 4: Using Templates

Instead of returning plain text, let's render an HTML file. We use Flask's render_template function for this.

We'll also pass our todos list to the template so we can display it on the webpage.

# app.py
from flask import Flask, render_template

app = Flask(__name__)

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

@app.route('/')
def index():
    # Pass the 'todos' list to the template
    return render_template('index.html', todos=todos)

# ... (if __name__ == '__main__') ...

Step 5: Creating the HTML

Now, let's create the index.html file inside the templates folder.

We'll use a Jinja2 for loop to iterate over the todos list we passed from our Python code and display each item in a list.


<h1>My To-Do List</h1>
<ul>
    {% for todo in todos %}
        <li>{{ todo }}</li>
    {% endfor %}
</ul>
    

Step 6: The "Add" Form

To add new to-dos, we need an HTML form in our index.html.

This form will send the new task data to a new URL, /add, using the POST method.

<!-- templates/index.html -->
<hr>
<h2>Add a new task</h2>
<form action="/add" method="post">
    <input type="text" name="todo_item" required>
    <button type="submit">Add Task</button>
</form>

Step 7: Handling Form Data

In app.py, we create a new route /add that only accepts POST requests.

It gets the form data, appends it to our todos list, and then redirects the user back to the main page to see the updated list.

# app.py
from flask import Flask, render_template, request, redirect

# ... (app and todos list setup) ...

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

@app.route('/add', methods=['POST'])
def add():
    new_todo = request.form.get('todo_item')
    todos.append(new_todo)
    return redirect('./')

# ... (if __name__ == '__main__') ...

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