Build a Python Health API with CI

Build, test, and containerize a FastAPI health API with GitHub Actions.

Introduction

30 Second Summary

A running app can still be unable to handle real traffic. Teams need clear signals before they send users its way.

In this project, you will build a Python health API with FastAPI. A GitHub Actions continuous integration workflow will show the missing readiness behavior in red before proving the fix in green.

What You'll Build

Your finished repository tells the story from a red readiness check to a green workflow that proves the fixed API works inside a Docker image.

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

  • A health endpoint you can open at /health to see {"status":"ok"}. An interactive /docs page lets you call it from the browser.
  • A readiness contract that fails on purpose while /ready is missing. The same test passes after the API returns {"status":"ready"}.
  • A public CI workflow that tests the API on a hosted runner. It builds a Docker image. It runs the same tests inside that image.
  • Secret Mission: Add a /version endpoint whose response changes through a container environment variable.

Are there any prerequisites?

Basic Python syntax plus a free GitHub account are all you need because the image builds on a hosted runner. A local Docker installation is optional.

Before We Start

This checkpoint gives you a clear goal for what the health API should demonstrate about your Python backend and DevOps skills before the hands-on work begins.

Set Up the Windows Project Environment

A reproducible backend depends on every machine using known tools. Machine-specific differences can make the same code behave differently.

You’ll use PowerShell to verify Python, Git for Windows, and Visual Studio Code. You’ll use a virtual environment to isolate the project’s packages.

You’ll also confirm access to a verified GitHub account. That gives the project a clear path to continuous integration later.

In this step, get ready to:
  • Verify the required Windows development tools.
  • Prepare your GitHub access plus local workspace.
  • Install the pinned Python packages inside an isolated environment.
Verify the local tools

Version checks reveal whether your computer can reproduce the project environment. Start with the three tools that support every later step.

  • Press the Windows key to open Windows search.
  • Type PowerShell into the search bar.
  • Press Enter to open PowerShell.
  • Check the installed tool versions by running these commands:
python --version
git --version
code --version

What do these checks show?

  • The Python check prints the interpreter version available through the python command.
  • The Git check prints the installed Git for Windows version.
  • The Visual Studio Code check prints its version information plus architecture.

✔️ I see the required tool versions

Your local tools are ready. Continue below to repeat the checks as one clean verification.

ⓧ I see an older pinned version

An older interpreter or Git release can produce different results from the project environment. Update each pinned tool that does not match.

ⓧ One or more commands are unavailable

A missing command means its tool is absent from your computer or unavailable through PowerShell. Install only the tools whose checks failed.

  • Install the Python Install Manager when the Python check fails by running:
winget install 9NQ7512CXL7T -e --accept-package-agreements --disable-interactivity

What does this command do?

WinGet installs the official Python Install Manager for your Windows account. The manager provides the supported route to Python 3.14.7.

  • Use the Python Install Manager to install Python 3.14.7.
  • Install Git for Windows when the Git check fails by running:
winget install --id Git.Git -e --source winget

What does this command do?

WinGet downloads Git for Windows from its registered package source. The current maintained release is 2.55.0.

New installations update the commands available to fresh PowerShell sessions. Close the current window before checking the tools again.

  • Close PowerShell.
  • Press the Windows key to reopen Windows search.
  • Type PowerShell into the search bar.
  • Press Enter to open a fresh PowerShell window.
  • Verify the three tools again by running:
python --version
git --version
code --version

What does this verification prove?

The fresh PowerShell session reads the updated command paths. Successful output proves each tool is available from the terminal.

You should see Python 3.14.7, Git for Windows 2.55.0, and Visual Studio Code version information.

A tool still missing?

Restart Windows if a newly installed command remains unavailable. This refreshes the command paths inherited by PowerShell.

Check that you installed the Windows User setup when Visual Studio Code works from search but its terminal command still fails.

Help me diagnose which Windows tool is missing from PowerShell.

Confirm your GitHub account and create the workspace

The project eventually publishes its test workflow through GitHub. A verified account ensures you can create that repository when the code is ready.

✔️ I already have a verified account

  • Sign in to GitHub in your browser.
  • Confirm that your signed-in account name appears on the GitHub home page.

Your verified account is ready for the repository step later in the project.

ⓧ I need to create an account

  • Visit the GitHub signup page in your browser.
  • Create a free personal account by following the signup prompts.
  • Verify the email address connected to the account.
  • Return to GitHub after verification.

You should see your signed-in account name on the GitHub home page.

The local workspace gives every project file one predictable home. PowerShell’s current location also determines which folder Visual Studio Code opens.

  • Move to your Desktop by running this command:
cd ~/Desktop

What does this command do?

PowerShell expands ~ to your Windows home directory. The command moves your terminal into its Desktop folder.

  • Create the workspace plus open it in Visual Studio Code by running these commands:
mkdir python-health-api
cd python-health-api
code .

What do these commands do?

  • The first command creates the python-health-api folder on your Desktop.
  • The second command moves PowerShell into that folder.
  • The final command opens the current folder in Visual Studio Code.

You should see an empty python-health-api workspace in Visual Studio Code. Your PowerShell prompt should end with the same folder name.

Visual Studio Code did not open?

Close PowerShell before opening a fresh session if you installed Visual Studio Code during this step. The terminal command becomes available after PowerShell reloads its command paths.

Help me open my project folder from PowerShell.

Install the pinned project dependencies

A requirements file records the exact package versions the API needs. A virtual environment keeps those packages separate from other Python projects on your computer.

  • Select the file-shaped plus control beside python-health-api in the left sidebar of Visual Studio Code.
  • Enter requirements.txt as the new file name.

You should see requirements.txt listed inside the workspace.

  • Fill requirements.txt with the four exact package pins by copying this code:
fastapi==0.141.1
httpx==0.28.1
pytest==9.1.1
uvicorn==0.52.4

What does this file control?

  • FastAPI 0.141.1 provides the API framework.
  • HTTPX 0.28.1 supports the API test client.
  • pytest 9.1.1 discovers and runs the automated tests.
  • Uvicorn 0.52.4 runs the API during local development.
  • Save requirements.txt in Visual Studio Code.
  • Confirm that all four package lines remain visible in the saved file.

Package pins look different?

Check each package name for lowercase spelling. Make sure every version uses two equals signs.

Help me compare my requirements file with the project pins.

✔️ Awesome, I've got everything!

Your dependency file now matches the project’s exact package set.

ⓧ I'd like to double check the full code

fastapi==0.141.1
httpx==0.28.1
pytest==9.1.1
uvicorn==0.52.4

How should I compare this file?

Compare each line with your saved requirements.txt file. The package order plus every version must match exactly.

The dependency list defines what to install. The next command creates the isolated location that receives those packages.

  • Create the .venv virtual environment in the PowerShell window from earlier by running:
python -m venv .venv

What does this command do?

Python creates an isolated interpreter plus package directory inside .venv. Project installations can now stay separate from system-level packages.

  • Confirm that .venv appears in the Visual Studio Code file sidebar.

The virtual environment folder is missing?

Check that PowerShell’s prompt ends with python-health-api. Running the command from another location creates .venv in the wrong folder.

Help me find where my virtual environment was created.

  • Activate the new virtual environment by running:
\.venv\Scripts\Activate.ps1

What does activation change?

Activation points the current PowerShell session at the interpreter inside .venv. Package commands now affect this project environment.

✔️ I see the environment name

Your PowerShell prompt should begin with (.venv). The isolated environment is active.

ⓧ PowerShell blocks the activation script

Windows can restrict local PowerShell scripts for your user account. The documented current-user policy allows the environment’s activation script to run.

  • Allow locally created PowerShell scripts for your current account by running:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

What does this command change?

The policy change applies only to your Windows user account. It allows the virtual environment’s local activation script to run.

  • Retry the virtual environment activation by running:
\.venv\Scripts\Activate.ps1

What does this retry confirm?

The retry activates the interpreter inside .venv after the policy update. A successful prompt begins with (.venv).

The active environment is ready to receive the packages recorded in requirements.txt.

  • Install the pinned project dependencies by running:
python -m pip install -r requirements.txt

What does this command do?

Python runs the package installer from the active virtual environment. The installer reads each exact version from requirements.txt.

PowerShell prints download plus installation progress. A successful run finishes without a package installation error.

Dependency installation failed?

Check that the PowerShell prompt starts with (.venv). Confirm that requirements.txt contains the exact four saved lines.

Help me diagnose this dependency installation failure.

Before you run the final check, which tool versions plus package entries do you expect PowerShell to report?

  • Verify the complete Windows environment by running:
python --version
git --version
code --version
python -m pip list

What does the final check prove?

The first three commands verify the local development tools. The final command lists packages from the active .venv environment.

You should see Python 3.14.7, Git for Windows 2.55.0, and Visual Studio Code version information. The package list should include FastAPI 0.141.1, HTTPX 0.28.1, pytest 9.1.1, and Uvicorn 0.52.4.

That’s the setup complete. Your Windows workspace now has the verified toolchain plus isolated dependencies needed for the health API.

