Containerize and Auto-Deploy a Web App

Build a containerized Python web app with an automated CI/CD pipeline.

Introduction

30 Second Summary

Think about how apps like Netflix or Spotify update without you ever downloading a new version. Behind the scenes, engineers have built automated systems that package code and ship it to users instantly.

In this project, you will build a Python web app, package it inside a Docker container, and set up an automated pipeline that rebuilds your app every time you push code changes.

What You'll Build

A containerized Python web server with a CI/CD pipeline that automatically builds and publishes your Docker image on every code push to GitHub.

By the end of this project, you'll have:

  • A Python web app running inside a Docker container on your local machine.
  • A GitHub Actions CI/CD pipeline that automatically builds your Docker image on every push.
  • A published Docker image on GitHub Container Registry that anyone can pull and run.
  • Secret Mission: Add health check and system info endpoints to make your app production-ready.

Are there any prerequisites?

None. This project walks you through installing every tool from scratch, including Visual Studio Code, Git, and Docker Desktop. You just need a Windows computer with at least 8GB of RAM.

Create Your GitHub Repository

Your tools are locked in. You have VS Code for editing code, Git for tracking changes, and Docker for running containers.

Now you need a home for your code online. GitHub is where developers store, share, and collaborate on code. It also provides the CI/CD automation service you will use later to build your container image automatically.

In this step, get ready to:
  • Create a GitHub account and repository.
  • Clone the repository to your local machine.
  • Open the project folder in VS Code.
Create a GitHub account and repository
  • Open your browser and go to https://github.com.
  • If you do not have an account yet, click Sign up and follow the prompts to create a free account.
  • Once you are signed in, click the + icon in the upper-right corner of any page.
  • Click New repository.
  • Type my-first-container in the Repository name field.
  • Select Public for the visibility setting.
  • Check the box next to Add a README file.
  • Click Create repository.

What is a repository?

A repository (or "repo") is a project folder that lives on GitHub. It stores all your files and tracks every change you make over time.

By making it public, anyone can see your code. This is required for the free CI/CD pipeline you will set up in a later step.

Clone your repository to your computer

Cloning copies your GitHub repository to your computer so you can edit files locally. The clone creates a folder with the same name as your repository.

  • Move to your Desktop by running this command in PowerShell:
cd ~/Desktop
  • Clone your repository by running this command:
git clone https://github.com/[[GITHUB_USERNAME="your-github-username"]]/my-first-container.git

What does this command do?

  • git clone downloads the entire repository from GitHub to your computer.
  • It creates a new folder called my-first-container on your Desktop containing the README.md file you added when creating the repo.
  • Confirm the folder was created by running:
ls

You should see my-first-container in the output.

Don't see the folder?

  • Double-check the URL you typed. The username must exactly match your GitHub username (case-sensitive).
  • Make sure you are connected to the internet. git clone downloads from GitHub's servers.
  • If you see "repository not found", confirm the repository is set to Public on GitHub.

Help me troubleshoot my git clone command

  • Move into your project folder by running this command:
cd my-first-container
  • Open the folder in VS Code by running:
code .

VS Code opens a new window showing the my-first-container folder. You should see README.md listed in the Explorer sidebar on the left.

VS Code did not open?

  • If code is not recognized, close PowerShell completely, reopen it, and try again. The PATH update from the VS Code installer sometimes needs a fresh terminal session.
  • You can also open VS Code manually. Press the Windows key, type VS Code, and press Enter. Then click File in the top menu bar, click Open Folder, and select the my-first-container folder on your Desktop.

Help me get VS Code open to my project folder

Your project has a home on GitHub and is open in VS Code. Next up, you will write the Python web app that will run inside your container.

Write Your Python Web App

Your GitHub repository is cloned and open in VS Code. Now it is time to write the actual application that will run inside your container.

You will create a simple web server using Python's built-in http.server module. This means zero extra packages to install. The server displays a personal dashboard page proving your container is alive and running.

In this step, get ready to:
  • Create a Python web app file in your project.
  • Test the app locally in your browser.
Create your app.py file

Your web app lives in a single Python file. It uses Python's standard library to serve an HTML page, so you do not need to install any third-party packages.

  • In VS Code's Explorer sidebar on the left, right-click inside the file list area.
  • Select New File.
  • Type app.py as the file name and press Enter.

The new file opens in the editor. This is where you will write your web server code.

  • Copy the following code into app.py to create the DashboardHandler class:
from http.server import HTTPServer, BaseHTTPRequestHandler
import datetime

class DashboardHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/":
            self.send_response(200)
            self.send_header("Content-type", "text/html")
            self.end_headers()
            html = """<!DOCTYPE html>
<html>
<head><title>Platform Engineer Dashboard</title>
<style>
body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; background: #1a1a2e; color: #eee; padding: 20px; }
h1 { color: #0f3460; background: #e94560; padding: 20px; border-radius: 8px; text-align: center; }
.card { background: #16213e; padding: 20px; border-radius: 8px; margin: 20px 0; }
.status { color: #4ecca3; font-weight: bold; }
</style></head>
<body>
<h1>Platform Engineer in Training</h1>
<div class="card">
<p class="status">Container Status: RUNNING</p>
<p>App started at: """ + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + """</p>
</div>
<div class="card">
<h3>Skills Being Built:</h3>
<ul>
<li>Docker Containers</li>
<li>CI/CD Pipelines</li>
<li>GitHub Actions</li>
<li>Container Registries</li>
</ul>
</div>
</body></html>"""
            self.wfile.write(html.encode())
        else:
            self.send_response(404)
            self.end_headers()

What does this code do?

DashboardHandler is a class that handles incoming web requests. When someone visits the root path (/), it sends back an HTML page styled as a dark-themed dashboard.

The HTML includes a status card showing when the app started and a list of skills you are building in this project. Any other path gets a 404 (not found) response.

  • Below the class, add the server startup code that actually runs your app:
if __name__ == "__main__":
    server = HTTPServer(("0.0.0.0", 8080), DashboardHandler)
    print("Server running on http://localhost:8080")
    server.serve_forever()

What does this code do?

This block runs when you execute the file directly. It creates an HTTPServer that listens on port 8080 and uses your DashboardHandler to respond to every request.

The address 0.0.0.0 means the server accepts connections from anywhere, not just your own machine. This is important later when the app runs inside a container.

  • Save app.py by pressing Ctrl+S.

You will not see a visible change yet. The next substep runs the server so you can see your dashboard in the browser.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete app.py file. Make sure your code matches this exactly:

from http.server import HTTPServer, BaseHTTPRequestHandler
import datetime

class DashboardHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/":
            self.send_response(200)
            self.send_header("Content-type", "text/html")
            self.end_headers()
            html = """<!DOCTYPE html>
<html>
<head><title>Platform Engineer Dashboard</title>
<style>
body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; background: #1a1a2e; color: #eee; padding: 20px; }
h1 { color: #0f3460; background: #e94560; padding: 20px; border-radius: 8px; text-align: center; }
.card { background: #16213e; padding: 20px; border-radius: 8px; margin: 20px 0; }
.status { color: #4ecca3; font-weight: bold; }
</style></head>
<body>
<h1>Platform Engineer in Training</h1>
<div class="card">
<p class="status">Container Status: RUNNING</p>
<p>App started at: """ + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + """</p>
</div>
<div class="card">
<h3>Skills Being Built:</h3>
<ul>
<li>Docker Containers</li>
<li>CI/CD Pipelines</li>
<li>GitHub Actions</li>
<li>Container Registries</li>
</ul>
</div>
</body></html>"""
            self.wfile.write(html.encode())
        else:
            self.send_response(404)
            self.end_headers()

if __name__ == "__main__":
    server = HTTPServer(("0.0.0.0", 8080), DashboardHandler)
    print("Server running on http://localhost:8080")
    server.serve_forever()
Test your app locally

