Add Docker image + Gitea CI for Virgo Dev deploy

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) <noreply@anthropic.com>
This commit is contained in:
2026-07-19 13:10:54 -04:00
parent e7c84885d9
commit bd078c058e
3 changed files with 192 additions and 0 deletions

34
.dockerignore Normal file
View File

@@ -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/

View File

@@ -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}"

53
Dockerfile Normal file
View File

@@ -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}"]