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
kubectletc. 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 manualkubectl 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.
- When to run tests (on push, on PR, on merging into
main, etc.). - How far to automate, and from which point you insert human approval.
- 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
mainbranch:- 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: testensures 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
kubeconfigas a GitHub Secret and reconstruct the file in the workflow. - Use cloud-provider-specific
setup-*actions (for example,gcloudfor GKE) for authentication.
In this example, we’ll use a simple pattern of putting the entire kubeconfig into a secret.
- From your local
~/.kube/config, extract the part for the target cluster and save it as a GitHub Secret namedKUBECONFIG_CONTENT(or similar). - 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 withsedis a simple approach. For more flexibility, you can use tools likekustomizeor Helm. - For production, create a
deploy-productionjob, setenvironment.name: production, and configure manual triggers and required reviews.
6. Environment Switching and Protection Rules
Typically, staging and production differ in these two aspects:
- The contents of Secrets / ConfigMaps (DB endpoints, external API keys, etc.).
- 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.
-
stagingenvironment:- Secrets:
KUBECONFIG_STAGING,DATABASE_URL_STAGING, etc. - Automatic deployment is allowed (with looser protection rules).
- Secrets:
-
productionenvironment:- Secrets:
KUBECONFIG_PROD,DATABASE_URL_PROD, etc. - Requires manual approval and can restrict who can approve.
- Secrets:
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
echoas-is. - Keep
set -xalways 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
TestClientorhttpx.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)
- Create a test-only CI workflow (for push / PR).
- Add a stage that builds Docker images and pushes them to a container registry like GHCR.
- Automate deployment to the staging cluster and integrate it with FastAPI settings using
ENV=stg. - Use GitHub Environments and Secrets to create a
deploy-productionjob (manual trigger + approval) for production. - 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.
-
GitHub Actions
-
FastAPI deployment related
-
Docker / Container registries
-
Kubernetes × GitHub Actions
-
CI/CD and Kubernetes in general
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.