Before putting your app into a container, you can run it directly with Python to confirm it works. This gives you a fast feedback loop.

  • Open the integrated terminal in VS Code by pressing Ctrl+` (the backtick key, above Tab).
  • Start your web server by running this command:
python app.py

You should see the message Server running on http://localhost:8080 printed in the terminal.

  • Open your web browser (Edge, Chrome, or Firefox).
  • Type http://localhost:8080 in the address bar and press Enter.

You should see your dark-themed dashboard page with the title "Platform Engineer in Training" and a green status message showing "Container Status: RUNNING".

  • Go back to VS Code's terminal and press Ctrl+C to stop the server.

Why stop the server?

Pressing Ctrl+C sends a stop signal to the running process. You need to stop it so the terminal is free for the commands in the next step.

Seeing a Microsoft Store page or "Python was not found"?

Windows includes stub executables that redirect the python command to the Microsoft Store if Python is not installed. You need to install Python from python.org/downloads. During installation, check the box labelled Add python.exe to PATH.

After installing, close and reopen your terminal in VS Code, then try python app.py again.

help me fix "Python was not found" on Windows so I can run my app.py server

Your app works locally. Next up, you will package it into a Docker container so it can run the same way on any machine.

Containerize Your App with Docker

In the previous step, you tested your Python web app by running it directly on your machine. That works, but it means anyone who wants to run your app needs the exact same Python version and setup you have.

Containers solve this problem by packaging your app together with everything it needs to run. In this step, you will create a Dockerfile that tells Docker how to package your app into a container, then build and run that container locally.

In this step, get ready to:
  • Create a Dockerfile that packages your Python app into a container image.
  • Build and run your Docker container locally.
  • Verify your containerized app works in the browser.
Create your Dockerfile

A Dockerfile is a text file with instructions that tell Docker exactly how to build your container image. Each line is a step in the process: which base image to start from, what files to copy in, and what command to run when the container starts.

  • In VS Code, right-click in the Explorer sidebar (the file panel on the left).
  • Click New File.
  • Name the file exactly Dockerfile with no file extension. No .txt, no .docker. Just Dockerfile.
  • Paste the following code into your new Dockerfile:
FROM python:3.12-slim

WORKDIR /app

COPY app.py .

EXPOSE 8080

CMD ["python", "app.py"]

What does each line do?

  • FROM python:3.12-slim starts from an official Python image that already has Python installed. The slim variant keeps the image small by excluding extras you don't need.
  • WORKDIR /app creates a folder called /app inside the container and makes it the working directory for all following commands.
  • COPY app.py . copies your app.py file from your computer into the container's /app folder.
  • EXPOSE 8080 documents that your app listens on port 8080. It does not publish the port automatically.
  • CMD ["python", "app.py"] tells Docker what command to run when the container starts.
  • Save Dockerfile by pressing Ctrl+S.
  • Confirm the file appears in the Explorer sidebar next to app.py.

Don't see Dockerfile in the sidebar?

Make sure the file is named exactly Dockerfile with a capital D and no extension. If you accidentally named it Dockerfile.txt, right-click it in the sidebar and select Rename to fix it.

help me fix my Dockerfile naming

✔️ Awesome, I've got everything!

Great. Make sure you have saved your Dockerfile before moving on.

ⓧ I'd like to double check the full code

Here is the complete Dockerfile for comparison:

FROM python:3.12-slim

WORKDIR /app

COPY app.py .

EXPOSE 8080

CMD ["python", "app.py"]
Build and run your Docker container

Your Dockerfile is a recipe. Now you will use it to actually build the container image and run it.

  • Open the terminal in VS Code by pressing Ctrl+` (the backtick key, above Tab).
  • Build your Docker image by running this command:
docker build -t my-first-container .

What does this command do?

  • docker build reads your Dockerfile and builds an image from it.
  • -t my-first-container gives the image a name (a "tag") so you can refer to it later.
  • The . at the end tells Docker to use the current folder as the build context (where to find your files).
  • Wait for the build to finish. You will see Docker downloading the Python base image and running each step in your Dockerfile.
  • You should see a line that says something like Successfully tagged my-first-container:latest at the end.

Seeing an error during the build?

Make sure Docker Desktop is running. You should see the Docker whale icon in your system tray. If it shows "Docker Desktop is starting", wait until it finishes.

Also check that your terminal is in the same folder as your Dockerfile. Run ls (or dir on Windows) and confirm you can see both app.py and Dockerfile listed.

help me fix my Docker build error

  • Now run your container by entering this command:
docker run -p 8080:8080 my-first-container

What does -p 8080:8080 mean?

The -p 8080:8080 flag maps port 8080 on your computer to port 8080 inside the container. Without this, your browser cannot reach the app running inside the container.

  • You should see Server running on http://localhost:8080 printed in your terminal.
  • Open your browser and go to http://localhost:8080.
  • You should see your "Platform Engineer in Training" dashboard page. This time, it is being served from inside a Docker container.
  • Press Ctrl+C in the terminal to stop the container.

Don't see your dashboard in the browser?

If the page does not load, make sure you did not skip the -p 8080:8080 flag in the run command. Without it, the container's port is not connected to your machine.

Also confirm the previous test server from Step 3 is not still running. Two programs cannot use the same port at the same time.

help me access my Docker container in the browser

Understand what you just built

Take a moment to appreciate what just happened. You packaged your entire application into a single portable unit.

Why are containers such a big deal?

Your container image includes your code, the Python runtime, and the operating system layer it needs. Anyone with Docker can run your image and get the exact same result, regardless of what operating system or software they have installed.