Your project environment is reproducible and ready. Next up, you’ll launch the first visible health endpoint.

Launch the Health Endpoint

Your isolated Python environment now holds every dependency this API needs. The next useful proof is a service that answers a real request.

A health response proves that an application process can answer traffic. In this step, you'll use FastAPI to expose that signal through /health before readiness gets its own contract.

In this step, get ready to:
  • Define the FastAPI application that serves /health.
  • Run the service locally with reload enabled.
  • Verify the health response through your browser.
Create the health endpoint

A path operation connects an HTTP method plus a URL path to a Python function. Your first path operation returns a small response that monitoring tools can request.

  • Use the new-file icon in Visual Studio Code's file sidebar to create main.py inside python-health-api.
  • Define the complete starter service in main.py by pasting this code:
from fastapi import FastAPI

app = FastAPI(title="Deployment Health API")


@app.get("/health")
def health() -> dict[str, str]:
    return {"status": "ok"}

What does this code do?

  • The FastAPI class supplies the application and routing features.
  • The app object represents the service that the local server loads.
  • The @app.get("/health") decorator connects GET requests for /health to health().
  • The health() function returns a Python dictionary. FastAPI converts that dictionary into a JSON response.
  • Save main.py with Ctrl+S.
  • Confirm the file is listed beneath python-health-api in Visual Studio Code's file sidebar.

Your project now contains the application object and its first route.

Can't find the saved file?

Confirm that the file is named main.py without an extra file extension. Make sure it sits directly inside python-health-api.

Help me check my file location and Python code.

✔️ Awesome, I've got everything!

Your main.py file is saved. Continue to the next substep to run it.

ⓧ I'd like to double check the full code

from fastapi import FastAPI

app = FastAPI(title="Deployment Health API")


@app.get("/health")
def health() -> dict[str, str]:
    return {"status": "ok"}

How to compare the file

Your file should begin with the FastAPI import. The app definition should follow it.

The /health path operation should match the reference exactly.

Start the development server

Uvicorn runs your FastAPI application as a local web service. Reload mode restarts the service whenever you save a source-code change.

  • Switch back to the PowerShell terminal where .venv is active.
  • Start main:app with reload enabled by running this command:
python -m uvicorn main:app --reload

What does this command do?

  • The python executable uses the active virtual environment.
  • The main:app value selects app from main.py.
  • The --reload flag watches your source code for saved changes.

Uvicorn keeps this PowerShell session busy while it serves the application. You'll see startup output for a local server on port 8000.

  • Leave this PowerShell session running while you test the endpoint.

Server doesn't stay running?

Confirm that PowerShell is still inside python-health-api. Confirm that .venv remains active before trying again.

Help me diagnose why the Uvicorn server stops during startup.

Call the endpoint and use the docs

The first browser request checks whether the running service can reach health(). Its response gives you a visible result from the code you just wrote.

Before you open the endpoint, what response do you expect the route to return? Hold that prediction for the next check.

  • Switch back to the browser you used for GitHub earlier.
  • Enter http://127.0.0.1:8000/health in the address bar.
  • Press Enter.

You'll see {"status":"ok"} in the browser. Your first live route is working: it answers health checks with a concrete JSON result.

What does health prove?

The /health result proves that the application process can accept a request. Monitoring systems can use this signal to detect a stopped service.

Deployment systems also need a readiness signal before sending production traffic. That separate contract comes after this working foundation.

FastAPI generates interactive browser documentation from the route declarations in main.py. This page lets you send the same request without writing another client.

Before you send the request from the documentation, will its body match the direct browser result? Keep your answer in mind.

  • Enter http://127.0.0.1:8000/docs in the browser address bar.
  • Press Enter.
  • Select the GET /health operation.
  • Click Try it out.
  • Click Execute.

You'll see response code 200 with a body containing "status": "ok". This confirms that the documentation and direct URL reach the same health function.

Health operation missing from the docs?

Save main.py before refreshing the documentation page. Confirm that the PowerShell session running Uvicorn is still active.

Help me find why GET /health is missing from the FastAPI documentation.

Your local service now answers health requests through both the direct URL and interactive documentation. Next, you'll turn the missing readiness behavior into a testable contract.

Make Readiness Fail in CI

Your FastAPI service now answers /health locally. That proves the process is alive.

A deployment also needs evidence that the service is ready for traffic. This step turns that expectation into an automated contract that GitHub Actions evaluates after your push.

In this step, get ready to:
  • Define automated contracts for both API endpoints.
  • Configure continuous integration for every push.
  • Publish the project to a public GitHub repository.
Define the API contracts

Pytest turns expected behavior into executable contracts. FastAPI's TestClient sends requests to your application without needing the development server.

  • Return to the PowerShell terminal running Uvicorn.
  • Press Ctrl+C to stop the local server.
  • In the VS Code Explorer sidebar, click the New File icon.
  • Enter test_main.py as the file name.
  • Add the health contract by pasting this code into test_main.py:
from fastapi.testclient import TestClient

from main import app

client = TestClient(app)


def test_health() -> None:
    response = client.get("/health")
    assert response.status_code == 200
    assert response.json() == {"status": "ok"}

What does this code do?

  • The client wraps your existing app so the test can send requests directly to it.
  • The test_health function requests /health to exercise the endpoint.
  • The assertions protect the response status plus the exact JSON contract.
  • Save test_main.py.
  • Predict whether the existing health contract passes.
  • Run the health contract from the activated PowerShell terminal by using this command:
python -m pytest

What should you see?

The command runs Pytest through your active Python interpreter. You should see one passing test.

Good. Your existing health behavior is now protected by an automated check.

Does the health test fail?

Confirm that .venv is still active in PowerShell. Check that main.py still defines app plus the /health path operation.

Compare the request path with the route in main.py because they must match exactly.

Help me diagnose my health test failure.

The readiness contract defines the response a deployment expects from /ready.

  • Place your cursor two blank lines below assert response.json() == {"status": "ok"}.
  • Add the readiness contract by pasting this code into test_main.py:
def test_ready() -> None:
    response = client.get("/ready")
    assert response.status_code == 200
    assert response.json() == {"status": "ready"}

What does this code do?

  • The test_ready function requests the deployment readiness path.
  • The first assertion requires a successful HTTP status.
  • The second assertion requires the readiness JSON contract.
  • Save test_main.py.
  • Predict whether both endpoint contracts pass with the current application.
  • Run both contracts from PowerShell by using this command:
python -m pytest

What does this check prove?

Pytest discovers both functions whose names begin with test_. It exercises the current application against both contracts.

You will see one passing test plus one failing test. The readiness failure is intentional because main.py has no /ready path operation.

Seeing a different test result?

If both tests pass, check whether main.py already contains a readiness path operation. Remove any readiness implementation so this step preserves the designed failure.

If both tests fail, compare the health request with the working /health route from the previous step.

Help me reproduce the intended readiness failure.

✔️ Awesome, I've got everything!

  • Confirm that test_main.py is saved.

ⓧ I'd like to double check the full code

from fastapi.testclient import TestClient

from main import app

client = TestClient(app)


def test_health() -> None:
    response = client.get("/health")
    assert response.status_code == 200
    assert response.json() == {"status": "ok"}


def test_ready() -> None:
    response = client.get("/ready")
    assert response.status_code == 200
    assert response.json() == {"status": "ready"}
Automate the contract in CI

A CI workflow runs the same contracts on a clean hosted machine after each push. The workflow also gives GitHub Actions only the repository access needed to read your code.

  • In the VS Code Explorer sidebar, click the New Folder icon.
  • Enter .github as the folder name.
  • Select the .github folder in the Explorer sidebar.
  • Click the New Folder icon.
  • Enter workflows as the nested folder name.
  • Select the workflows folder in the Explorer sidebar.
  • Click the New File icon.
  • Enter ci.yml as the file name.
  • Define the CI workflow by pasting this code into .github/workflows/ci.yml:
name: CI

on: push

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v7

      - name: Set up Python
        uses: actions/setup-python@v7
        with:
          python-version: "3.14.7"
          cache: "pip"

      - name: Install dependencies
        run: python -m pip install -r requirements.txt

      - name: Run tests
        run: python -m pytest

How does this workflow operate?

  • The on: push trigger starts the workflow after a push to any branch.
  • The permissions section limits repository access to reading contents.
  • The checkout step places your repository files on the Ubuntu runner.
  • The Python setup step selects version 3.14.7 plus the pip cache.
  • The final steps install the pinned dependencies before running the test suite.
  • Save .github/workflows/ci.yml.
  • Confirm that the VS Code Explorer shows .github/workflows/ci.yml inside python-health-api.

Can't see the workflow file?

Expand .github plus workflows in the Explorer sidebar. Confirm that ci.yml sits inside the nested workflows folder.

Help me check my GitHub Actions workflow path.

✔️ Awesome, I've got everything!

  • Confirm that .github/workflows/ci.yml is saved.

ⓧ I'd like to double check the full code

name: CI

