Module 8: Capstone Project — Deployed AI System

2. Architecture Integration — Connecting the Pieces

Description

In this capsule you'll understand how Docker images, CI/CD pipelines, and deployment platforms connect into a coherent production flow. You won't learn new tools — you'll see how the tools you already know fit together. The difference between "I know how to use Docker" and "I know how to put an AI system in production" is exactly this: integration.

Context: In previous modules, you worked on each piece separately: optimized Docker images (M2), CI/CD with GitHub Actions (#16), deployment platforms (M7). Now you connect them. The diagram that comes out of this capsule is the map of everything you'll run in the following capsules.


The Integration Architecture

The complete flow

When you git push to your main branch, this is what should happen — without manual intervention:

Developer                  GitHub                    Platform
   │                         │                          │
   │  git push main          │                          │
   ├────────────────────────►│                          │
   │                         │  Trigger workflow         │
   │                         ├──────┐                   │
   │                         │      │ Run tests         │
   │                         │      │ Build image       │
   │                         │      │ Push to registry  │
   │                         │◄─────┘                   │
   │                         │                          │
   │                         │  Deploy (push/webhook)   │
   │                         ├─────────────────────────►│
   │                         │                          │  Pull image
   │                         │                          │  Start container
   │                         │                          │  Health check
   │                         │                          │
   │                         │  Post-deploy validation  │
   │                         ├──────┐                   │
   │                         │      │ Smoke tests       │
   │                         │      │ Verify inference  │
   │                         │◄─────┘                   │
   │                         │                          │
   │  ✅ Deploy complete     │                          │
   │◄────────────────────────┤                          │

Each arrow is an integration point. Each integration point is a place where something can fail. This capsule prepares you for each one.

The three components

┌─────────────────────────────────────────────────────────┐
│                  INTEGRATION ARCHITECTURE                │
│                                                         │
│  ┌──────────┐    ┌──────────────┐    ┌───────────────┐ │
│  │  Docker   │───►│   CI/CD      │───►│   Platform    │ │
│  │  Image    │    │   Pipeline   │    │   (Deploy)    │ │
│  └──────────┘    └──────────────┘    └───────────────┘ │
│       │                │                     │          │
│  Dockerfile       GitHub Actions      Render/Railway    │
│  .dockerignore    build + test        Fly.io/AWS        │
│  Multi-stage      Push to registry    Pull + run        │
│                   Deploy trigger      Health check      │
│                                                         │
│  ┌──────────────────────────────────────────────────┐   │
│  │              CONFIGURATION LAYER                  │   │
│  │  .env.local  │  .env.staging  │  .env.production │   │
│  └──────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

Component 1: Optimized Docker Image

What you already have from Module 2

Your Dockerfile must produce an image that:

  • Is reproducible (same version, same result)
  • Is small (multi-stage build)
  • Contains no secrets (no .env in the image)
  • Has an integrated health check

Docker Image for production

# === Stage 1: Builder ===
FROM python:3.11-slim AS builder

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt

COPY src/ ./src/

# === Stage 2: Runtime ===
FROM python:3.11-slim AS runtime

WORKDIR /app

COPY --from=builder /root/.local /root/.local
COPY --from=builder /app/src ./src/

ENV PATH=/root/.local/bin:$PATH
ENV PYTHONUNBUFFERED=1

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8000/health || exit 1

CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]

The .dockerignore that protects your image

.git
.github
.env
.env.*
__pycache__
*.pyc
.pytest_cache
docs/
tests/
*.md
.vscode
.cursor
node_modules

Local verification before integrating

# Build the image
docker build -t my-ai-app:latest .

# Run locally
docker run -d --name test-app \
    -p 8000:8000 \
    -e OPENAI_API_KEY=$OPENAI_API_KEY \
    my-ai-app:latest

# Verify health
curl http://localhost:8000/health

# Verify inference
curl -X POST http://localhost:8000/api/inference \
    -H "Content-Type: application/json" \
    -d '{"prompt": "Explain what Docker is in one sentence"}'

# Cleanup
docker stop test-app && docker rm test-app

If your image doesn't work locally, it won't work in production. Verify BEFORE integrating.


Component 2: CI/CD Pipeline

How CI/CD connects with Docker

