Skip to content

06. CI & Docker

In this section, we will set up Continuous Integration (CI) using GitHub Actions and containerize our application with Docker. This will allow us to automate testing and deployment, ensuring that our application is always in a releasable state.

6.1 Update Project Board

Update Project Board

  1. Go to your repository on GitHub
  2. Navigate to the Projects tab
  3. Open your project board
  4. Find the issue card for "Introduce CI and Docker" (should be in "Todo" or "Backlog" column)
  5. Drag the card to the "In Progress" column

6.2 Download Configuration Files

Important: The configuration files for this section are provided in the course repository. Download them before proceeding.

Download from GitHub Repository

You will need to download four files from the cse120-ucm/workshop repository.

  1. Dockerfile: Click here to view, then click the "Download raw file" icon (or "Raw" button) to save it.
  2. .dockerignore: Click here to view, then save it.
  3. test.yml: Navigate to .github/workflows/test.yml here and save it.
  4. release.yml: Navigate to .github/workflows/release.yml here and save it.

Upload to Your Repository

  1. In your Codespace, create the .github/workflows/ directory if it doesn't exist.
  2. Upload test.yml to .github/workflows/test.yml.
  3. Upload release.yml to .github/workflows/release.yml.
  4. Upload Dockerfile to the project root.
  5. Upload .dockerignore to the project root.

Note: You can upload files by dragging them into the VS Code file explorer, or by using the "Upload Files" option when right-clicking in the file explorer.

6.3 Review GitHub Actions Workflow

This is a GitHub Actions workflow that runs tests, linting, and type checking on every pull request to the main branch.

Open .github/workflows/test.yml and review its content:

name: Test

on:
  pull_request:
    branches: [ main ]

jobs:
  build-test:
    runs-on: ubuntu-latest

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

      - name: Install uv
        uses: astral-sh/setup-uv@v5

      - name: Install the project
        run: uv sync --all-extras --dev

      - name: Run tests
        run: uv run pytest

      - name: Run linters
        run: uv run ruff check

      - name: Run type checker
        run: uv run mypy app

6.4 Review Dockerfile

Docker allows you to package your application with all its dependencies into a container that can run anywhere. Containers are lightweight, portable, and ensure your application runs consistently across different environments.

Open the Dockerfile in your project root and review its content:

FROM python:3.13-slim AS builder

# Minimal system deps (curl for healthcheck only)
RUN apt-get update \
    && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

RUN pip install --upgrade pip uv

# Copy project metadata & sources for build
COPY pyproject.toml ./
COPY README.md ./
COPY app ./app

# Build wheel & sdist into dist/
RUN uv build

FROM python:3.13-slim AS runtime

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

RUN pip install --upgrade pip uv

# Copy built artifacts from builder stage
COPY --from=builder /app/dist /tmp/dist

# Install wheel (prefer wheel over sdist)
RUN uv pip install /tmp/dist/*.whl --system --no-cache

# Copy only runtime essentials (optional: keep README for transparency)
COPY README.md ./
COPY app ./app

# Expose port (Render sets $PORT; fastapi/uvicorn will bind to it)
EXPOSE 8000

# HEALTHCHECK (optional - Render has native health checks; still useful locally)
HEALTHCHECK --interval=30s --timeout=5s --retries=3 CMD curl -f http://localhost:8000/health || exit 1

# Default command uses dynamic PORT with fallback
CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}"]

6.5 Review Release Workflow

This GitHub Actions workflow automates the release process using semantic versioning and builds/pushes a Docker image to GitHub Container Registry (GHCR) on every merged pull request to main.

Open .github/workflows/release.yml and review its content.

Note: Make sure to check the variable names for app-id and private-key.

name: Release

on:
  pull_request:
    types: [closed]
    branches:
      - main

# Global permissions kept minimal; job-level overrides applied.
permissions:
  contents: read

concurrency: release

jobs:
  release:
    name: Build and Release Python Package
    runs-on: ubuntu-latest
    if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true

    permissions:
      contents: write
      packages: write

    outputs:
      released: ${{ steps.release.outputs.released }}
      version: ${{ steps.release.outputs.version }}
      tag: ${{ steps.release.outputs.tag }}

    steps:
      - name: Generate GitHub App Token
        id: generate_token
        uses: actions/github-app-token@v2
        with:
          app-id: ${{ secrets.APP_ID }}
          private-key: ${{ secrets.APP_PRIVATE_KEY }}

      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          token: ${{ steps.generate_token.outputs.token }}

      - id: release
        name: Python Semantic Release
        uses: python-semantic-release/python-semantic-release@master
        with:
          github_token: ${{ steps.generate_token.outputs.token }}

      - name: Publish Release (GitHub)
        if: steps.release.outputs.released == 'true'
        uses: python-semantic-release/publish-action@main
        with:
          github_token: ${{ steps.generate_token.outputs.token }}
          tag: ${{ steps.release.outputs.tag }}

  docker:
    name: Build and Release Docker Image
    needs: release
    runs-on: ubuntu-latest
    if: needs.release.outputs.released == 'true'

    permissions:
      contents: read
      packages: write
      id-token: write

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

    steps:
      - name: Checkout (main)
        uses: actions/checkout@v4
        with:
          ref: main
          fetch-depth: 0

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

      - name: Extract metadata (tags, labels)
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

      - name: Build and Push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:${{ needs.release.outputs.version }}
            ghcr.io/${{ github.repository }}:latest
          labels: ${{ steps.meta.outputs.labels }}

6.6 Commit CI/CD Configuration

Let's commit and push these changes to your repository.

VS Code UI: 1. Go to the Source Control panel (icon with three branches) 2. Stage all changes by clicking the + icon next to Changes 3. In the message box, type:

```
ci: add GitHub Actions workflows and Docker configuration

closes #5
```

Note: Replace `#5` with the actual issue number if it's different.
  1. Click the checkmark icon to commit

Terminal: Alternatively, you can run the following commands in the terminal:

Stop the FastAPI server if it's running with Ctrl+C. Then open your terminal and run.

git add .
git commit -m "ci: add GitHub Actions workflows and Docker configuration

closes #5"
git push origin fastapi-setup

Note: Replace #5 with your actual CI/CD issue number.


Learn More

Understanding GitHub Actions

GitHub Actions automate your workflow with CI/CD:

Key Concepts: - Workflow: Automated process defined in YAML - Job: Set of steps that execute on the same runner - Step: Individual task (run command, use action) - Runner: Server that runs your workflows - Action: Reusable unit of code

Common Triggers: - push: When code is pushed - pull_request: When PR is opened/updated - schedule: Run on a schedule (cron) - workflow_dispatch: Manual trigger

Understanding Docker Multi-Stage Builds

Our Dockerfile uses multi-stage builds for efficiency:

  1. Builder Stage: Compiles and builds the application
  2. Uses full Python image with build tools
  3. Installs dependencies
  4. Creates wheel distribution

  5. Runtime Stage: Runs the application

  6. Uses minimal Python image
  7. Only includes runtime dependencies
  8. Results in smaller, more secure image

Benefits: - Smaller final image size - Faster deployment - Improved security (fewer tools in production) - Better layer caching

Understanding Semantic Versioning

The release workflow uses semantic versioning (semver):

  • MAJOR (1.x.x): Breaking changes
  • MINOR (x.1.x): New features, backwards compatible
  • PATCH (x.x.1): Bug fixes, backwards compatible

Conventional commits automatically determine version bumps: - feat: → Minor version bump - fix: → Patch version bump - feat!: or BREAKING CHANGE: → Major version bump


Useful Resources