on: push

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v7

      - name: Set up Python
        uses: actions/setup-python@v7
        with:
          python-version: "3.14.7"
          cache: "pip"

      - name: Install dependencies
        run: python -m pip install -r requirements.txt

      - name: Run tests
        run: python -m pytest
Publish the deliberate failure

A public GitHub repository lets the hosted runner execute this workflow at no cost. Every committed file becomes visible to other people.

Your current project contains no credentials. Keep secrets out of this repository as you extend it.

  • Return to your signed-in GitHub account in the browser.
  • Select New repository from the create menu.
  • Choose your account in the Owner field.
  • Enter python-health-api in the Repository name field.
  • Select Public under visibility.
  • Leave the repository initialization options unchecked.
  • Click Create repository.

Your empty public repository is ready to receive the local project. GitHub shows connection instructions on the repository page.

  • Copy the repository URL shown on the page into REMOTE-URL.
  • Return to the PowerShell terminal in the python-health-api folder.
  • Create the local Git history by running these commands:
git init -b main
git add .
git commit -m "Add health API and readiness contract"

What do these commands do?

  • The first command initializes the repository with main as its branch.
  • The second command stages the files in python-health-api.
  • The final command records the health API plus its readiness contract in one commit.

PowerShell should show a new commit summary with the project files listed.

Did the commit fail?

A first Git commit can require an author name or email address. Follow Git's prompt to configure the missing identity before retrying the commit.

Help me complete my first commit.

The first push can pause for GitHub authentication. The prompt belongs to the sign-in flow for the verified account you used earlier.

  • Connect the local repository to GitHub plus push the main branch by running these commands:
git remote add origin [[REMOTE_URL="REMOTE-URL"]]
git push -u origin main

What do these commands do?

  • The first command names your GitHub repository origin.
  • The second command uploads main to the remote repository.
  • The upstream link lets later pushes use the shorter git push command.
  • Complete the GitHub authentication flow if Git opens one.

After the push completes, GitHub Actions starts the CI workflow from your committed configuration.

Did the push fail?

Confirm that the repository URL belongs to your python-health-api repository. Complete any browser authentication prompt with the verified GitHub account from earlier.

Help me diagnose my GitHub push.

  • Predict whether the hosted runner satisfies both endpoint contracts.
  • Return to the python-health-api repository page in your browser.
  • Click the Actions tab.
  • Open the newest CI workflow run.
  • Select the test job.
  • Expand the Run tests step.

You will see a red workflow run with one passing health test plus one failing readiness test. That red result proves CI blocks a deployment contract your application does not satisfy.

Don't see the red workflow?

If no workflow appears, confirm that the pushed repository contains .github/workflows/ci.yml. Check that the file is on the main branch.

If the workflow is green, confirm that test_ready exists in test_main.py. Confirm that main.py still lacks the readiness path operation.

Help me find my missing or unexpected CI result.

Your readiness contract now fails in PowerShell plus GitHub Actions. Next, you will implement the missing endpoint to turn both checks green.

Implement Readiness and Turn CI Green

Your deliberately failing readiness test did its job. It proved that the current API cannot satisfy the deployment contract.

The failing contract now tells you exactly what production orchestration needs. A small FastAPI path operation should satisfy the test and turn your GitHub Actions workflow green.

In this step, get ready to:
  • Implement the missing readiness endpoint.
  • Confirm both API tests pass locally.
  • Push the fix and verify the new CI run succeeds.
Add the readiness endpoint

A readiness endpoint reports whether the service can accept traffic. The existing test already defines the expected route.

It also defines the required JSON response. Your implementation can follow that contract directly.

  • In the VS Code Explorer sidebar, select main.py.
  • Find the health() function near the bottom of main.py.
  • Add the readiness path operation directly below the health() function by copying this code:
@app.get("/ready")
def ready() -> dict[str, str]:
    return {"status": "ready"}

What Does This Code Do?

  • The @app.get("/ready") decorator connects incoming requests for /ready to the function below it.
  • The ready() function returns the readiness status required by the existing test.
  • Save main.py.

Before you rerun the tests, do you expect the readiness contract to pass now?

  • Test the new endpoint through the active virtual environment by running:
python -m pytest

What Does This Check?

The command runs every discovered test through the Python interpreter in your active virtual environment. This checks the existing health contract plus the new readiness contract.

PowerShell should now report 2 passed. Your implementation satisfies both API contracts.

Still Seeing a Failing Readiness Test?

Check that the route is spelled /ready. Confirm that ready() returns the exact status value expected by test_ready().

Help me diagnose why my readiness test still fails.

✔️ Awesome, I've got everything!

Your two tests pass. Double-check that main.py is saved before you continue.

ⓧ I'd like to double check the full code