The GitHub Actions pipeline is the bridge between your code and the platform. Its job:

  1. Trigger: Activates when you push to main (or the branch you configure)
  2. Test: Runs unit and integration tests
  3. Build: Builds the Docker image
  4. Push: Uploads the image to a registry (if the platform requires it)
  5. Deploy: Triggers the deployment on the platform
  6. Validate: Runs smoke tests against production

Base pipeline for any platform

# .github/workflows/deploy.yml
name: Deploy AI System

on:
  push:
    branches: [main]

env:
  PYTHON_VERSION: "3.11"

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}

      - name: Install dependencies
        run: |
          pip install -r requirements.txt
          pip install pytest httpx

      - name: Run tests
        run: pytest tests/ -v --tb=short
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          ENVIRONMENT: test

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # === YOUR PLATFORM-SPECIFIC SECTION GOES HERE ===
      # See the per-platform variants below

  validate:
    needs: deploy
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Wait for deployment
        run: sleep 30

      - name: Health check
        run: |
          response=$(curl -s -o /dev/null -w "%{http_code}" ${{ vars.PRODUCTION_URL }}/health)
          if [ "$response" != "200" ]; then
            echo "Health check failed with status $response"
            exit 1
          fi

      - name: Smoke test - inference
        run: |
          response=$(curl -s -X POST ${{ vars.PRODUCTION_URL }}/api/inference \
            -H "Content-Type: application/json" \
            -d '{"prompt": "Test prompt: respond with OK"}')
          echo "Response: $response"
          if echo "$response" | grep -q "error"; then
            echo "Smoke test failed"
            exit 1
          fi

Per-platform variants

Render — Deploy via automatic Git push:

# Render deploys automatically when it detects a push on the connected repo.
# You don't need a deploy step in GitHub Actions.
# Just configure the repo in the Render dashboard.

deploy:
  needs: test
  runs-on: ubuntu-latest
  steps:
    - name: Trigger Render deploy
      run: |
        curl -X POST "${{ secrets.RENDER_DEPLOY_HOOK_URL }}"
      # You get the deploy hook in Settings → Deploy Hook on Render

Railway — Deploy with the CLI:

deploy:
  needs: test
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4

    - name: Install Railway CLI
      run: npm install -g @railway/cli

    - name: Deploy to Railway
      run: railway up --service ${{ vars.RAILWAY_SERVICE_ID }}
      env:
        RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}

Fly.io — Deploy with flyctl:

deploy:
  needs: test
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4

    - name: Setup Fly.io CLI
      uses: superfly/flyctl-actions/setup-flyctl@master

    - name: Deploy to Fly.io
      run: flyctl deploy --remote-only
      env:
        FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

AWS Lambda — Deploy with SAM or the Serverless Framework:

deploy:
  needs: test
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: us-east-1

    - name: Deploy with SAM
      run: |
        sam build
        sam deploy --no-confirm-changeset --no-fail-on-empty-changeset

Component 3: Deployment Platform

What the platform does for you

Regardless of which one you choose, the platform takes care of:

What YOU provide:               What the PLATFORM does:
─────────────────────            ──────────────────────────
Docker image (or code)     →     Build the container
Environment variables      →     Secret injection
Health check endpoint      →     Health verification
App port                   →     Routing + SSL/TLS
                           →     Public domain (*.railway.app, etc.)
                           →     Accessible logs
                           →     Automatic restart if it crashes

Minimum configuration per platform

For Render:

# render.yaml (Infrastructure as Code)
services:
  - type: web
    name: my-ai-app
    env: docker
    plan: free
    healthCheckPath: /health
    envVars:
      - key: OPENAI_API_KEY
        sync: false
      - key: ENVIRONMENT
        value: production

For Railway:

# railway.toml
[build]
builder = "dockerfile"
dockerfilePath = "Dockerfile"

[deploy]
healthcheckPath = "/health"
healthcheckTimeout = 30
restartPolicyType = "on_failure"
restartPolicyMaxRetries = 5

For Fly.io:

# fly.toml
app = "my-ai-app"
primary_region = "iad"

[build]
  dockerfile = "Dockerfile"

[http_service]
  internal_port = 8000
  force_https = true

  [[http_service.checks]]
    interval = "30s"
    timeout = "10s"
    grace_period = "5s"
    method = "GET"
    path = "/health"

