From bd078c058e071c1c04373cebe30b27d6f0d8d967 Mon Sep 17 00:00:00 2001 From: Robert Helewka Date: Sun, 19 Jul 2026 13:10:54 -0400 Subject: [PATCH] Add Docker image + Gitea CI for Virgo Dev deploy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Containerize Hold Slayer as a single image (one FastAPI process serving REST/WS/MCP and its built SvelteKit dashboard) for deployment to Virgo Dev on triton. - Dockerfile: 3-stage (node builds the dashboard → python wheels → runtime). Runs from source via `pip install -e .` so db/database.py resolves alembic.ini (via __file__.parent.parent) and migrations run on boot. The loose top-level modules (main.py, config.py) also require the source layout. pjsua2 is left unbuilt (documented stub media) — fine for a mock-SIP deploy. - .dockerignore: keep .env and gitignored dashboard build artifacts out of the context; the node stage builds a fresh dashboard. - CI (.gitea/workflows): single-image Trivy scan + build + push to git.helu.ca/r/hold-slayer (sha / latest-on-main / semver tags), mirroring the Demeter workflow. Requires a PACKAGE_TOKEN Actions secret. Co-Authored-By: Claude Opus 4.8 (1M context) --- .dockerignore | 34 +++++++ .gitea/workflows/cve-scan-docker-build.yml | 105 +++++++++++++++++++++ Dockerfile | 53 +++++++++++ 3 files changed, 192 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/cve-scan-docker-build.yml create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d725718 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +# Secrets — never bake into the image (injected at runtime via compose env). +.env + +# The dashboard is rebuilt in the node stage and COPY'd in fresh; keep the +# gitignored working-tree copies out of the build context. +dashboard/build/ +dashboard/node_modules/ +dashboard/.svelte-kit/ + +# Python build/cache cruft. +__pycache__/ +**/__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +venv/ +.pytest_cache/ +.ruff_cache/ + +# Local runtime artifacts. +recordings/ +*.db +*.sqlite3 + +# VCS / editor / OS. +.git/ +.gitea/ +.vscode/ +.idea/ +.DS_Store + +# Not needed at runtime. +tests/ +docs/ diff --git a/.gitea/workflows/cve-scan-docker-build.yml b/.gitea/workflows/cve-scan-docker-build.yml new file mode 100644 index 0000000..5520ffe --- /dev/null +++ b/.gitea/workflows/cve-scan-docker-build.yml @@ -0,0 +1,105 @@ +name: CVE Scan & Docker Build + +on: + push: + branches: [main] + # A pushed version tag (e.g. 0.2.0 or v0.2.0) cuts a release: the build + # below stamps the image with the matching semver tag (e.g. :0.2.0) so the + # deploy can pin an immutable release instead of a moving :latest/:sha. + tags: ['*'] + +env: + REGISTRY: git.helu.ca + IMAGE_NAME: ${{ gitea.repository }} + +jobs: + security-scan: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Trivy + run: | + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sudo sh -s -- -b /usr/local/bin + trivy --version + + - name: Install pip-tools and resolve Python dependencies + run: | + python3 -m venv /tmp/scanenv + /tmp/scanenv/bin/pip install --quiet pip-tools + /tmp/scanenv/bin/pip-compile pyproject.toml \ + -o /tmp/requirements.txt \ + --no-header --quiet --allow-unsafe --strip-extras \ + --resolver=backtracking || { + /tmp/scanenv/bin/pip install --quiet . && \ + /tmp/scanenv/bin/pip freeze > /tmp/requirements.txt + } + cat /tmp/requirements.txt + + - name: Scan Python dependencies for CVEs + continue-on-error: true + run: | + trivy fs --scanners vuln --severity HIGH,CRITICAL --format table /tmp/requirements.txt + + - name: Audit dashboard npm dependencies + continue-on-error: true + run: | + cd dashboard + npm ci --ignore-scripts + npm audit --audit-level=high || true + + - name: Scan repository for secrets + continue-on-error: true + run: | + trivy fs --scanners secret --severity HIGH,CRITICAL --format table . + + build-and-push: + runs-on: ubuntu-latest + needs: security-scan + if: always() + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ gitea.actor }} + password: ${{ secrets.PACKAGE_TOKEN }} + + # Single image: Hold Slayer is one FastAPI process exposing REST/WS/MCP + # and serving its own built SvelteKit dashboard at "/" — no separate + # web/nginx image. The Dockerfile's node stage builds the dashboard. + - name: Extract metadata for image + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=sha,prefix= + type=raw,value=latest,enable=${{ gitea.ref == 'refs/heads/main' }} + type=semver,pattern={{version}} + + - name: Build and push image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Scan image for CVEs + continue-on-error: true + run: | + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sudo sh -s -- -b /usr/local/bin + IMAGE_TAG=$(echo "${{ steps.meta.outputs.tags }}" | head -n1) + echo "Scanning image: ${IMAGE_TAG}" + trivy image --severity HIGH,CRITICAL --format table "${IMAGE_TAG}" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6c13e1e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,53 @@ +# Hold Slayer — single image: FastAPI process that also serves the built +# SvelteKit dashboard at "/". One container, four surfaces (REST/WS/MCP/dash). +# +# pjsua2 is deliberately NOT built here — it is not pip-installable (compiled +# from pjproject) and the media pipeline degrades to documented stub mode +# without it. That is correct for a mock-SIP dev deploy (USE_MOCK_SIP=true); +# /health honestly reports the mock engine as "degraded". Building real media +# is a separate, deliberate piece of work. + +# Stage 1: build the SvelteKit dashboard → dashboard/build/ (SPA, static). +# dashboard/build and dashboard/node_modules are gitignored, so build fresh +# here rather than copying a stale working-tree artifact. +FROM node:22-alpine AS dashboard +WORKDIR /dashboard +COPY dashboard/package.json dashboard/package-lock.json ./ +RUN npm ci +COPY dashboard/ ./ +RUN npm run build + +# Stage 2: runtime. The app runs FROM SOURCE at /app (not purely from +# site-packages): db/database.py locates alembic.ini via +# Path(__file__).parent.parent, and main.py/config.py are loose top-level +# modules — both require the source tree layout under the working dir. An +# editable install puts the deps + entry points in place while keeping /app/db, +# /app/config.py, /app/alembic.ini resolving to the real files. +FROM python:3.12-slim +WORKDIR /app + +# build-essential: some deps compile from source (no manylinux wheel). +# curl: required for the compose healthcheck (GET /health). +RUN apt-get update \ + && apt-get install -y --no-install-recommends build-essential curl \ + && rm -rf /var/lib/apt/lists/* + +# Install dependencies first (better layer caching) using just the manifest, +# then the source. -e keeps the package importable from /app so alembic.ini +# and the loose modules resolve correctly at runtime. +COPY pyproject.toml README.md ./ +COPY . . +# Bring in the freshly built dashboard (overwrites any stale gitignored copy). +COPY --from=dashboard /dashboard/build ./dashboard/build + +RUN pip install --no-cache-dir -e . \ + && apt-get purge -y build-essential && apt-get autoremove -y + +EXPOSE 21081 + +# Migrations run in the app's own init_db() on boot (db/database.py), so no +# separate `alembic upgrade` here. Bind host/port from the same env vars +# pydantic-settings reads (HOST/PORT) so configured values and the actual bind +# cannot drift. Deploy sets PORT=21081 (image default 8000 collides with other +# host-net services on triton). +CMD ["sh", "-c", "uvicorn main:app --host ${HOST:-0.0.0.0} --port ${PORT:-21081}"]