from fastapi import FastAPI

app = FastAPI(title="Deployment Health API")


@app.get("/health")
def health() -> dict[str, str]:
    return {"status": "ok"}


@app.get("/ready")
def ready() -> dict[str, str]:
    return {"status": "ready"}

This reference shows the complete saved contents of main.py at this point in the project.

Confirm readiness in the API docs

FastAPI generates interactive documentation from the path operations registered on app. Starting the service lets you confirm that the new route is available outside the test suite.

  • Start the development server from the PowerShell terminal in python-health-api by running:
python -m uvicorn main:app --reload

What Does This Command Do?

  • Uvicorn loads the app object from main.py.
  • The --reload option watches your project files during local development.
  • The service listens locally on port 8000.
  • Enter http://127.0.0.1:8000/docs in your browser's address bar.
  • Find the operation for /ready in the generated documentation.

You should see separate operations for /health and /ready. This proves the running application exposes both deployment checks.

  • Return to the PowerShell terminal running the development server.
  • Press Ctrl+C to stop the development server.

Missing the Readiness Operation?

Confirm that you saved main.py before starting the server. Restart the development server if the documentation still shows only /health.

Help me find why my FastAPI docs do not show the readiness endpoint.

Push the green fix

The local result proves your code satisfies the contract. The workflow uses the same test command after every push to main.

This push creates a new CI run without changing the earlier red run. The two runs preserve the full red-to-green story.

Before you push the fix, do you expect the new workflow run to stay red or turn green?

  • Publish the readiness implementation to origin/main by running these commands:
git add .
git commit -m "Implement readiness endpoint"
git push

What Do These Commands Do?

  • The first command stages the saved readiness implementation.
  • The second command records the change with a descriptive commit message.
  • The final command sends the commit to the connected GitHub repository.

Your commit reaches origin/main. The push triggers a fresh CI workflow run.

Push Rejected or Authentication Requested?

Complete the GitHub authentication prompt if one appears. Confirm that PowerShell is still inside the python-health-api folder if Git cannot find the repository.

Help me diagnose why my Git push did not reach origin/main.

  • Return to the python-health-api repository in your browser.
  • Select the Actions tab near the top of the repository page.
  • Open the newest CI workflow run.
  • Wait for the hosted runner to finish the test job.
  • Open the Run tests step in the completed job.

Your latest workflow run should show a green success state. The Run tests step should report 2 passed.

You have closed the readiness gap. The same contract now passes on your Windows machine and the hosted Ubuntu runner.

Your API now proves both liveness and readiness through local tests plus CI. Next, you will package the same tested service into a container image.

Build and Test the Container in CI

Your readiness contract now passes locally. The latest GitHub Actions run is green.

The remaining risk is portability. Passing tests on one machine does not prove the same code can start in another runtime.

A Docker image packages the application with its runtime instructions. The hosted Ubuntu runner builds that image before starting a container from it.

The runner then executes the same tests inside the packaged runtime. This connects your passing backend behavior to a repeatable delivery workflow.

In this step, get ready to:
  • Package the health API in a Docker image.
  • Exclude local metadata from the container build.
  • Prove the image with a container test stage in CI.
Create the container files

A Dockerfile defines the image recipe. The ignore files keep local caches out of the image build or future commits.

  • In the VS Code Explorer sidebar, create a file named Dockerfile inside the python-health-api folder.
  • Add the container recipe by pasting this code into Dockerfile:
FROM python:3.14.7-slim

WORKDIR /app

COPY requirements.txt .
RUN python -m pip install -r requirements.txt

COPY . .

EXPOSE 8000

CMD ["python", "-m", "uvicorn", "main:app", "--host=0.0.0.0", "--port=8000"]

What Does This Recipe Do?

  • The FROM instruction pins the runtime to python:3.14.7-slim.
  • The WORKDIR instruction gives the application a predictable location inside the image.
  • The first COPY instruction adds requirements.txt before the dependency installation step.
  • The second COPY instruction adds the project source after the dependencies are installed.
  • The EXPOSE instruction documents port 8000 as the service port.
  • The CMD instruction starts Uvicorn with the main:app application.
  • Save Dockerfile.
  • Confirm Dockerfile appears directly inside the python-health-api folder in the Explorer sidebar.

Does Dockerfile Look Like a Text File?

  • Check that the filename is exactly Dockerfile with no hidden .txt suffix.
  • Compare the first instruction with FROM python:3.14.7-slim if syntax highlighting does not appear.

Help me check my Dockerfile.