This is exactly what platform engineers provide to development teams. Instead of saying "here are 15 steps to install my app", you hand them a container that runs with one command.

What is a container image vs. a container?

A container image is the packaged snapshot you built with docker build. A container is a running instance of that image, created with docker run. You can run multiple containers from the same image.

Your app is containerized and running locally. Next up, you will set up an automated pipeline so that every time you push code to GitHub, your container image gets rebuilt and published automatically.

Set Up Automated CI/CD with GitHub Actions

You now have a working containerized web app. You built and ran it locally, which is great for testing. But what happens when you change your code?

Rebuilding the image by hand every single time gets old fast. This is where CI/CD comes in. You will create a GitHub Actions workflow that automatically builds your Docker image and publishes it to GitHub Container Registry every time you push code.

In this step, get ready to:
  • Create a GitHub Actions workflow file that defines your CI/CD pipeline.
  • Push your code to GitHub and watch the pipeline run automatically.
  • Verify your published container image on GitHub Container Registry.
Create the GitHub Actions workflow file

GitHub Actions looks for workflow files inside a special folder called .github/workflows/ in your repository. Each YAML file in that folder defines a separate pipeline.

  • In VS Code's Explorer sidebar (left panel), right-click on the empty space below your existing files.
  • Click New File.
  • Type .github/workflows/build.yml and press Enter.

Why type the full path?

When you type a file path with slashes, VS Code automatically creates all the intermediate folders for you. Typing .github/workflows/build.yml creates the .github folder, the workflows folder inside it, and the build.yml file.

  • Paste the following workflow configuration into build.yml:
name: Build and Push Container Image

on:
  push:
    branches:
      - main

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest

    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata for Docker
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

      - name: Build and push Docker image
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

What does this workflow do?

This YAML file tells GitHub Actions what to do every time you push code to the main branch:

  • Checkout repository downloads your code onto a cloud server (called a runner).
  • Log in to GitHub Container Registry authenticates with ghcr.io so the runner can push images there.
  • Extract metadata generates the correct image tags and labels automatically from your repository name.
  • Build and push reads your Dockerfile, builds the image (just like you did locally), and pushes it to the registry.

The secrets.GITHUB_TOKEN is provided automatically by GitHub. You do not need to create it yourself.

  • Save build.yml by pressing Ctrl+S.
  • Confirm the file appears in your Explorer sidebar under .github > workflows > build.yml.

Don't see the .github folder?

If VS Code did not create the folders, try right-clicking again and selecting New File. Make sure you type the full path .github/workflows/build.yml including the leading dot. The dot makes it a hidden folder, but VS Code still shows it in the Explorer sidebar.

Help me create the .github/workflows/build.yml file in VS Code

Push your code to GitHub