Configuration Layer: Multi-environment

The same code, different configuration

Your app must work in three environments with the same Docker image, differentiated only by environment variables:

# src/config.py
from pydantic_settings import BaseSettings
from functools import lru_cache

class Settings(BaseSettings):
    environment: str = "development"
    app_name: str = "AI System"
    debug: bool = False

    openai_api_key: str = ""
    openai_model: str = "gpt-4o-mini"

    log_level: str = "INFO"
    cors_origins: list[str] = ["http://localhost:3000"]

    class Config:
        env_file = ".env"

    @property
    def is_production(self) -> bool:
        return self.environment == "production"

@lru_cache()
def get_settings() -> Settings:
    return Settings()

Variables per environment

# .env.local (development)
ENVIRONMENT=development
DEBUG=true
LOG_LEVEL=DEBUG
OPENAI_API_KEY=sk-...
CORS_ORIGINS=["http://localhost:3000"]

# .env.staging (if applicable)
ENVIRONMENT=staging
DEBUG=false
LOG_LEVEL=INFO
OPENAI_API_KEY=sk-...
CORS_ORIGINS=["https://staging.your-domain.com"]

# Variables on the platform (production)
# NOT in a file — configured in the dashboard or CI/CD secrets
ENVIRONMENT=production
DEBUG=false
LOG_LEVEL=WARNING
OPENAI_API_KEY=sk-...  # In the platform's secrets
CORS_ORIGINS=["https://your-domain.com"]

Health check endpoint

# src/main.py
from fastapi import FastAPI
from src.config import get_settings

app = FastAPI()
settings = get_settings()

@app.get("/health")
async def health_check():
    return {
        "status": "healthy",
        "environment": settings.environment,
        "version": "1.0.0",
    }

@app.post("/api/inference")
async def inference(request: InferenceRequest):
    # Your AI inference logic
    ...

Complete Integration Diagram

Everything together

┌──────────────────────────────────────────────────────────────┐
│                    LOCAL DEVELOPMENT                          │
│                                                              │
│   docker compose up  →  localhost:8000  →  test manually     │
│   .env.local loaded                                          │
└──────────────────────┬───────────────────────────────────────┘
                       │
                  git push main
                       │
┌──────────────────────▼───────────────────────────────────────┐
│                    GITHUB ACTIONS                             │
│                                                              │
│   ┌─────────┐   ┌─────────┐   ┌──────────┐   ┌──────────┐  │
│   │  Test    │──►│  Build  │──►│  Deploy  │──►│ Validate │  │
│   │  pytest  │   │  docker │   │  platform│   │  smoke   │  │
│   └─────────┘   └─────────┘   └──────────┘   └──────────┘  │
│                                                              │
│   Secrets: OPENAI_API_KEY, PLATFORM_TOKEN, PRODUCTION_URL    │
└──────────────────────────────────────────────────────────────┘
                       │
                   deploy trigger
                       │
┌──────────────────────▼───────────────────────────────────────┐
│                    PRODUCTION PLATFORM                        │
│                                                              │
│   Pull image → Start container → Health check → Live ✅      │
│   .env from platform secrets                                 │
│   URL: https://your-app.platform.app                         │
│   Auto-restart on failure                                    │
│   SSL/TLS included                                           │
└──────────────────────────────────────────────────────────────┘

Common Failure Points

Where the integration breaks

PointWhat failsSymptomSolution
Docker buildIncompatible dependenciesBuild fails in CI but not locallyUse an exact pip freeze > requirements.txt
SecretsVariable not configured on the platformApp crashes on startupVerify ALL env vars before deploy
Health checkEndpoint doesn't respond in timePlatform kills the containerAdjust the timeout, check the cold start
PortApp listens on a different portConnection refusedVerify the PORT env var or hardcoded value
RegistryImage not accessibleDeploy fails: image not foundVerify registry permissions
CORSFrontend can't call the APIError in the browser, API works in curlConfigure CORS_ORIGINS for production

Troubleshooting

Problem 1: "Docker build works locally but fails in GitHub Actions"

Cause: Platform difference (M1/M2 Mac vs Linux in CI), system dependencies, or corrupt cache.

Solution:

# Force a no-cache build in CI
- name: Build Docker image
  run: docker build --no-cache -t my-app:latest .

# If you use dependencies with C extensions (numpy, etc.)
# make sure to install build tools
RUN apt-get update && apt-get install -y build-essential

Problem 2: "The deploy completes but the app doesn't respond"

Cause: The app crashes on startup because an environment variable or a dependent service is missing.

Solution:

import sys
from src.config import get_settings

settings = get_settings()
if not settings.openai_api_key:
    print("ERROR: OPENAI_API_KEY not set", file=sys.stderr)
    sys.exit(1)

Problem 3: "Health check passes but inference doesn't work"

Cause: The health check only verifies that the API responds, not that the inference service is configured correctly.

Solution: Implement a health check that verifies dependencies (see capsule 04).

Problem 4: "Everything works the first time but the redeploy fails"

Cause: Stale state on the platform, container doesn't restart cleanly.

Solution:

# Force a clean redeploy (Render)
curl -X POST "$RENDER_DEPLOY_HOOK_URL"

# Railway: force a redeploy
railway up --service SERVICE_ID

# Fly.io: redeploy
flyctl deploy --remote-only --strategy immediate

Problem 5: "Environment variables don't load in production"

Cause: The env vars are in local .env but not configured on the platform.

Solution:

# List configured variables

# Railway
railway variables

# Fly.io
flyctl secrets list

# Render: check in Dashboard → Environment

# Verification script
python -c "
from src.config import get_settings
s = get_settings()
print(f'Environment: {s.environment}')
print(f'API Key set: {bool(s.openai_api_key)}')
print(f'Model: {s.openai_model}')
"

Hands-On Exercises

Exercise 1: Verify your Docker image

Build your Docker image and verify that it works before integrating it with CI/CD.

# Build
docker build -t my-ai-app:test .

# Run with env vars
docker run -d --name test-integration \
    -p 8000:8000 \
    -e OPENAI_API_KEY=$OPENAI_API_KEY \
    -e ENVIRONMENT=test \
    my-ai-app:test

# Verify
curl http://localhost:8000/health
curl -X POST http://localhost:8000/api/inference \
    -H "Content-Type: application/json" \
    -d '{"prompt": "Say hello"}'
See solution
# Successful build
$ docker build -t my-ai-app:test .
# [+] Building 45.3s (12/12) FINISHED

# Successful run
$ docker run -d --name test-integration \
    -p 8000:8000 \
    -e OPENAI_API_KEY=$OPENAI_API_KEY \
    -e ENVIRONMENT=test \
    my-ai-app:test
# abc123def456...

# Health check
$ curl http://localhost:8000/health
# {"status":"healthy","environment":"test","version":"1.0.0"}

# Inference
$ curl -s -X POST http://localhost:8000/api/inference \
    -H "Content-Type: application/json" \
    -d '{"prompt": "Say hello"}'
# {"response":"Hello! How can I help you today?","model":"gpt-4o-mini"}

# Cleanup
$ docker stop test-integration && docker rm test-integration

If the health check fails, review the logs with docker logs test-integration. If inference fails but the health check passes, your API key wasn't injected correctly.

Exercise 2: Create the base GitHub Actions pipeline

Create the .github/workflows/deploy.yml file with the test, deploy, and validate jobs.

See solution
# .github/workflows/deploy.yml
name: Deploy AI System

on:
  push:
    branches: [main]
  workflow_dispatch:

env:
  PYTHON_VERSION: "3.11"

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}
      - run: pip install -r requirements.txt && pip install pytest httpx
      - run: pytest tests/ -v --tb=short
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          ENVIRONMENT: test

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # Add YOUR platform's steps (Render/Railway/Fly.io/AWS)
      - name: Deploy
        run: echo "Add your platform-specific deploy step here"

  validate:
    needs: deploy
    runs-on: ubuntu-latest
    steps:
      - name: Wait for deployment
        run: sleep 45
      - name: Health check
        run: |
          for i in 1 2 3 4 5; do
            status=$(curl -s -o /dev/null -w "%{http_code}" ${{ vars.PRODUCTION_URL }}/health)
            if [ "$status" = "200" ]; then
              echo "Health check passed"
              exit 0
            fi
            echo "Attempt $i: status $status, retrying in 15s..."
            sleep 15
          done
          echo "Health check failed after 5 attempts"
          exit 1