The runner sends the current project folder to the image build. A .dockerignore file removes local environments plus repository metadata from that build context.

  • In the VS Code Explorer sidebar, create .dockerignore inside the python-health-api folder.
  • Add the build exclusions by pasting this content into .dockerignore:
.venv
__pycache__
.pytest_cache
.git
.github

What Does Docker Ignore?

  • The .venv entry keeps your Windows virtual environment outside the Linux image.
  • The cache entries keep generated Python test data outside the image.
  • The .git entry removes local repository history from the build context.
  • The .github entry keeps workflow configuration outside the runtime image.
  • Save .dockerignore.
  • Confirm .dockerignore appears beside Dockerfile in the Explorer sidebar.

Is the Ignore File Missing?

  • Check that the filename begins with a full stop.
  • Remove any hidden .txt suffix from the filename.

Help me fix my Docker ignore file.

Git also needs a separate exclusion list. The .gitignore file keeps new local Python environments or caches out of future commits.

  • In the VS Code Explorer sidebar, create .gitignore inside the python-health-api folder.
  • Add the repository exclusions by pasting this content into .gitignore:
.venv/
__pycache__/
.pytest_cache/

What Does Git Ignore?

  • The .venv/ entry excludes the local virtual environment directory.
  • The __pycache__/ entry excludes generated Python bytecode caches.
  • The .pytest_cache/ entry excludes generated pytest cache data.
  • Save .gitignore.
  • Confirm .gitignore appears beside the other container files in the Explorer sidebar.

Are Local Files Still Listed?

  • Check each directory entry for the trailing forward slash shown in the reference file.
  • Save .gitignore before checking the repository changes again.

Help me check my Git ignore rules.

Extend the CI workflow

The current workflow proves the code on the hosted runner. The updated job keeps that check before adding an image build plus a second test run inside the image.

The Ubuntu runner provides Docker for this stage. Your Windows machine does not need a local Docker installation.

  • In the VS Code Explorer sidebar, select .github/workflows/ci.yml.
  • Replace the current workflow with this complete configuration:
name: CI

on: push

permissions:
  contents: read

jobs:
  test-and-build:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v7

      - name: Set up Python
        uses: actions/setup-python@v7
        with:
          python-version: "3.14.7"
          cache: "pip"

      - name: Install dependencies
        run: python -m pip install -r requirements.txt

      - name: Run tests on the runner
        run: python -m pytest

      - name: Build the container image
        run: docker build -t python-health-api:ci .

      - name: Run tests inside the container
        run: docker run --rm python-health-api:ci python -m pytest

How Does the Updated Job Work?

  • The test-and-build job name now reflects both responsibilities.
  • The runner installs the pinned dependencies before executing the existing tests.
  • The image build creates the python-health-api:ci tag from the current project files.
  • The final step starts a temporary container from that image.
  • The final step overrides the default server command with the pytest command.
  • The --rm option removes the temporary container after the tests finish.
  • Save .github/workflows/ci.yml.
  • Confirm the workflow ends with the Run tests inside the container step.

Does the Workflow Indentation Look Uneven?

  • Check that test-and-build sits directly below jobs.
  • Check that each named step lines up beneath steps.
  • Replace any tabs with spaces because YAML uses indentation to define structure.

Help me inspect my CI workflow indentation.

✔️ Awesome, I've got everything!

Your six project files now match the complete containerized health API.

ⓧ I'd like to double check the full code

.venv
__pycache__
.pytest_cache
.git
.github
name: CI

on: push

permissions:
  contents: read

jobs:
  test-and-build:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v7

      - name: Set up Python
        uses: actions/setup-python@v7
        with:
          python-version: "3.14.7"
          cache: "pip"

      - name: Install dependencies
        run: python -m pip install -r requirements.txt

      - name: Run tests on the runner
        run: python -m pytest

      - name: Build the container image
        run: docker build -t python-health-api:ci .

      - name: Run tests inside the container
        run: docker run --rm python-health-api:ci python -m pytest
.venv/
__pycache__/
.pytest_cache/
FROM python:3.14.7-slim

WORKDIR /app

COPY requirements.txt .
RUN python -m pip install -r requirements.txt

COPY . .

EXPOSE 8000

CMD ["python", "-m", "uvicorn", "main:app", "--host=0.0.0.0", "--port=8000"]
from fastapi import FastAPI

app = FastAPI(title="Deployment Health API")


@app.get("/health")
def health() -> dict[str, str]:
    return {"status": "ok"}


@app.get("/ready")
def ready() -> dict[str, str]:
    return {"status": "ready"}
fastapi==0.141.1
httpx==0.28.1
pytest==9.1.1
uvicorn==0.52.4
from fastapi.testclient import TestClient

from main import app

client = TestClient(app)


def test_health() -> None:
    response = client.get("/health")
    assert response.status_code == 200
    assert response.json() == {"status": "ok"}


def test_ready() -> None:
    response = client.get("/ready")
    assert response.status_code == 200
    assert response.json() == {"status": "ready"}
Push and inspect the container build

The workflow only receives these files after a push to origin/main. That push starts the portability check on the hosted runner.

  • Commit the four changed files with the message Build container in CI using the Git workflow from the previous step.

Good progress: your local history now captures the container recipe plus its CI proof.

Before you push, pause to predict whether both test stages will agree.

  • Push the commit to origin/main using the same Git push command from the previous step.

The image build can take a few minutes. A running status means the hosted runner is still assembling the image.

  • Return to the Actions tab in your python-health-api repository.
  • Select the newest CI workflow run.
  • Select the test-and-build job.
  • Expand Run tests on the runner.
  • Expand Build the container image.
  • Expand Run tests inside the container.

You’ll see 2 passed in the host test step.

The image build completes with the python-health-api:ci tag.

The container test step also reports 2 passed.

You have closed the portability gap: the packaged API now passes the same contract as your local application.

Is the Container Job Red?

  • Open the failed step to identify whether the workflow stopped during the image build or the container tests.
  • Compare Dockerfile with the full-code reference if the image build failed.
  • Check that the final workflow step uses the python-health-api:ci image tag if the image cannot be found.

Help me debug the failed container job.

Secret mission

Expose Runtime Version Metadata

Your CI workflow already proves that one container image passes its tests. Now make that same image report release metadata supplied when the container starts.

Clean Up Your Resources

Clean Up Your Resources

Your public GitHub repository has no ongoing cost. You can keep your resources, pause local work, or delete everything.

Resources you used:

  • Local python-health-api folder containing the .venv virtual environment.
  • Public python-health-api repository containing its GitHub Actions workflow history.

Keep everything running

No action is needed. This option keeps the API available for future development or reference.

  • Keep the local python-health-api folder so you can run the API again.
  • Keep the public python-health-api repository so its code and workflow history remain available.
  • Push another commit whenever you want the workflow to run again.

Pause - I'll come back to this later

Pausing means stopping any local process while keeping the project files. Your repository remains available without running a workflow.

  • Switch back to the PowerShell window from earlier.
  • Press Ctrl+C if the Uvicorn server is still running.
  • Keep the local python-health-api folder for your next session.

No GitHub-hosted runner remains active after the workflow ends. Its temporary Docker image has already been discarded.

Delete - I don't want to use this again

This option permanently removes the local project files and the public repository.

Deletion Is Permanent

Removing the repository permanently erases its commit history. Its workflow history is erased too.

Removing python-health-api from Recycle Bin permanently erases the local files.

These actions can feel drastic. The Keep tab remains available if you want more time.

Delete local files:

  • Close Visual Studio Code.
  • Close PowerShell.
  • Press the Windows key to open Windows search.
  • Type File Explorer in the search bar.
  • Press Enter to open File Explorer.
  • Locate the python-health-api folder in the place where you created it.
  • Select the python-health-api folder.
  • Press the Delete key.

You should now see python-health-api in Recycle Bin.

Windows may ask you to confirm the permanent deletion in the next action.

  • Open Recycle Bin from the Windows desktop.
  • Select the python-health-api folder.
  • Use the Recycle Bin toolbar to delete the selected folder permanently.
  • Confirm the permanent deletion if prompted.

You should no longer see python-health-api in Recycle Bin.

Delete the public repository:

GitHub uses a confirmation flow before it permanently deletes a repository.

  • Go to your public python-health-api repository page on GitHub.
  • Click Settings in the top navigation bar.
  • Select General.
  • Scroll to the Danger Zone section.
  • Click Delete this repository.
  • Complete GitHub's confirmation flow.

You should no longer find python-health-api in your GitHub repositories.

Nice Work!

Nice Work!

You did it! Your python-health-api repository now delivers a tested FastAPI service through a green GitHub Actions workflow.

What you learned:

  • Built a liveness endpoint at /health with a visible JSON response. Added a readiness endpoint at /ready for deployment checks.
  • Used a deliberately failing readiness test to expose a missing deployment contract. Implemented /ready to turn the workflow from red to green.
  • Used GitHub Actions to build a Docker image after the host tests passed. Ran the same test suite inside the image to prove the packaged API works.
  • Secret Mission: Exposed runtime version metadata through APP_VERSION. Proved that the same image can report 1.1.0 without a rebuild.

Ready to quiz yourself?