green snake
Photo by Pixabay on Pexels.com

Introduction to Building a CI/CD Pipeline with FastAPI × GitHub Actions — Test Automation, Docker Build, Container Registry, and Kubernetes Deployment


Summary (Big Picture First)

  • We’ll build a CI/CD pipeline with GitHub Actions so that changes to a FastAPI app can flow automatically from push to production release.
  • The pipeline is based on three stages: Tests (CI) → Docker Build & Push → Kubernetes Deployment, and safely switches between development, staging, and production environments.
  • We’ll use GitHub Container Registry (GHCR) as the example container registry, but the structure can be applied to any registry.
  • Deployment to Kubernetes is a push-based approach (calling kubectl etc. directly from GitHub Actions), while leaving room to evolve later to pull-based approaches like Argo CD.
  • We’ll organize design points that make operations easier: secrets, environment-specific protection rules, test granularity, rollback strategies, and more.

Who This Is For (Very Concrete)

  • Individual developers / students
    You host your FastAPI app on GitHub, but you manually run tests, build Docker images, and deploy every time.
    → We aim for a “semi-automatic” workflow where pushing code automatically runs tests and deploys to the staging environment.

  • Small teams (3–5 people) of web / backend engineers
    Reviews, tests, and staging deployments for each Pull Request tend to be ad hoc and person-dependent.
    → We make the GitHub Actions workflow the shared rule so tests and deployments become the team’s “standard process.”

  • Startup teams running FastAPI on Kubernetes
    You are already on Kubernetes, but you’re piled up with ad hoc scripts and manual kubectl apply, and nobody is sure which script is the source of truth.
    → We centralize “Build → Registry → K8s Deploy” into the workflow and unify the production release flow.


Accessibility Evaluation

  • Information structure
    The article progresses step by step: big picture up front → CI design → CD design → Kubernetes integration → environment & secrets management → operational tips → summary.

  • Terminology
    Terms are briefly explained the first time they appear, then used consistently afterwards to avoid confusion. English terms are not introduced more than necessary.

  • Code and YAML
    They are shown in fixed-width blocks with short comments and plenty of blank lines for easy visual scanning.

  • Range of target readers
    For readers new to CI/CD, each step includes background reasoning. For experienced readers, the content is deep enough to be reused as a “template.”

Overall, the goal is AA-level readability for a technical article.


1. First Things to Decide: CI/CD Design Policy for FastAPI

This applies not only to FastAPI but to web apps in general: if you decide the following three items first, you’ll have fewer doubts later when designing CI/CD.

  1. When to run tests (on push, on PR, on merging into main, etc.).
  2. How far to automate, and from which point you insert human approval.
  3. Where to store artifacts (e.g., Docker images) and when to distribute them to each environment.

In this article, we’ll follow a common setup with the following policies.

  • On push to the main branch:

    • Run tests (CI)
    • Build Docker image & push to registry
    • Automatically deploy to the staging environment
  • For production deployment, use GitHub Actions Environments and manual triggers (workflow_dispatch) to add human approval.

This structure strikes a good balance that’s easy to operate from individual projects up to small teams, without building bad habits.


2. Aligning FastAPI Repository Assumptions

2.1 Example Directory Structure

fastapi-app/
├─ app/
│  ├─ main.py
│  ├─ routers/
│  └─ core/
├─ tests/
│  └─ test_*.py
├─ Dockerfile
├─ requirements.txt / pyproject.toml
└─ .github/
   └─ workflows/
      └─ ci-cd.yaml   # To be created

2.2 Unifying the Test Command

The test command run in CI should be the same command you run locally.

Example (in [tool.pytest.ini_options] of pyproject.toml, etc.):

pytest -q

Or if you also want to include a formatter and type checking:

ruff check .
pytest -q
mypy app

This habit of “running the same command locally and in CI” makes troubleshooting much easier when something goes wrong.


3. Basics of GitHub Actions: Skeleton of a Workflow

