Files
brain-of-reese/Containerfile
T
ducoterra e29d68d9f0 phase: 82_security_headers
All completion criteria verified green — no defects found, nothing to fix. Final report:

**Phase 82 (security headers) — final verification pass: all green**

- Verified prior-run implementation: `app/core/security_headers.py` (pure-ASGI, header-only, exact A1 CSP), registration in `app/main.py` after `configure_caching` (outermost), unit/integration/E2E suites.
- Deviation confirmed sound: `data:`-URI favicon (blocked by locked CSP) → static `frontend/assets/favicon.svg` in 5 templates + Containerfile `cp`; SVG element byte-identical to the old data-URI (verified programmatically); serves 200 with all three headers.
- Curl check (server booted like e2e conftest, log: `/tmp/curl_security_headers_final.log`): `/`, `/api/health`, `/assets/styles.css`, `/nope` (404) → all three headers, CSP exactly `default-src 'self'; base-uri 'none'; frame-ancestors 'none'`.
- `uv run pytest tests/unit/test_security_headers.py tests/integration/test_security_headers.py -v --no-cov` → 13 passed (incl. SSE byte-identity pin).
- `uv run pytest tests/e2e/test_security_headers.py -v --no-cov` (isolated) → 2 passed (headers + zero CSP violations + painted page).
- SSE tripwire `uv run pytest tests/e2e/test_chat_rag.py -v --no-cov` → 3 passed.
- `uv run pytest --cov=app --cov-report=term-missing` → 1665 passed, app/ 99% (>90%); `uv run ruff check . && uv run pyright` → clean (0 errors).
- `git diff --stat` limited to phase-82 files + the two documented deviations (favicon set, `tests/unit/__init__.py`); no `pyproject.toml`/`uv.lock`/JS diffs.
- Commit + phase-dir move left to the harness per pipeline rules (not executed by me).

Next pending phase: `83_chat_save_payload_limits`.
2026-09-07 23:54:41 -04:00

67 lines
3.4 KiB
Docker

# syntax=docker/dockerfile:1
# Brain of Reese — production image (Podman/Docker compatible).
#
# Stage 1 (frontend): minify/bundle the local frontend with esbuild.
# NO CDN — every asset is built into this image.
# Stage 2 (python): install dependencies with uv (locked).
# Stage 3 (runtime): slim, non-root, migrations + uvicorn.
# ---------- Stage 1: frontend ----------
FROM docker.io/node:22-alpine AS frontend
WORKDIR /build
# Global install: puts the pinned esbuild binary on the PATH for the build step below
# (a local `npm install` leaves it in node_modules/.bin, invisible to RUN).
RUN npm install --no-audit --no-fund -g esbuild@0.25.5
COPY frontend ./
RUN mkdir -p /out/assets \
&& esbuild ./assets/app.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/app.js \
&& esbuild ./assets/router.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/router.js \
&& esbuild ./assets/token-gate.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/token-gate.js \
&& esbuild ./assets/document.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/document.js \
&& esbuild ./assets/login.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/login.js \
&& esbuild ./assets/shared.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/shared.js \
&& esbuild ./assets/doc-edit.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/doc-edit.js \
&& esbuild ./assets/brand.js --minify --outfile=/out/assets/brand.js \
&& esbuild ./assets/markdown.js --minify --outfile=/out/assets/markdown.js \
&& esbuild ./assets/styles.css --minify --outfile=/out/assets/styles.css \
&& cp -r ./assets/themes /out/assets/themes \
&& cp ./assets/favicon.svg /out/assets/favicon.svg \
&& cp ./index.html ./document.html ./login.html ./shared.html ./doc-edit.html /out/
# ---------- Stage 2: python dependencies ----------
FROM docker.io/python:3.12-slim AS python
WORKDIR /app
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-cache --no-install-project
COPY app ./app
RUN uv sync --frozen --no-dev --no-cache
# ---------- Stage 3: runtime ----------
FROM docker.io/python:3.12-slim AS runtime
ENV PATH="/app/.venv/bin:$PATH" \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
BOR_STATIC_DIR=/app/static \
BOR_ENVIRONMENT=production
RUN apt update && apt install -y git
RUN useradd --create-home --uid 10001 reese
WORKDIR /app
COPY --from=python /app/.venv /app/.venv
COPY --from=python /app/app /app/app
# app/api/sync.py (phase 32) imports scripts.git_sync / scripts.import_docs
# at module level — the scripts package must ship in the image or the
# container crashes on boot (phase 33: ModuleNotFoundError: No module
# named 'scripts').
COPY scripts ./scripts
COPY --from=frontend /out /app/static
COPY alembic ./alembic
COPY alembic.ini ./alembic.ini
COPY scripts/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh && chown -R reese:reese /app
USER reese
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=5 \
CMD ["python", "-c", "import sys, httpx; sys.exit(0 if httpx.get('http://127.0.0.1:8000/api/health', timeout=4).status_code == 200 else 1)"]
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]