DevOps

CI/CD for Containerized FastAPI Applications

H1Cloud TeamJanuary 20, 20266 min read

Building a Production CI/CD Pipeline

FastAPI has become the framework of choice for building Python APIs that serve ML models and power backend services. Combined with Docker and a solid CI/CD pipeline, you can achieve rapid, reliable deployments with zero downtime. This guide walks through building a complete pipeline using GitHub Actions, Docker, and blue-green deployment to a Kubernetes cluster.

The pipeline we will build handles linting, testing, building, security scanning, and deploying — all triggered automatically on push to the main branch or on pull request creation.

Dockerfile Best Practices

Start with a well-structured multi-stage Dockerfile that minimizes image size and attack surface:

# Stage 1: Build dependencies
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# Stage 2: Production image
FROM python:3.12-slim AS production
WORKDIR /app

# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser

# Copy only installed packages and application code
COPY --from=builder /install /usr/local
COPY ./app ./app

# Set ownership and switch to non-root
RUN chown -R appuser:appuser /app
USER appuser

EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

Key optimizations: multi-stage build reduces final image size by 60-70%, the non-root user prevents container escape exploits, and pinning the Python version ensures reproducibility. Always use --no-cache-dir with pip to avoid storing package archives in the image.

GitHub Actions Workflow

Here is the complete CI/CD workflow. It runs on every push to main and on pull requests:

name: CI/CD Pipeline

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

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

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          cache: pip
      - run: pip install -r requirements.txt -r requirements-dev.txt
      - run: ruff check app/
      - run: ruff format --check app/
      - run: pytest tests/ -v --cov=app --cov-report=xml
      - uses: codecov/codecov-action@v4

  build-and-push:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: |
          kubectl set image deployment/api \
            api=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
            --namespace=production
          kubectl rollout status deployment/api --namespace=production --timeout=300s

Blue-Green Deployment Strategy

For zero-downtime deployments, we implement blue-green deployments using Kubernetes services and label selectors. The strategy works as follows: deploy the new version with a green label, run health checks against the green deployment, then switch the service selector from blue to green. If anything goes wrong, switching back is a single label change.

This approach eliminates the risk window during rolling updates where old and new versions serve traffic simultaneously — important for API changes that are not backward compatible or for ML model updates where you need consistent predictions across all pods.

Security Scanning

Add container security scanning as a required step before deployment. We use Trivy for vulnerability scanning and Cosign for image signing:

- name: Run Trivy vulnerability scanner
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
    format: 'sarif'
    output: 'trivy-results.sarif'
    severity: 'CRITICAL,HIGH'
    exit-code: '1'

This ensures that no container image with critical or high-severity vulnerabilities reaches production. Combined with automated dependency updates via Dependabot and regular base image updates, this creates a defense-in-depth approach to container security.

Monitoring Deployments

After deployment, automated smoke tests validate the new version. We configure Prometheus alerts that watch error rate and latency spikes for 10 minutes after each deployment. If the error rate exceeds the baseline by more than 2x, an automatic rollback is triggered via ArgoCD or a simple kubectl rollout undo command. This closes the loop and ensures that broken deployments are caught and reverted within minutes, not hours.

Want help implementing these practices?

Let H1Cloud Handle Your Infrastructure