GitHub Actions automatically runs the workflows defined in .github/workflows/*.yaml on events such as push and PR.

Let’s start with the smallest configuration that just runs tests.

3.1 Test-Only Workflow

# .github/workflows/ci-cd.yaml
name: CI/CD for FastAPI

on:
  push:
    branches: ["main"]
  pull_request:
    branches: ["main"]

jobs:
  test:
    runs-on: ubuntu-latest

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

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt
          # If needed: pip install -r requirements-dev.txt

      - name: Run tests
        run: pytest -q

Now tests will automatically run on pushes and PRs to the main branch.

Decision points:

  • If tests become slow, you can split linting, unit tests, and integration tests into separate jobs and run them in parallel.
  • You can also split workflows by events, such as “tests only on non-main branches, and build + deploy when merging into main.”

4. Building Docker Images and Pushing to a Registry

If you use a FastAPI app in production, in many cases it is recommended to distribute it as a Docker image.

Here we’ll add a job to build and push an image to GitHub Container Registry (GHCR).

4.1 Preparation for GHCR

  • In the repository’s Settings → Packages, confirm that GHCR is available.
  • A common convention for image names: ghcr.io/<OWNER>/<REPO>:<TAG>

<OWNER> is usually your GitHub username or organization name.

4.2 Adding the Build & Push Job

By using the official docker/build-push-action, you can write build and push concisely.

jobs:
  test:
    # ... (the test job above)

  build-and-push:
    needs: test    # Run only if tests pass
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'  # Build only on main branch

    permissions:
      contents: read
      packages: write
      id-token: write   # For using OIDC, etc.

    env:
      REGISTRY: ghcr.io
      IMAGE_NAME: ${{ github.repository }}  # owner/repo

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

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

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

Key points:

  • needs: test ensures that the build only runs if tests succeed.
  • By using GITHUB_TOKEN, you can push to GHCR without creating a separate token (but be mindful of visibility and organization settings).
  • In addition to latest, tag images with the Git commit SHA so that rolling back to a specific version is easier.

5. Automating Deployment to Kubernetes

Next, we’ll add a CD stage that deploys the built Docker image to a Kubernetes cluster. GitHub’s official docs and several articles already show examples of using kubectl or kustomize from GitHub Actions to deploy to Kubernetes.

Here we’ll show a minimal push-based deployment that runs kubectl apply directly from Actions.

5.1 Registering Cluster Connection Info on GitHub

Cluster authentication depends on your cloud provider and setup, but here are common patterns:

  • Store the contents of kubeconfig as a GitHub Secret and reconstruct the file in the workflow.
  • Use cloud-provider-specific setup-* actions (for example, gcloud for GKE) for authentication.

In this example, we’ll use a simple pattern of putting the entire kubeconfig into a secret.

  1. From your local ~/.kube/config, extract the part for the target cluster and save it as a GitHub Secret named KUBECONFIG_CONTENT (or similar).
  2. In the workflow, output it to a file and set it as KUBECONFIG.

5.2 Example Deployment Job

  deploy-staging:
    needs: build-and-push
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    environment:
      name: staging
      url: https://stg.example.com   # Staging URL (optional)

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

      - name: Set up kubectl
        uses: azure/setup-kubectl@v4
        with:
          version: "v1.30.0"

      - name: Configure kubeconfig
        run: |
          echo "${KUBECONFIG_CONTENT}" > kubeconfig
          chmod 600 kubeconfig
        env:
          KUBECONFIG_CONTENT: ${{ secrets.KUBECONFIG_STAGING }}
      - name: Set KUBECONFIG env
        run: echo "KUBECONFIG=$PWD/kubeconfig" >> $GITHUB_ENV

      - name: Update image in manifest
        run: |
          IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
          sed -i "s|image: .*|image: ${IMAGE}|g" k8s/deployment.yaml

      - name: Apply manifests
        run: |
          kubectl apply -f k8s/configmap.yaml
          kubectl apply -f k8s/secret.yaml
          kubectl apply -f k8s/deployment.yaml
          kubectl apply -f k8s/service.yaml
          kubectl apply -f k8s/ingress.yaml

Key points:

  • By specifying environment: staging, you can use GitHub’s Environment feature to set protection rules and reviewers per environment.
  • Overwriting the image: field in YAML with sed is a simple approach. For more flexibility, you can use tools like kustomize or Helm.
  • For production, create a deploy-production job, set environment.name: production, and configure manual triggers and required reviews.

6. Environment Switching and Protection Rules

Typically, staging and production differ in these two aspects:

  1. The contents of Secrets / ConfigMaps (DB endpoints, external API keys, etc.).
  2. The deployment flow in terms of automation vs manual steps (staging is automatic, production is manual + approval).

6.1 Using GitHub Environments

GitHub Actions provides an Environments feature that lets you have environment-specific Secrets and protection rules.

  • staging environment:

    • Secrets: KUBECONFIG_STAGING, DATABASE_URL_STAGING, etc.
    • Automatic deployment is allowed (with looser protection rules).
  • production environment:

    • Secrets: KUBECONFIG_PROD, DATABASE_URL_PROD, etc.
    • Requires manual approval and can restrict who can approve.

In workflows, specifying an environment in a job allows you to use the Secrets specific to that environment.

environment:
  name: production
  url: https://app.example.com

6.2 Integrating with FastAPI Settings

By using pydantic-settings and environment variables such as ENV=stg / ENV=prod, you can inject environment-specific values from Kubernetes and switch configurations without changing your application code.


7. Secrets and Security Precautions

GitHub Actions Secrets are encrypted on GitHub’s side and are not displayed directly in logs, but you still need to be careful about how you handle them in workflows.

7.1 What You Must Not Do

  • Output secrets to logs using echo as-is.
  • Keep set -x always enabled while running commands that include secrets as arguments.
  • Embed kubeconfig directly into manifests or commit it to the repository.

7.2 Tips for Safe Handling

  • Only pass the minimum necessary secrets into the workflow (don’t stuff “everything” into it).
  • Separate Secrets between production and staging to prevent accidents like updating production with staging credentials.
  • Delete temporary files (such as kubeconfig) at the end of the job for extra safety.

8. How to Tie Your Test Strategy to CI

The quality of your CI/CD ultimately depends on what kind of tests you run and how much of them you automate.

8.1 Example Layers of Tests

  • Linting (style / static analysis): ruff, flake8, mypy, etc.
  • Unit tests: functions and classes in the service layer.
  • API tests: validating FastAPI endpoints using TestClient or httpx.AsyncClient.
  • Integration tests: end-to-end tests using actual DBs and middleware.

In GitHub Actions, you can split these into separate jobs, run them in parallel, and expose the result as status badges in your README so you can see the health of the project at a glance.

8.2 Practical Scope for Each Stage

  • On PR: Lint + unit tests + critical API tests
  • On merge to main: Above plus some integration tests
  • On nightly or scheduled runs: Long-running integration tests or load tests

If your team agrees on “when it’s okay to run heavier tests”, it becomes easier to balance CI time and test coverage.


9. Rollback and Operations in Case of Trouble

Once you have CI/CD, the ability to quickly revert a “broken version” becomes crucial.

9.1 Leveraging Docker Image Tags

As mentioned earlier, tagging Docker images not only with latest but also with Git SHAs makes it easy to roll back to a specific commit.

  • Example: ghcr.io/owner/repo:3f2a9c1

You can simply replace the image: in your Kubernetes manifest with this tag and re-apply it to revert to the previous state.

9.2 Rollback Workflows in GitHub Actions

Once you’re comfortable with the basics, it’s helpful to create a rollback-specific workflow using workflow_dispatch (manual trigger).

  • Accept the rollback target (tag or commit SHA) as input.
  • Apply the manifests to Kubernetes following the same steps as the deploy job.

With this in place, even late-night incidents can be handled safely using a “standardized procedure.”


10. CI/CD Impact by Reader Type

10.1 Individual Developers / Learners

  • Simply pushing code automatically runs tests, so accidents like “forgot to test, deployed broken code” are greatly reduced.
  • Since Docker builds are automated, you’ll be freed from relying on memory like “how did I build yesterday’s production image again?”
  • Even if you don’t use Kubernetes yet, once you learn this “CI template,” you’ll be able to apply it to any future environment.

10.2 Small Teams / Contract Development

  • The flow from PR → tests → review → staging deployment is automated, so reviewers can focus on the content of the review.
  • Since release steps are codified in workflows, you eliminate issues like “everyone has a different deployment method.”
  • New members can feel safe knowing “you can understand the entire release flow just by reading this YAML.”

10.3 Startup SaaS Teams

  • By centralizing production access in GitHub Actions, you remove the need for individuals to touch production directly from their local machines.
  • Environment-specific secrets and protection rules make it easier to design fine-grained permissions for production releases.
  • If you later adopt Helm or Argo CD and move to a GitOps style, you can evolve from the current workflows as a solid foundation.

11. Common Pitfalls and How to Avoid Them

Symptom Cause Countermeasure
CI is slow and nobody waits for it Running a full test suite every time Split tests between PR and main, parallelize jobs
Only production fails Differences in secrets or environment variables Manage settings per environment using Environments, maintain .env.example and documentation
Kubeconfig is lost / leaked Raw info written in workflows or logs Store kubeconfig in Secrets, never log it, delete temporary files
Deployment fails unnoticed Insufficient notification settings Enable GitHub notifications, integrate with Slack or email to avoid missing failures
YAML becomes overly complex Trying to do everything in one workflow Split CI / CD, and staging / production into separate workflows

12. Introduction Roadmap (Gradual, Step-by-Step)

  1. Create a test-only CI workflow (for push / PR).
  2. Add a stage that builds Docker images and pushes them to a container registry like GHCR.
  3. Automate deployment to the staging cluster and integrate it with FastAPI settings using ENV=stg.
  4. Use GitHub Environments and Secrets to create a deploy-production job (manual trigger + approval) for production.
  5. Evolve as needed: rollback workflows, Helm / kustomize integration, HPA linked to custom metrics, etc.

13. Reference Links

Here are official docs and trustworthy articles for readers who want to go deeper.


14. Summary

  • With GitHub Actions, you can combine tests, Docker builds, registry pushes, and Kubernetes deployments for a FastAPI app into a single automated workflow.
  • By leveraging Environments and Secrets, you can easily manage differences and permissions between staging and production, reducing human error.
  • Using Docker image tags, rollback workflows, and a well-thought-out test strategy, you’ll have a system that lets you respond calmly even when issues occur.
  • Instead of aiming for perfection from day one, start with the three steps of “Tests → Build → Staging Deploy,” then gradually expand to production and more advanced mechanisms.

For today, take the first step by creating a test-only workflow, and then layer Docker builds and deployments on top of it.
Seeing your FastAPI app move closer to a “push-to-release” state will be a very exciting experience.


By greeden

Leave a Reply

Your email address will not be published. Required fields are marked *

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)