Your workflow file only runs when code is pushed to GitHub. Right now, all your files (app.py, Dockerfile, and build.yml) exist only on your local machine.

  • Open the terminal in VS Code by pressing Ctrl+` (the backtick key, usually above Tab).
  • Stage all your files, create a commit, and push to GitHub by running these commands:
git add .
git commit -m "Add containerized web app with CI/CD"
git push origin main

What do these three commands do?

  • git add . stages all new and changed files in your project for the next commit.
  • git commit -m "..." saves a snapshot of your staged files with a descriptive message.
  • git push origin main uploads your commit to the main branch on GitHub.

GitHub may ask you to sign in

If this is your first time pushing from this computer, a browser window will open asking you to sign in to GitHub. Follow the prompts to authorize Git. After signing in, return to your terminal and the push will continue automatically.

  • Go to your repository on GitHub in your browser: https://github.com/YOUR-USERNAME/my-first-container.
  • Click the Actions tab at the top of the page.
  • You should see a workflow run named Add containerized web app with CI/CD (your commit message). Click it.
  • Wait for the workflow to finish. This takes about 1-2 minutes. When it completes, you will see a green checkmark next to the build-and-push job.

Seeing a red X instead of a green checkmark?

Click the failed run to see the error details. Common causes:

  • Your repository is set to Private. GitHub Container Registry requires a Public repository for free package publishing. Go to Settings > General > scroll to Danger Zone > Change visibility and set it to Public.
  • A typo in build.yml. YAML is sensitive to indentation. Make sure you copied the full file exactly as shown above.

Help me fix my failing GitHub Actions workflow

Verify your published container image

Your pipeline just built and pushed a Docker image to GitHub Container Registry. This means anyone in the world can now pull and run your app with a single command.

  • Go to your GitHub profile page (click your avatar in the top-right corner, then click Your profile).
  • Click the Packages tab.
  • You should see my-first-container listed as a package. This is your published container image.

What does this mean?

Anyone can now run your app on their own machine by pulling your image from ghcr.io. The command would look like this:

docker pull ghcr.io/YOUR-USERNAME/my-first-container:main

You packaged your code, its runtime, and its environment into a portable unit. Then you automated the entire build-and-publish process. Every future code change triggers the same pipeline automatically.

Don't see the Packages tab?

The Packages tab only appears on your profile page, not on the repository page. Click your profile avatar in the top-right corner of GitHub, select Your profile, and look for the Packages tab next to Repositories.

Help me find my published container image on GitHub

✔️ Awesome, I've got everything!

Great. Double check you have saved all your files before moving on.

ⓧ I'd like to double check the full code

Here is the complete build.yml file. Make sure yours matches exactly:

name: Build and Push Container Image

on:
  push:
    branches:
      - main

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest

    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata for Docker
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

      - name: Build and push Docker image
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

Your automated pipeline is live. From now on, every code change you push triggers a fresh build and publish. Next up, you will take on a Secret Mission to add production-ready features to your app.

Secret mission

Add a Health Check Endpoint and a System Info Page

Your container is running, but how do monitoring systems know it's healthy? In production, every service needs endpoints that report its status automatically. Add a /health and /info endpoint to your app, then teach Docker to monitor your container's health on its own.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally and uses free GitHub services, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Local project folder my-first-container on your Desktop.
  • Local Docker images built during the project.
  • GitHub repository my-first-container.
  • Container image published to GitHub Container Registry (ghcr.io).
  • Docker Desktop (installed application).
  • Visual Studio Code (installed application).
  • Git (installed application).
  • GitHub account.

Keep everything running

No action needed. Choose this if you are still actively building or plan to extend this project soon.

  • Your Docker Desktop will continue running in the background and use some memory.
  • Your GitHub repository and container image remain publicly accessible.
  • Your local project files stay on your Desktop.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your files and repository stay intact for when you return.

  • Stop any running Docker containers by opening PowerShell and running the command below.
  • Quit Docker Desktop by right-clicking its icon in the system tray and selecting Quit Docker Desktop.
docker stop $(docker ps -q)

This frees up memory without removing any files, images, or cloud resources.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below to fully clean up.

Delete local Docker images
  • Open PowerShell and remove the project image and clean up unused data by running:
docker rmi my-first-container
docker system prune -f
Delete local project folder
  • Remove the my-first-container folder from your Desktop by running:
Remove-Item -Recurse -Force ~\Desktop\my-first-container
Delete container image on GitHub Container Registry
  • Go to your my-first-container repository on GitHub.
  • In the right sidebar, click Packages.
  • Click the my-first-container package.
  • On the package page, click Package settings on the right-hand side.
  • Scroll to the bottom and under Danger Zone, click Delete this package.
  • Type the package name to confirm, then click I understand, delete this package.
Delete GitHub repository
  • Go to your my-first-container repository on GitHub.
  • Click Settings in the repository navigation bar.
  • Scroll down to the Danger Zone section and click Delete this repository.
  • Click I want to delete this repository.
  • Click I have read and understand these effects.
  • Type your repository name to confirm, then click Delete this repository.
Uninstall Docker Desktop
  • Press the Windows key and type Add or remove programs. Press Enter.
  • Search for Docker Desktop in the list.
  • Click the three dots next to it and select Uninstall.
Uninstall Visual Studio Code
  • In the same Add or remove programs window, search for Visual Studio Code.
  • Click the three dots next to it and select Uninstall.
Uninstall Git
  • In the same Add or remove programs window, search for Git.
  • Click the three dots next to it and select Uninstall.
Delete GitHub account (optional)
  • You likely want to keep your GitHub account for future projects. If you do want to delete it, go to https://github.com/settings/admin and follow the account deletion steps.

That's a Wrap!

That's a Wrap!

Nice work! You just completed the foundational workflow of platform engineering: code, container, automated pipeline, registry.

You've learned how to:

  • Written a Python web application from scratch and packaged it into a Docker container for portable, reproducible deployments.
  • Set up a GitHub Actions CI/CD pipeline that automatically builds and publishes your container image on every push.
  • Published a container image to GitHub Container Registry so anyone can pull and run your app with a single command.
  • Added health check and observability endpoints to make your container production-ready.

Ready to quiz yourself?