Documents

Building a GitHub Actions CI/CD Pipeline

6 min readAug 1, 2025Feb 22, 2026

Building a GitHub Actions CI/CD Pipeline

This is a writeup of how I built a GitHub Actions-based CI/CD pipeline to support the rapid deployment cycle of the XGen platform.

Pipeline Structure

Push to main → Lint/Test → Build Docker Image → Push to Registry → Deploy to Server

CI Workflow: Tests and Linting

# .github/workflows/ci.yml
name: CI

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

jobs:
  lint-and-test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_DB: xgen_test
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
        ports:
          - 5432:5432

      redis:
        image: redis:7
        ports:
          - 6379:6379

    steps:
      - uses: actions/checkout@v4

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

      - name: Install dependencies
        run: |
          cd backend
          pip install -r requirements.txt
          pip install pytest pytest-asyncio ruff

      - name: Lint with Ruff
        run: |
          cd backend
          ruff check .

      - name: Run tests
        env:
          DATABASE_URL: postgresql+asyncpg://test:test@localhost:5432/xgen_test
          REDIS_URL: redis://localhost:6379
        run: |
          cd backend
          pytest tests/ -v --asyncio-mode=auto

CD Workflow: Docker Build and Deploy

# .github/workflows/cd.yml
name: CD

on:
  push:
    branches: [main]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    needs: [lint-and-test]

    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Docker Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ secrets.DOCKER_REGISTRY }}
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push backend
        uses: docker/build-push-action@v5
        with:
          context: ./backend
          push: true
          tags: |
            ${{ secrets.DOCKER_REGISTRY }}/xgen-backend:latest
            ${{ secrets.DOCKER_REGISTRY }}/xgen-backend:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Deploy to server
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /opt/xgen
            docker compose pull
            docker compose up -d --build
            docker image prune -f

Per-Environment Deployment Strategy

Development, staging, and production environments are managed separately.

# Branch-to-environment deployment mapping
# develop → staging server
# main → production server

  deploy:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        include:
          - branch: develop
            server: staging
            env_file: .env.staging
          - branch: main
            server: production
            env_file: .env.production

Dockerfile Optimization

Multi-stage builds and layer caching keep build times short.

# backend/Dockerfile
FROM python:3.11-slim AS builder

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

FROM python:3.11-slim

WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .

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

Health Checks and Zero-Downtime Deployment

services:
  backend:
    image: xgen-backend:latest
    deploy:
      update_config:
        parallelism: 1
        delay: 10s
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

With this CI/CD pipeline in place, deployments complete automatically within roughly 5 minutes of a code push. Over 303 commits, the value of this pipeline was apparent every single time. Deployment frequency increased by more than 3× compared to manual deploys, and deployment-related mistakes dropped to nearly zero.

Tags
GitHubActionsCI/CDDockerDevOpsAutomated Deployment