The validate job has retry logic because the deploy can take longer than expected. 5 attempts × 15 seconds = 75 seconds of tolerance.

Exercise 3: Configure multi-environment

Create the src/config.py file with Pydantic Settings and verify that it works locally and in production.

See solution
# src/config.py
from pydantic_settings import BaseSettings
from functools import lru_cache
from typing import Optional

class Settings(BaseSettings):
    environment: str = "development"
    app_name: str = "AI System"
    debug: bool = False
    version: str = "1.0.0"

    openai_api_key: str = ""
    openai_model: str = "gpt-4o-mini"
    openai_temperature: float = 0.7

    log_level: str = "INFO"
    cors_origins: list[str] = ["http://localhost:3000"]
    port: int = 8000

    class Config:
        env_file = ".env"
        env_file_encoding = "utf-8"

    @property
    def is_production(self) -> bool:
        return self.environment == "production"

    @property
    def is_development(self) -> bool:
        return self.environment == "development"

    def validate_for_production(self) -> list[str]:
        """Returns a list of configuration errors."""
        errors = []
        if not self.openai_api_key:
            errors.append("OPENAI_API_KEY is required")
        if self.debug and self.is_production:
            errors.append("DEBUG must be False in production")
        return errors

@lru_cache()
def get_settings() -> Settings:
    return Settings()

Verification:

# Local
ENVIRONMENT=development python -c "
from src.config import get_settings
s = get_settings()
print(f'Env: {s.environment}, Debug: {s.debug}, Key set: {bool(s.openai_api_key)}')
errors = s.validate_for_production()
print(f'Production-ready: {len(errors) == 0}, Errors: {errors}')
"

Exercise 4: Draw your integration diagram

Using this capsule's template, draw the integration diagram specific to your stack. Include: your chosen platform, the services your app needs, and the environment variables per environment.

See solution

Example for a RAG app with Railway:

┌───────────────────────────────────────────────┐
│           LOCAL (docker compose up)            │
│                                               │
│  FastAPI (:8000) ──► ChromaDB (:8001)         │
│       │                                       │
│       └──► OpenAI API (external)              │
│                                               │
│  ENV: .env.local                              │
│  OPENAI_API_KEY=sk-dev...                     │
│  ENVIRONMENT=development                      │
└──────────────────┬────────────────────────────┘
                   │ git push main
┌──────────────────▼────────────────────────────┐
│         GITHUB ACTIONS                         │
│                                               │
│  pytest → build check → railway up            │
│                                               │
│  Secrets: OPENAI_API_KEY, RAILWAY_TOKEN       │
└──────────────────┬────────────────────────────┘
                   │ deploy
┌──────────────────▼────────────────────────────┐
│         RAILWAY (production)                   │
│                                               │
│  FastAPI service ──► ChromaDB volume          │
│       │                                       │
│       └──► OpenAI API (external)              │
│                                               │
│  URL: https://my-rag-app.railway.app          │
│  ENV: configured in Railway dashboard         │
│  OPENAI_API_KEY=sk-prod...                    │
│  ENVIRONMENT=production                       │
└───────────────────────────────────────────────┘

What matters is that you identify each service, each environment variable, and each connection point between components.


Summary

  • The integration architecture connects three components: Docker image, CI/CD pipeline, and deployment platform
  • The flow is: git push → test → build → deploy → validate — without manual intervention
  • Multi-environment means the same code with different configuration: local, staging, production
  • Environment variables are the mechanism for differentiating environments — never hardcode secrets
  • The failure points are at the interfaces: unconfigured secrets, incorrect ports, health checks with timeouts
  • The per-platform configuration varies (render.yaml, railway.toml, fly.toml) but the concept is the same
  • Verify locally before integrating — if Docker doesn't work on your machine, it won't work in production

Additional Resources

  1. GitHub Actions Documentation — Complete GitHub Actions reference
  2. Docker Multi-stage Builds — Docker image optimization
  3. Pydantic Settings Management — Configuration with Pydantic
  4. Render Deploy Hooks — Automate deploys on Render
  5. Railway CLI Reference — Railway CLI for CI/CD
  6. Fly.io Continuous Deployment — CI/CD with Fly.io