diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7336fb7..9906aab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,3 +114,37 @@ jobs: assert len(tables) >= 4, f'Expected at least 4 tables, got {len(tables)}: {tables}' conn.close() " + + docker-build: + name: Docker Build Smoke Test + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build API image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile.api + push: false + cache-from: type=gha,scope=api + cache-to: type=gha,mode=max,scope=api + - name: Build Worker image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile.worker + push: false + cache-from: type=gha,scope=worker + cache-to: type=gha,mode=max,scope=worker + - name: Build Web image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile.web + push: false + cache-from: type=gha,scope=web + cache-to: type=gha,mode=max,scope=web diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ae7a8fa..a93946c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -4,25 +4,67 @@ on: push: branches: [main] +concurrency: + group: deploy-${{ github.ref }} + cancel-in-progress: true + +env: + REGISTRY: ghcr.io + IMAGE_PREFIX: ghcr.io/mriechers/cardigan + jobs: - deploy: - name: Deploy to Production + build-and-push: + name: Build & Push Images runs-on: ubuntu-latest - env: - DEPLOY_SHA: ${{ github.sha }} - DEPLOY_BRANCH: ${{ github.ref_name }} + permissions: + contents: read + packages: write + steps: - uses: actions/checkout@v4 - - name: Deploy placeholder - run: | - echo "=========================================" - echo " Deploy would happen here" - echo " Commit: $DEPLOY_SHA" - echo " Branch: $DEPLOY_BRANCH" - echo "=========================================" - echo "" - echo "Future steps:" - echo " 1. Build Docker image" - echo " 2. Push to container registry" - echo " 3. Deploy to production server" - echo " 4. Run health checks" + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build & push API image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile.api + push: true + tags: | + ${{ env.IMAGE_PREFIX }}-api:latest + ${{ env.IMAGE_PREFIX }}-api:${{ github.sha }} + cache-from: type=gha,scope=deploy-api + cache-to: type=gha,mode=max,scope=deploy-api + + - name: Build & push Worker image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile.worker + push: true + tags: | + ${{ env.IMAGE_PREFIX }}-worker:latest + ${{ env.IMAGE_PREFIX }}-worker:${{ github.sha }} + cache-from: type=gha,scope=deploy-worker + cache-to: type=gha,mode=max,scope=deploy-worker + + - name: Build & push Web image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile.web + push: true + tags: | + ${{ env.IMAGE_PREFIX }}-web:latest + ${{ env.IMAGE_PREFIX }}-web:${{ github.sha }} + cache-from: type=gha,scope=deploy-web + cache-to: type=gha,mode=max,scope=deploy-web diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..3eecf7a --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,119 @@ +# Production deployment — pulls pre-built images from GHCR. +# Usage: docker compose -f docker-compose.prod.yml up -d +# +# Requires: +# - .env file with API keys and CARDIGAN_API_KEY +# - docker login to ghcr.io (see Task 7 in the plan) + +services: + api: + image: ghcr.io/mriechers/cardigan-api:latest + ports: + - "8000:8000" + env_file: .env + environment: + - DATABASE_PATH=/data/db/dashboard.db + - OUTPUT_DIR=/data/output + - CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:3000} + - CARDIGAN_API_KEY=${CARDIGAN_API_KEY} + - TRANSCRIPTS_DIR=/data/transcripts + volumes: + - db-data:/data/db + - output-data:/data/output + - transcript-data:/data/transcripts + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/system/health')"] + interval: 30s + timeout: 5s + start_period: 10s + retries: 3 + labels: + - "com.centurylinklabs.watchtower.enable=true" + restart: unless-stopped + + worker: + image: ghcr.io/mriechers/cardigan-worker:latest + env_file: .env + environment: + - DATABASE_PATH=/data/db/dashboard.db + - OUTPUT_DIR=/data/output + - CARDIGAN_API_KEY=${CARDIGAN_API_KEY} + - TRANSCRIPTS_DIR=/data/transcripts + volumes: + - db-data:/data/db + - output-data:/data/output + - transcript-data:/data/transcripts + depends_on: + api: + condition: service_healthy + labels: + - "com.centurylinklabs.watchtower.enable=true" + restart: unless-stopped + + web: + image: ghcr.io/mriechers/cardigan-web:latest + ports: + - "3000:3000" + environment: + - CARDIGAN_API_KEY=${CARDIGAN_API_KEY} + depends_on: + - api + labels: + - "com.centurylinklabs.watchtower.enable=true" + restart: unless-stopped + + mcp: + image: ghcr.io/mriechers/cardigan-api:latest + command: ["python", "-m", "mcp_server.server"] + ports: + - "8080:8080" + env_file: .env + environment: + - MCP_TRANSPORT=sse + - EDITORIAL_API_URL=http://api:8000 + - DATABASE_PATH=/data/db/dashboard.db + - OUTPUT_DIR=/data/output + - TRANSCRIPTS_DIR=/data/transcripts + volumes: + - db-data:/data/db + - output-data:/data/output + - transcript-data:/data/transcripts + depends_on: + api: + condition: service_healthy + profiles: + - mcp + restart: unless-stopped + + tunnel: + image: cloudflare/cloudflared:latest + command: tunnel --no-autoupdate run + environment: + - TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN} + depends_on: + api: + condition: service_healthy + web: + condition: service_started + profiles: + - tunnel + restart: unless-stopped + + watchtower: + image: containrrr/watchtower + volumes: + - /var/run/docker.sock:/var/run/docker.sock + # Set DOCKER_CONFIG in .env if Docker runs as non-root user + # (e.g., DOCKER_CONFIG=/home/ubuntu/.docker) + - ${DOCKER_CONFIG:-/root/.docker}/config.json:/config.json:ro + environment: + - WATCHTOWER_CLEANUP=true + - WATCHTOWER_POLL_INTERVAL=300 + # Only update containers with the watchtower.enable=true label + - WATCHTOWER_LABEL_ENABLE=true + restart: unless-stopped + +volumes: + db-data: + output-data: + transcript-data: