Skip to content

feat: add container packaging, Docker Compose, and CI pipeline#198

Closed
Aureliolo wants to merge 1 commit intomainfrom
feat/container-packaging-ci
Closed

feat: add container packaging, Docker Compose, and CI pipeline#198
Aureliolo wants to merge 1 commit intomainfrom
feat/container-packaging-ci

Conversation

@Aureliolo
Copy link
Copy Markdown
Owner

@Aureliolo Aureliolo commented Mar 10, 2026

Summary

  • Backend container: Three-stage Dockerfile (builder → setup → Chainguard Python distroless runtime). CIS Docker Benchmark v1.6.0 hardened: non-root UID 65532, cap_drop: ALL, no-new-privileges, read_only rootfs, no shell/uv/package-manager in final image. All base images version-pinned with Dependabot auto-updates for digests.
  • Web UI scaffold: nginxinc/nginx-unprivileged:1.29.5-alpine with SPA routing, /api/ and /ws reverse proxy to backend, security headers (X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy), gzip compression. Placeholder index.html with live backend health status.
  • Docker Compose: CIS-hardened orchestration with named volumes, container-level healthchecks, tmpfs for writable paths, configurable ports via env vars (BACKEND_PORT, WEB_PORT).
  • CI pipeline (.github/workflows/docker.yml): Parallel backend + web builds, GHCR push, version tagging from pyproject.toml ([tool.commitizen].version) + semver + SHA, Trivy + Grype vulnerability scans (fail on critical/high), cosign image signing, SBOM + SLSA provenance.
  • Dependabot: Added docker ecosystem entries for docker/backend/ and docker/web/ — auto-updates image digests and versions daily.
  • Housekeeping: Moved CONTRIBUTING.md and SECURITY.md to .github/, deleted empty config/ directory, added web/node_modules/ and web/dist/ to .gitignore.
  • Documentation: Updated README.md with Docker Compose quickstart and tech stack additions. Updated CLAUDE.md with Docker commands, CI workflow reference, and Dependabot note.

Architecture

docker/
  backend/
    Dockerfile          # 3-stage: python:3.14.3-slim → chainguard/python (setup) → chainguard/python (distroless)
    .dockerignore
  web/
    Dockerfile          # nginxinc/nginx-unprivileged:1.29.5-alpine
    .dockerignore
  compose.yml           # Orchestration with CIS hardening
  compose.override.yml  # Local dev overrides (debug logging)
  .env.example          # Environment variable reference
web/
  index.html            # Placeholder dashboard with health check
  nginx.conf            # SPA routing + API/WebSocket proxy
.github/workflows/
  docker.yml            # CI: build → push → scan → sign

Verified locally

  • docker compose -f docker/compose.yml build — both images build
  • docker compose -f docker/compose.yml up -d — both services start, backend healthy
  • curl localhost:8000/api/v1/health — backend responds (v0.1.0)
  • curl localhost:3000 — web UI serves HTML
  • curl localhost:3000/api/v1/health — nginx proxies to backend correctly
  • Backend runs as non-root (UID 65532, distroless — no whoami, no ls, no shell)
  • No uv binary in final image (CVE-free)
  • Backend image: 460MB, Web image: 81.5MB
  • Zero latest tags in any Dockerfile or compose file

Test plan

  • CI: verify docker.yml workflow runs on push to main (builds, scans, signs)
  • CI: verify Trivy + Grype pass with zero critical/high CVEs
  • CI: verify images push to GHCR with correct version tags
  • Local: docker compose -f docker/compose.yml build && docker compose -f docker/compose.yml up -d
  • Local: verify health endpoint, web proxy, non-root execution

Review notes

  • Pre-reviewed in quick mode (no Python source changes — infra/docs only)
  • No existing code was modified — all new files + doc updates
  • DESIGN_SPEC.md intentionally NOT updated (deferred until UI framework is chosen per plan)

Closes #267

Three-stage backend Dockerfile (builder → setup → Chainguard Python
distroless runtime) with CIS Docker Benchmark hardening: non-root
UID 65532, cap_drop ALL, no-new-privileges, read-only rootfs,
no shell/uv/package-manager in final image.

Web UI scaffold with nginx-unprivileged (non-root), SPA routing,
API/WebSocket reverse proxy to backend, security headers.

Docker Compose orchestration with named volumes, healthchecks,
tmpfs for writable paths, and configurable ports via env vars.

GitHub Actions CI workflow: parallel backend + web builds, GHCR push,
version tagging from pyproject.toml, Trivy + Grype vulnerability scans,
cosign image signing, SBOM + SLSA provenance.

Housekeeping: move CONTRIBUTING.md and SECURITY.md to .github/,
delete empty config/ directory, add Dependabot Docker digest updates,
update README.md and CLAUDE.md with Docker documentation.
Copilot AI review requested due to automatic review settings March 10, 2026 16:25
@github-actions
Copy link
Copy Markdown
Contributor

Dependency Review

The following issues were found:
  • ✅ 0 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ⚠️ 7 package(s) with unknown licenses.
See the Details below.

License Issues

.github/workflows/docker.yml

PackageVersionLicenseIssue Type
actions/checkout6.*.*NullUnknown License
anchore/scan-action6.*.*NullUnknown License
docker/build-push-action6.*.*NullUnknown License
docker/login-action3.*.*NullUnknown License
docker/metadata-action5.*.*NullUnknown License
docker/setup-buildx-action3.*.*NullUnknown License
sigstore/cosign-installer3.*.*NullUnknown License
Allowed Licenses: MIT, MIT-0, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, MPL-2.0, PSF-2.0, Unlicense, 0BSD, CC0-1.0, Python-2.0, Python-2.0.1, LicenseRef-scancode-free-unknown

OpenSSF Scorecard

PackageVersionScoreDetails
actions/actions/checkout 6.*.* 🟢 5.9
Details
CheckScoreReason
Maintained⚠️ 23 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 2
Code-Review🟢 10all changesets reviewed
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Binary-Artifacts🟢 10no binaries found in the repo
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Packaging⚠️ -1packaging workflow not detected
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Pinned-Dependencies🟢 3dependency not pinned by hash detected -- score normalized to 3
Security-Policy🟢 9security policy file detected
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
SAST🟢 8SAST tool detected but not run on all commits
actions/anchore/scan-action 6.*.* 🟢 7.9
Details
CheckScoreReason
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Code-Review🟢 10all changesets reviewed
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
Binary-Artifacts🟢 10no binaries found in the repo
Packaging⚠️ -1packaging workflow not detected
Token-Permissions🟢 10GitHub workflow tokens follow principle of least privilege
Pinned-Dependencies🟢 8dependency not pinned by hash detected -- score normalized to 8
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
License🟢 10license file detected
Fuzzing⚠️ 0project is not fuzzed
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ -1internal error: error during branchesHandler.setup: internal error: some github tokens can't read classic branch protection rules: https://github.com/ossf/scorecard-action/blob/main/docs/authentication/fine-grained-auth-token.md
Security-Policy🟢 10security policy file detected
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
actions/aquasecurity/trivy-action master 🟢 6.7
Details
CheckScoreReason
Maintained🟢 1017 commit(s) and 2 issue activity found in the last 90 days -- score normalized to 10
Code-Review🟢 9Found 15/16 approved changesets -- score normalized to 9
Packaging⚠️ -1packaging workflow not detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Binary-Artifacts🟢 10no binaries found in the repo
Pinned-Dependencies🟢 8dependency not pinned by hash detected -- score normalized to 8
Token-Permissions🟢 7detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ -1internal error: error during branchesHandler.setup: internal error: some github tokens can't read classic branch protection rules: https://github.com/ossf/scorecard-action/blob/main/docs/authentication/fine-grained-auth-token.md
Security-Policy⚠️ 0security policy file not detected
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
actions/docker/build-push-action 6.*.* 🟢 6.4
Details
CheckScoreReason
Code-Review🟢 10all changesets reviewed
Security-Policy🟢 9security policy file detected
Maintained🟢 1030 commit(s) and 12 issue activity found in the last 90 days -- score normalized to 10
Binary-Artifacts🟢 10no binaries found in the repo
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Branch-Protection⚠️ -1internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration
Signed-Releases⚠️ -1no releases found
Packaging🟢 10packaging workflow detected
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
actions/docker/login-action 3.*.* 🟢 6.8
Details
CheckScoreReason
Maintained🟢 1030 commit(s) and 3 issue activity found in the last 90 days -- score normalized to 10
Security-Policy🟢 9security policy file detected
Code-Review🟢 10all changesets reviewed
Packaging⚠️ -1packaging workflow not detected
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Binary-Artifacts🟢 10no binaries found in the repo
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
License🟢 10license file detected
Fuzzing⚠️ 0project is not fuzzed
Branch-Protection⚠️ -1internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration
Signed-Releases⚠️ -1no releases found
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
SAST🟢 9SAST tool detected but not run on all commits
actions/docker/metadata-action 5.*.* 🟢 6.4
Details
CheckScoreReason
Security-Policy🟢 9security policy file detected
Code-Review🟢 10all changesets reviewed
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Maintained🟢 1030 commit(s) and 4 issue activity found in the last 90 days -- score normalized to 10
Binary-Artifacts🟢 10no binaries found in the repo
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Branch-Protection⚠️ -1internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration
Signed-Releases⚠️ -1no releases found
Packaging🟢 10packaging workflow detected
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
actions/docker/setup-buildx-action 3.*.* 🟢 6.4
Details
CheckScoreReason
Code-Review🟢 10all changesets reviewed
Security-Policy🟢 9security policy file detected
Maintained🟢 1030 commit(s) and 2 issue activity found in the last 90 days -- score normalized to 10
Binary-Artifacts🟢 10no binaries found in the repo
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ -1internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration
Packaging🟢 10packaging workflow detected
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
actions/sigstore/cosign-installer 3.*.* 🟢 7.5
Details
CheckScoreReason
Code-Review🟢 10all changesets reviewed
Binary-Artifacts🟢 10no binaries found in the repo
Maintained🟢 56 commit(s) and 1 issue activity found in the last 90 days -- score normalized to 5
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Packaging⚠️ -1packaging workflow not detected
Pinned-Dependencies🟢 10all dependencies are pinned
Token-Permissions🟢 10GitHub workflow tokens follow principle of least privilege
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ -1internal error: error during branchesHandler.setup: internal error: some github tokens can't read classic branch protection rules: https://github.com/ossf/scorecard-action/blob/main/docs/authentication/fine-grained-auth-token.md
Security-Policy🟢 10security policy file detected
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0

Scanned Files

  • .github/workflows/docker.yml

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 10, 2026

📝 Walkthrough

Walkthrough

Introduces comprehensive Docker containerization and CI/CD infrastructure for the project. Adds multi-stage Dockerfiles for backend and web services, GitHub Actions workflow for image building, scanning, signing, and publishing to GHCR, Docker Compose configuration with security hardening, web dashboard UI, and updated documentation.

Changes

Cohort / File(s) Summary
Dependency and CI Configuration
.github/dependabot.yml, .github/workflows/docker.yml
Adds Dependabot configuration for Docker updates for backend and web directories. Introduces new GitHub Actions workflow orchestrating multi-stage Docker builds, security scanning with Trivy and Grype, image signing with cosign, and GHCR publishing with caching, SBOM, and provenance.
Backend Containerization
docker/backend/Dockerfile, docker/backend/.dockerignore
Multi-stage Dockerfile (builder, setup, runtime) using Chainguard distroless Python runtime with CIS hardening, non-root operation, and health checks. Includes UV for dependency management and layer caching. .dockerignore excludes development artifacts from build context.
Web Containerization
docker/web/Dockerfile, docker/web/nginx.conf, docker/web/.dockerignore, web/index.html
Introduces nginx-unprivileged-based web Dockerfile with OCI metadata labels and health checks. Adds nginx configuration with gzip, security headers, SPA routing, API proxy to backend, and WebSocket support. Includes simple dashboard landing page with JavaScript health check polling backend every 15 seconds.
Docker Compose Stack
docker/compose.yml, docker/compose.override.yml, docker/.env.example
Defines multi-service Docker Compose stack with security hardening (no-new-privileges, dropped capabilities, read-only filesystems, tmpfs mounts). Backend service uses named volume for data persistence; web service depends on backend health check. Override file enables debug logging for local development. Environment template provides configuration placeholders.
Documentation and Ignores
README.md, CLAUDE.md, .gitignore
Expands README with Docker and Docker Compose setup instructions, system requirements, and getting started workflows. Updates CLAUDE.md with Docker section and CI details. Adds web build artifacts and dependencies to .gitignore.

Sequence Diagram(s)

sequenceDiagram
    participant GH as GitHub
    participant GHA as GitHub Actions
    participant Buildx as Docker Buildx
    participant GHCR as GHCR Registry
    participant Trivy as Trivy Scanner
    participant Grype as Grype Scanner
    participant Cosign as Cosign

    GH->>GHA: push to main or tag v*
    GHA->>GHA: Extract version from pyproject.toml
    GHA->>Buildx: Build backend image (multi-stage, with cache)
    Buildx->>GHCR: Push backend with tags and SBOM
    GHA->>Trivy: Scan backend image (CRITICAL/HIGH)
    Trivy-->>GHA: Report vulnerabilities
    GHA->>Grype: Scan backend image (severity: high)
    Grype-->>GHA: Report vulnerabilities
    GHA->>Cosign: Sign backend image
    Cosign->>GHCR: Store signature
    
    par Build Web in Parallel
        GHA->>Buildx: Build web image (nginx-unprivileged)
        Buildx->>GHCR: Push web with tags and SBOM
        GHA->>Trivy: Scan web image (CRITICAL/HIGH)
        Trivy-->>GHA: Report vulnerabilities
        GHA->>Grype: Scan web image (severity: high)
        Grype-->>GHA: Report vulnerabilities
        GHA->>Cosign: Sign web image
        Cosign->>GHCR: Store signature
    end
    
    GHA-->>GH: Workflow complete
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and accurately summarizes the main changes: container packaging (backend and web Dockerfiles, compose files), Docker Compose orchestration, and CI pipeline (docker.yml workflow).
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The pull request description is comprehensive and directly related to the changeset, detailing backend containerization, Docker Compose orchestration, CI pipeline, security hardening, and documentation updates.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/container-packaging-ci
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/container-packaging-ci

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces comprehensive containerization for the AI Company application, encompassing both its backend and a new placeholder web UI. It establishes a robust Docker Compose setup for local development and deployment, alongside a secure and automated CI pipeline for building, scanning, and signing container images. These changes significantly enhance the project's deployability, security posture, and development workflow by standardizing the environment and automating image management.

Highlights

  • Containerization: Implemented multi-stage Dockerfiles for the backend (Python, Chainguard distroless, CIS-hardened) and a web UI (Nginx unprivileged) to ensure secure and minimal images.
  • Orchestration: Introduced Docker Compose for local development and deployment, including CIS hardening, named volumes, container-level health checks, and tmpfs for writable paths.
  • CI/CD Pipeline: Established a GitHub Actions workflow (.github/workflows/docker.yml) for parallel backend and web builds, pushing to GHCR, vulnerability scanning (Trivy, Grype), image signing (cosign), and generating SBOMs and SLSA provenance.
  • Security Enhancements: Applied CIS Docker Benchmark v1.6.0 hardening, non-root execution (UID 65532), capability dropping (cap_drop: ALL), no-new-privileges, read_only rootfs, and security headers for the web UI.
  • Dependency Management: Configured Dependabot to automatically update Docker image digests and versions daily for both backend and web services.
  • Project Structure & Housekeeping: Refactored CONTRIBUTING.md and SECURITY.md to the .github/ directory, deleted the empty config/ directory, and updated .gitignore to exclude web UI build artifacts (node_modules/, dist/).
  • Documentation: Updated README.md with a Docker Compose quickstart guide and tech stack additions, and CLAUDE.md with Docker commands, CI workflow references, and Dependabot notes.
Changelog
  • .github/dependabot.yml
    • Added Docker ecosystem entries for backend and web directories to enable daily image digest and version updates.
  • .gitignore
    • Appended entries to ignore node_modules and dist directories within the web/ folder.
  • CLAUDE.md
    • Expanded the documentation with a new 'Docker' section detailing build/run commands, container characteristics, CI workflow, and Dependabot integration.
    • Updated the CI section to mention the new Docker workflow.
  • CONTRIBUTING.md
    • Renamed to .github/CONTRIBUTING.md.
  • README.md
    • Updated the 'Tech Stack' and 'System Requirements' sections to include Docker and Nginx.
    • Added a new 'Docker Compose (full stack)' quickstart guide.
    • Updated the link to CONTRIBUTING.md.
  • SECURITY.md
    • Renamed to .github/SECURITY.md.
  • docker/.env.example
    • Added a new example environment file for Docker Compose, defining LLM API key, application logging, database paths, and container ports.
  • docker/backend/.dockerignore
    • Added a new .dockerignore file for the backend Docker build context, excluding unnecessary files and directories.
  • docker/backend/Dockerfile
    • Added a new multi-stage Dockerfile for the backend, utilizing Chainguard Python distroless for a CIS-hardened, non-root runtime environment.
  • docker/compose.override.yml
    • Added a new Docker Compose override file for local development, allowing debug logging and optional Docker socket mounting.
  • docker/compose.yml
    • Added a new Docker Compose configuration file to orchestrate the backend and web services, including CIS hardening, named volumes, and health checks.
  • docker/web/.dockerignore
    • Added a new .dockerignore file for the web Docker build context, excluding source code and other irrelevant files.
  • docker/web/Dockerfile
    • Added a new Dockerfile for the web UI, based on nginxinc/nginx-unprivileged:1.29.5-alpine, configuring Nginx to serve static files and proxy API/WebSocket requests.
  • web/index.html
    • Added a new placeholder HTML file for the web dashboard, featuring a basic UI and a JavaScript-based backend health check.
  • web/nginx.conf
    • Added a new Nginx configuration file for the web UI, setting up SPA routing, API/WebSocket reverse proxies, gzip compression, and security headers.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/docker.yml
Activity
  • The author performed local verification, confirming successful image builds, service startup, backend health, web UI serving, correct Nginx proxying, non-root backend execution, and optimized image sizes.
  • A test plan was outlined for verifying the CI workflow, vulnerability scans, image pushing to GHCR, and local Docker Compose functionality.
  • The changes were pre-reviewed in 'quick mode' as they primarily involve infrastructure and documentation, with no modifications to existing Python source code.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive and well-thought-out containerization setup for the project, including multi-stage, security-hardened Dockerfiles for the backend and web frontend, a robust Docker Compose configuration, and an associated CI pipeline. The attention to detail, especially regarding security best practices like using distroless images, non-root users, and read-only filesystems, is commendable.

My review has identified a couple of minor areas for improvement in the web container configuration:

  • The nginx.conf uses a deprecated X-XSS-Protection header which should be removed in favor of the existing Content Security Policy.
  • The Dockerfile for the web service overrides the base image's default healthcheck; it would be simpler and more robust to remove this override and inherit the proven healthcheck from the base nginx-unprivileged image.

Overall, this is an excellent contribution that significantly enhances the project's deployment and development story.

Comment on lines +21 to +22
HEALTHCHECK --interval=10s --timeout=3s --retries=3 --start-period=5s \
CMD ["wget", "--spider", "--quiet", "http://localhost:8080/"]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The nginxinc/nginx-unprivileged base image already includes a perfectly suitable HEALTHCHECK instruction that uses curl. By specifying a new HEALTHCHECK, you are overriding this proven implementation. It's better to remove this custom HEALTHCHECK and let the Dockerfile inherit the one from the base image. This simplifies the Dockerfile and relies on the base image's tested configuration.

# Security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The X-XSS-Protection header is deprecated and can introduce security vulnerabilities in some older browsers. Modern browsers do not support it and rely on a strong Content-Security-Policy (CSP) for protection against XSS. Since you already have a CSP in place, this header should be removed to align with current security best practices.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds containerization, Compose orchestration, and a GHCR CI pipeline to run the project as a hardened “backend + nginx web” stack, plus supporting docs/housekeeping.

Changes:

  • Introduces multi-stage backend (Chainguard distroless runtime) and unprivileged nginx web images, plus Compose configuration and env template.
  • Adds GitHub Actions workflow to build/push/scan/sign images and Dependabot rules for Docker updates.
  • Updates docs and ignores for the new Docker/web layout.

Reviewed changes

Copilot reviewed 13 out of 17 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
docker/backend/Dockerfile Backend multi-stage build targeting Chainguard distroless runtime.
docker/backend/.dockerignore Backend-specific ignore rules (currently ineffective with repo-root build context).
docker/web/Dockerfile nginx-unprivileged image serving SPA + proxying API/WS.
docker/web/.dockerignore Web-specific ignore rules (currently ineffective with repo-root build context).
docker/compose.yml Compose stack with CIS-style hardening and named volume for backend data.
docker/compose.override.yml Local-only overrides (debug + optional Docker socket mount).
docker/.env.example Example env file for Compose usage.
web/nginx.conf SPA routing + reverse proxy rules + headers + gzip.
web/index.html Placeholder UI with periodic backend health polling.
.github/workflows/docker.yml CI pipeline for build/push/scan/sign images to GHCR.
.github/dependabot.yml Adds Docker ecosystem updates for backend/web Dockerfiles.
.github/SECURITY.md Adds repository security policy.
.github/CONTRIBUTING.md Moves contributing guide under .github/ (needs link fixes).
README.md Documents Docker Compose quickstart and updated Contributing link.
CLAUDE.md Adds Docker usage notes and references new Docker workflow.
.gitignore Ignores web/node_modules/ and web/dist/.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +3 to +5
build:
context: ..
dockerfile: docker/backend/Dockerfile
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build.context is set to .. (repo root), so Docker will only honor a .dockerignore located at the repo root. The per-image ignores in docker/backend/.dockerignore and docker/web/.dockerignore won’t be applied during docker compose build, which can bloat build context and accidentally send unnecessary files to the daemon/CI. Consider adding a single root .dockerignore suitable for both builds, or change each service’s build context to its own directory and adjust COPY paths accordingly.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +59
# Git
.git/
.github/
.gitignore

# Tests
tests/

# Web UI
web/

# Docker configs (not needed in build context)
docker/

# Virtual environments
.venv/
venv/

# Caches
__pycache__/
*.py[cod]
.mypy_cache/
.ruff_cache/
.pytest_cache/

# Coverage
htmlcov/
coverage.xml
.coverage
.coverage.*

# Databases
*.db
*.sqlite3

# Environment files
.env
.env.*

# IDE
.idea/
.vscode/

# Claude Code config
.claude/

# Documentation (not needed at runtime)
docs/
*.md

# Logs
logs/

# OS files
Thumbs.db
.DS_Store

# uv
.python-version
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This .dockerignore won’t be used by the current build configuration: both docker compose and the CI workflow build the backend with context set to the repo root, and Docker only reads .dockerignore from the build context root. If you want backend-specific ignore rules, switch the backend build context to docker/backend/ (and vendor/copy required sources into that context) or consolidate ignore rules into a single root .dockerignore.

Suggested change
# Git
.git/
.github/
.gitignore
# Tests
tests/
# Web UI
web/
# Docker configs (not needed in build context)
docker/
# Virtual environments
.venv/
venv/
# Caches
__pycache__/
*.py[cod]
.mypy_cache/
.ruff_cache/
.pytest_cache/
# Coverage
htmlcov/
coverage.xml
.coverage
.coverage.*
# Databases
*.db
*.sqlite3
# Environment files
.env
.env.*
# IDE
.idea/
.vscode/
# Claude Code config
.claude/
# Documentation (not needed at runtime)
docs/
*.md
# Logs
logs/
# OS files
Thumbs.db
.DS_Store
# uv
.python-version
# Intentionally left empty.
# Backend images are built from the repository root, which uses the root .dockerignore.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +6
# Only web/ static files are needed in build context
.git/
.github/
docker/
src/
tests/
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This .dockerignore won’t be used by the current build configuration: both docker compose and the CI workflow build the web image with context set to the repo root, and Docker only reads .dockerignore from the build context root. Either add a single root .dockerignore (that still includes web/) or change the web build context to docker/web/ and update COPY paths accordingly.

Copilot uses AI. Check for mistakes.
provenance: true

- name: Trivy scan
uses: aquasecurity/trivy-action@master
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using aquasecurity/trivy-action@master makes the workflow non-reproducible and increases supply-chain risk because the action can change without review. Pin Trivy to a released version tag or (preferably) a commit SHA, consistent with the rest of the workflow’s security goals.

Suggested change
uses: aquasecurity/trivy-action@master
uses: aquasecurity/trivy-action@v0.24.0

Copilot uses AI. Check for mistakes.
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 10, 2026

Greptile Summary

This PR introduces a complete containerisation layer for the AI Company project: a three-stage, CIS-hardened backend Dockerfile targeting Chainguard Python distroless, an unprivileged nginx web container, a Docker Compose orchestration file with strong security defaults (read_only, cap_drop: ALL, no-new-privileges, tmpfs), and a GitHub Actions CI pipeline covering build, GHCR push, vulnerability scanning (Trivy + Grype), and cosign image signing. Supporting changes include Dependabot entries for both Docker contexts, a .env.example reference, and documentation updates to README.md and CLAUDE.md.

Key findings:

  • Critical — Images pushed before scanning: Both build-backend and build-web jobs push images to GHCR with push: true before any vulnerability scan runs. A build that later fails a Trivy or Grype check has already published the image publicly.
  • High — trivy-action@master is unpinned: Every other action in the workflow pins to an immutable version tag; using @master for aquasecurity/trivy-action creates a supply chain risk where any upstream commit silently affects the CI pipeline.
  • Style — Missing Content-Security-Policy in nginx.conf: The other four security headers are present, but CSP — the primary XSS mitigation — is absent. The deprecated X-XSS-Protection header should also be removed.
  • The backend Dockerfile, Compose file, Dependabot config, and overall architecture are well-considered and follow security best practices.

Confidence Score: 2/5

  • Not safe to merge as-is — vulnerable images can be published to GHCR before scans run, and an unpinned @master action introduces a supply chain risk.
  • Two genuine security issues in the CI pipeline lower the score significantly: the push-before-scan ordering means the vulnerability gates cannot prevent a bad image from reaching the registry, and trivy-action@master is a mutable ref in an otherwise tightly-pinned workflow. These are straightforward to fix but should be resolved before merging.
  • .github/workflows/docker.yml needs attention for both the unpinned action ref and the push/scan ordering in both build-backend and build-web jobs.

Important Files Changed

Filename Overview
.github/workflows/docker.yml Two security issues: aquasecurity/trivy-action@master is unpinned (supply chain risk contradicting every other pinned action), and images are pushed to GHCR before vulnerability scans complete, meaning vulnerable images can land in the public registry.
docker/backend/Dockerfile Well-structured three-stage build: ephemeral builder with uv, Chainguard -dev for setup/symlink fixing, distroless runtime at UID 65532. Digest-pinned Chainguard base images, bytecode pre-compiled, no uv/shell in final layer.
web/nginx.conf Missing Content-Security-Policy header and includes the deprecated X-XSS-Protection header. SPA routing, API/WebSocket proxy, and gzip compression are correctly configured.
docker/compose.yml Solid CIS hardening: read-only rootfs, cap_drop ALL, no-new-privileges, tmpfs mounts for writable paths, named volume for persistence, and condition: service_healthy dependency between web and backend.
docker/web/Dockerfile Clean single-stage unprivileged nginx build; version-pinned base image, OCI labels, healthcheck via wget, correct port exposure at 8080.
.github/dependabot.yml Adds docker ecosystem entries for both docker/backend and docker/web directories; daily schedule, consistent configuration with existing entries.
web/index.html Minimal placeholder dashboard with polled backend health check; no external dependencies, vanilla JS, safe for a pre-framework scaffold.
docker/.env.example Comprehensive environment variable reference covering LLM API key, log level, DB/memory paths, host ports, and optional Docker socket; no secrets committed.

Sequence Diagram

sequenceDiagram
    participant Dev as Developer (push to main)
    participant GHA as GitHub Actions
    participant GHCR as GHCR Registry
    participant Trivy as Trivy / Grype
    participant Cosign as cosign (Sigstore)

    Dev->>GHA: push / tag

    GHA->>GHA: version job — extract version from pyproject.toml

    par build-backend & build-web (parallel)
        GHA->>GHCR: docker build-push-action (push: true) ⚠️ image pushed here
        Note over GHCR: Image already public before scan
        GHA->>Trivy: scan image from GHCR
        Trivy-->>GHA: pass / fail (CRITICAL, HIGH)
        GHA->>Trivy: Grype scan
        Trivy-->>GHA: pass / fail (high+)
        GHA->>Cosign: cosign sign image@digest
        Cosign-->>GHCR: transparency log entry
    end
Loading

Last reviewed commit: 6c305c5

provenance: true

- name: Trivy scan
uses: aquasecurity/trivy-action@master
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trivy-action@master is an unpinned mutable ref

aquasecurity/trivy-action@master resolves to whatever the upstream repo's master branch points to at workflow runtime — any commit pushed there automatically takes effect in your CI without review. This is a supply chain attack vector, and it is inconsistent with every other action in this workflow, which pins to a specific immutable version tag (e.g. @v3, @v5, @v6).

Pin this to a specific release tag or, ideally, a full commit SHA. The same issue appears in the build-web job (line 163).

Suggested change
uses: aquasecurity/trivy-action@master
uses: aquasecurity/trivy-action@0.31.0
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/docker.yml
Line: 86

Comment:
**`trivy-action@master` is an unpinned mutable ref**

`aquasecurity/trivy-action@master` resolves to whatever the upstream repo's `master` branch points to at workflow runtime — any commit pushed there automatically takes effect in your CI without review. This is a supply chain attack vector, and it is inconsistent with every other action in this workflow, which pins to a specific immutable version tag (e.g. `@v3`, `@v5`, `@v6`).

Pin this to a specific release tag or, ideally, a full commit SHA. The same issue appears in the `build-web` job (line 163).

```suggestion
        uses: aquasecurity/trivy-action@0.31.0
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +70 to +91
- name: Build and push
id: build
uses: docker/build-push-action@v6
with:
context: .
file: docker/backend/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64
sbom: true
provenance: true

- name: Trivy scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/aureliolo/ai-company-backend:${{ needs.version.outputs.app_version }}
format: table
exit-code: "1"
severity: CRITICAL,HIGH
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vulnerable images land in GHCR before scanning

The "Build and push" step (line 70) uses push: true, which publishes the image to GHCR unconditionally. The Trivy and Grype vulnerability scans only run after the image is already public (lines 85–98). If a scan fails and the workflow is cancelled or the image is never deleted, a critical-severity image remains in the registry.

The same sequencing problem exists in build-web (lines 147–175).

The idiomatic fix is a two-step approach: build to the local Docker daemon (or export as a tarball) for scanning, then push only when all scans pass:

# Step 1 — build only (no push), load into local daemon for scanning
- name: Build (no push)
  id: build
  uses: docker/build-push-action@v6
  with:
    context: .
    file: docker/backend/Dockerfile
    push: false
    load: true
    tags: ${{ steps.meta.outputs.tags }}
    ...

# Step 2 — scan the local image
- name: Trivy scan
  uses: aquasecurity/trivy-action@0.31.0
  with:
    image-ref: ghcr.io/aureliolo/ai-company-backend:${{ needs.version.outputs.app_version }}
    exit-code: "1"
    severity: CRITICAL,HIGH

# Step 3 — push only after all scans pass
- name: Push image
  uses: docker/build-push-action@v6
  with:
    ...
    push: true
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/docker.yml
Line: 70-91

Comment:
**Vulnerable images land in GHCR before scanning**

The "Build and push" step (line 70) uses `push: true`, which publishes the image to GHCR unconditionally. The Trivy and Grype vulnerability scans only run **after** the image is already public (lines 85–98). If a scan fails and the workflow is cancelled or the image is never deleted, a critical-severity image remains in the registry.

The same sequencing problem exists in `build-web` (lines 147–175).

The idiomatic fix is a two-step approach: build to the local Docker daemon (or export as a tarball) for scanning, then push only when all scans pass:

```yaml
# Step 1 — build only (no push), load into local daemon for scanning
- name: Build (no push)
  id: build
  uses: docker/build-push-action@v6
  with:
    context: .
    file: docker/backend/Dockerfile
    push: false
    load: true
    tags: ${{ steps.meta.outputs.tags }}
    ...

# Step 2 — scan the local image
- name: Trivy scan
  uses: aquasecurity/trivy-action@0.31.0
  with:
    image-ref: ghcr.io/aureliolo/ai-company-backend:${{ needs.version.outputs.app_version }}
    exit-code: "1"
    severity: CRITICAL,HIGH

# Step 3 — push only after all scans pass
- name: Push image
  uses: docker/build-push-action@v6
  with:
    ...
    push: true
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +13 to +18
# Security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing Content-Security-Policy header; X-XSS-Protection is deprecated

Two issues with the security headers block:

  1. Content-Security-Policy is absent. CSP is widely considered the most impactful browser security header — it controls which origins scripts, styles, and other resources may load from and is the primary mitigation against XSS. Even a restrictive default policy like default-src 'self' (with script-src 'unsafe-inline' relaxed once a real JS bundle exists) would meaningfully raise the security posture of this container.

  2. X-XSS-Protection "1; mode=block" is deprecated. This header was removed from Chromium in version 78 and from the living standard. Modern browsers ignore it entirely; some older Webkit-based browsers had documented bypass vulnerabilities when it was enabled. It can be safely removed.

Suggested change
# Security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;
Prompt To Fix With AI
This is a comment left during a code review.
Path: web/nginx.conf
Line: 13-18

Comment:
**Missing `Content-Security-Policy` header; `X-XSS-Protection` is deprecated**

Two issues with the security headers block:

1. **`Content-Security-Policy` is absent.** CSP is widely considered the most impactful browser security header — it controls which origins scripts, styles, and other resources may load from and is the primary mitigation against XSS. Even a restrictive default policy like `default-src 'self'` (with `script-src 'unsafe-inline'` relaxed once a real JS bundle exists) would meaningfully raise the security posture of this container.

2. **`X-XSS-Protection "1; mode=block"` is deprecated.** This header was removed from Chromium in version 78 and from the living standard. Modern browsers ignore it entirely; some older Webkit-based browsers had documented bypass vulnerabilities when it was enabled. It can be safely removed.

```suggestion
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/docker.yml:
- Around line 70-98: The workflow currently builds and pushes the image in the
"Build and push" step (uses: docker/build-push-action@v6 with push: true) before
running Trivy (aquasecurity/trivy-action@master) and Grype
(anchore/scan-action@v6) scans; change it to build without pushing by setting
push: false and load: true in the docker/build-push-action@v6 step (id: build)
so the image is available locally, run the Trivy and Grype steps against that
local image reference (use the same tags/output name produced by the build), and
only after both scans succeed add a separate push step (either a second
docker/build-push-action@v6 invocation with push: true or an explicit docker
push) to publish the image.
- Around line 162-168: The Trivy workflow step "Trivy scan" currently uses the
floating reference aquasecurity/trivy-action@master; change that to a specific
released tag to pin the action (match the same pinned version used in the
backend job) by replacing the `@master` reference with the chosen tag (e.g.,
`@vX.Y.Z`) so the step consistently uses a fixed, reviewed release instead of the
master branch.
- Around line 29-34: The version extraction step fails without clear diagnostics
if pyproject.toml is missing, malformed, or lacks tool.commitizen.version;
update the shell step that sets VERSION (the python3 -c invocation using
tomllib.load and accessing ['tool']['commitizen']['version']) to perform robust
validation: wrap parsing and key access in a try/except, check that the nested
keys exist, emit a clear error message to stderr and exit non‑zero on failure,
and only write app_version to GITHUB_OUTPUT and echo the version when validation
succeeds.
- Around line 85-91: The Trivy step "Trivy scan" is using the floating reference
aquasecurity/trivy-action@master; replace that with a specific, pinned release
tag (for example an official semver tag like aquasecurity/trivy-action@vX.Y.Z)
to avoid supply-chain and reproducibility issues, update the workflow to
reference that exact tag in place of "@master" and optionally add a comment
noting the chosen version and rationale so future updates are explicit.

In `@docker/.env.example`:
- Around line 4-5: Update the copy instruction in the comment so it works from
the repository root: replace the current "cp .env.example .env" with "cp
docker/.env.example docker/.env" (i.e., reference docker/.env.example and
docker/.env) so users can run the command from the repo root without cd'ing into
docker/.

In `@docker/backend/.dockerignore`:
- Around line 1-59: The .dockerignore under docker/backend won't be used when
building with context at the repo root (e.g., docker build -f
docker/backend/Dockerfile .); either move or copy these ignore rules into the
repository root .dockerignore so Docker excludes those files when the context is
the repo root, or rename docker/backend/.dockerignore to
docker/backend/Dockerfile.dockerignore so Docker will apply it specifically for
the Dockerfile at docker/backend/Dockerfile; update CI/build scripts to
reference the chosen approach consistently.

In `@docker/backend/Dockerfile`:
- Around line 15-17: The Dockerfile's builder stage uses an unpinned base image
("FROM python:3.14.3-slim"); replace that tag with a digest-pinned reference
(e.g., "python:3.14.3-slim@sha256:...") to ensure reproducible builds—update the
FROM line in the builder stage accordingly by looking up the official Python
image digest for 3.14.3-slim and substituting it in place of the tag.

In `@docker/compose.override.yml`:
- Around line 1-10: The header comment in compose.override.yml wrongly states
the file is "auto-merged by Docker Compose"; update that top comment (or remove
the file) to accurately state that compose.override.yml is only auto-merged when
Docker Compose file discovery is used (no -f flags), and document the correct
usage options: either (a) update the comment to say to include it explicitly
with `-f docker/compose.yml -f docker/compose.override.yml`, (b) remove the
override file if you won't rely on discovery, or (c) switch CI/dev scripts to
use file discovery so the override is picked up; make the change in the
compose.override.yml header comment so the repository guidance is accurate.

In `@docker/web/.dockerignore`:
- Around line 1-16: Move the ignore rules from docker/web/.dockerignore (and any
docker/backend/.dockerignore) into a single repository-root .dockerignore so the
Docker build context (.) actually uses them; consolidate the listed patterns
(e.g., .git/, .github/, docker/, src/, tests/, docs/, .venv/, *.md, *.db, .env*,
*.log, .claude/, .idea/, .vscode/, __pycache__/) into that root .dockerignore,
then remove or rename the service-local .dockerignore files (or document them as
Dockerfile.dockerignore) so they don’t give a false impression that Docker is
excluding those paths.

In `@docker/web/Dockerfile`:
- Line 7: The FROM line uses a non-existent tag; update the base image reference
in the Dockerfile to use the published variant and pin it by digest: replace
"nginxinc/nginx-unprivileged:1.29.5-alpine" with the official
"nginxinc/nginx-unprivileged:1.29.5-alpine-slim@<digest>" (fetch the exact
SHA256 digest for the 1.29.5-alpine-slim manifest from Docker Hub) so the
Dockerfile's FROM instruction uses the -slim tag and is immutable for
reproducible builds and Dependabot updates.

In `@web/index.html`:
- Around line 74-80: The health-check success path currently treats any HTTP
response (including 502/503) as "connected"; update the fetch handling to first
check the Response.ok flag on the Response object (the r in the existing then
callback) before calling r.json(), and if !r.ok set el.className to "status
status-disconnected" and set text.textContent to include the HTTP status (e.g.,
r.status and r.statusText or a short "unhealthy" message) instead of "Backend
connected"; only parse JSON and show the connected message (using
data.data.version and status) when r.ok is true, and ensure any JSON
parse/network errors fall through to the disconnected path as well.

In `@web/nginx.conf`:
- Around line 14-16: Remove the deprecated X-XSS-Protection header and add a
Content-Security-Policy header for defense-in-depth: replace the add_header
X-XSS-Protection "1; mode=block" always; line with an add_header
Content-Security-Policy "<policy>" always; entry (e.g. a minimal policy like
default-src 'self'; script-src 'self' 'unsafe-inline' if needed for the UI) and
keep the existing X-Content-Type-Options and X-Frame-Options lines; ensure the
CSP value is tuned to the app's resources and use the same add_header directive
style as the other headers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 98b7792c-3123-4683-a38e-559feda95c58

📥 Commits

Reviewing files that changed from the base of the PR and between 3274a86 and 6c305c5.

📒 Files selected for processing (17)
  • .github/CONTRIBUTING.md
  • .github/SECURITY.md
  • .github/dependabot.yml
  • .github/workflows/docker.yml
  • .gitignore
  • CLAUDE.md
  • README.md
  • config/.gitkeep
  • docker/.env.example
  • docker/backend/.dockerignore
  • docker/backend/Dockerfile
  • docker/compose.override.yml
  • docker/compose.yml
  • docker/web/.dockerignore
  • docker/web/Dockerfile
  • web/index.html
  • web/nginx.conf
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Agent
  • GitHub Check: Greptile Review
🧰 Additional context used
🪛 Checkov (3.2.334)
docker/web/Dockerfile

[low] 1-22: Ensure that a user for the container has been created

(CKV_DOCKER_3)

🪛 Hadolint (2.14.0)
docker/backend/Dockerfile

[warning] 46-46: Last USER should not be root

(DL3002)

🪛 LanguageTool
CLAUDE.md

[uncategorized] ~59-~59: The official name of this software platform is spelled with a capital “H”.
Context: ...iles, compose, .env.example - CI: .github/workflows/docker.yml — build + push to...

(GITHUB)

README.md

[typographical] ~100-~100: To join two clauses or introduce examples, consider using an em dash.
Context: ...etting Started](docs/getting_started.md) - Setup and installation guide - [Contribu...

(DASH_RULE)


[uncategorized] ~101-~101: The official name of this software platform is spelled with a capital “H”.
Context: ...p and installation guide - Contributing - Branch, commit, and ...

(GITHUB)


[typographical] ~101-~101: To join two clauses or introduce examples, consider using an em dash.
Context: ... Contributing - Branch, commit, and PR workflow - [CLAUD...

(DASH_RULE)

🔇 Additional comments (9)
.gitignore (1)

49-51: Good ignore coverage for frontend artifacts.

Ignoring web/node_modules/ and web/dist/ matches the new Web UI workflow and avoids committing dependency/install state plus generated build output.

web/nginx.conf (1)

1-46: LGTM! Solid nginx configuration.

The configuration correctly handles SPA routing, API proxying, and WebSocket connections with appropriate headers and timeouts. Security hardening with X-Frame-Options, X-Content-Type-Options, and Referrer-Policy is well done.

README.md (1)

57-97: LGTM! Clear and accurate Docker documentation.

The quickstart instructions, service descriptions, and port mappings align with docker/compose.yml. The health check endpoint matches the backend configuration.

.github/workflows/docker.yml (1)

1-14: Good workflow structure with proper security controls.

Appreciate the empty top-level permissions (line 9) with per-job scoping, concurrency control, and persist-credentials: false on checkouts.

docker/compose.yml (2)

32-34: Verify backend healthcheck is defined for service_healthy condition.

The depends_on with condition: service_healthy requires the backend service to have a HEALTHCHECK instruction. This is correctly defined in docker/backend/Dockerfile (lines 97-98), so this will work as expected.


1-47: Excellent CIS-hardened compose configuration.

Strong security posture with:

  • no-new-privileges, cap_drop: ALL, read_only filesystem
  • Scoped tmpfs mounts for required writable paths
  • Named volumes for persistence
  • Health-based service dependencies
docker/backend/Dockerfile (3)

44-60: Hadolint DL3002 is a false positive here.

The static analysis warning about "Last USER should not be root" refers to stage 2 (setup), but this is an intermediate build stage. The final runtime stage correctly runs as non-root user 65532 (line 92). No action needed.


96-98: Clever healthcheck approach for distroless.

Using Python's urllib in exec form for healthcheck is a smart solution for a distroless image without curl/wget. Works correctly without a shell.


1-102: Well-structured multi-stage Dockerfile with strong security posture.

Excellent implementation:

  • Three-stage build with ephemeral builder/setup stages
  • Chainguard distroless runtime (zero CVEs, no shell)
  • Non-root UID 65532, proper ownership handling
  • Digest-pinned base images for reproducibility
  • Optimized layer caching with dependency-first installation
  • OCI-compliant labels

Comment on lines +29 to +34
- name: Extract version from pyproject.toml
id: version
run: |
VERSION=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['tool']['commitizen']['version'])")
echo "app_version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "App version: ${VERSION}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Version extraction lacks error handling.

If tool.commitizen.version is missing or the file is malformed, the step will fail with an unhelpful error. Consider adding validation.

🛡️ Proposed defensive version extraction
       - name: Extract version from pyproject.toml
         id: version
         run: |
-          VERSION=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['tool']['commitizen']['version'])")
+          VERSION=$(python3 -c "
+import tomllib
+import sys
+try:
+    data = tomllib.load(open('pyproject.toml', 'rb'))
+    print(data['tool']['commitizen']['version'])
+except (KeyError, FileNotFoundError) as e:
+    print(f'::error::Failed to extract version: {e}', file=sys.stderr)
+    sys.exit(1)
+          ")
           echo "app_version=${VERSION}" >> "$GITHUB_OUTPUT"
           echo "App version: ${VERSION}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Extract version from pyproject.toml
id: version
run: |
VERSION=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['tool']['commitizen']['version'])")
echo "app_version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "App version: ${VERSION}"
- name: Extract version from pyproject.toml
id: version
run: |
VERSION=$(python3 -c "
import tomllib
import sys
try:
data = tomllib.load(open('pyproject.toml', 'rb'))
print(data['tool']['commitizen']['version'])
except (KeyError, FileNotFoundError) as e:
print(f'::error::Failed to extract version: {e}', file=sys.stderr)
sys.exit(1)
")
echo "app_version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "App version: ${VERSION}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/docker.yml around lines 29 - 34, The version extraction
step fails without clear diagnostics if pyproject.toml is missing, malformed, or
lacks tool.commitizen.version; update the shell step that sets VERSION (the
python3 -c invocation using tomllib.load and accessing
['tool']['commitizen']['version']) to perform robust validation: wrap parsing
and key access in a try/except, check that the nested keys exist, emit a clear
error message to stderr and exit non‑zero on failure, and only write app_version
to GITHUB_OUTPUT and echo the version when validation succeeds.

Comment on lines +70 to +98
- name: Build and push
id: build
uses: docker/build-push-action@v6
with:
context: .
file: docker/backend/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64
sbom: true
provenance: true

- name: Trivy scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/aureliolo/ai-company-backend:${{ needs.version.outputs.app_version }}
format: table
exit-code: "1"
severity: CRITICAL,HIGH

- name: Grype scan
uses: anchore/scan-action@v6
with:
image: ghcr.io/aureliolo/ai-company-backend:${{ needs.version.outputs.app_version }}
fail-build: true
severity-cutoff: high
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Vulnerability scans run after image is pushed to registry.

The current order builds and pushes the image (lines 70-83), then runs scans (lines 85-98). If a scan fails, a potentially vulnerable image is already public in GHCR. Consider building without pushing first, scanning locally, then pushing only if scans pass.

💡 Alternative approach: scan before push
  1. Build with push: false and load: true to load image locally
  2. Run Trivy/Grype scans against the local image
  3. If scans pass, push with a separate docker push step or rebuild with push: true

This adds build time but prevents publishing vulnerable images.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/docker.yml around lines 70 - 98, The workflow currently
builds and pushes the image in the "Build and push" step (uses:
docker/build-push-action@v6 with push: true) before running Trivy
(aquasecurity/trivy-action@master) and Grype (anchore/scan-action@v6) scans;
change it to build without pushing by setting push: false and load: true in the
docker/build-push-action@v6 step (id: build) so the image is available locally,
run the Trivy and Grype steps against that local image reference (use the same
tags/output name produced by the build), and only after both scans succeed add a
separate push step (either a second docker/build-push-action@v6 invocation with
push: true or an explicit docker push) to publish the image.

Comment on lines +85 to +91
- name: Trivy scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/aureliolo/ai-company-backend:${{ needs.version.outputs.app_version }}
format: table
exit-code: "1"
severity: CRITICAL,HIGH
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What is the latest stable version of aquasecurity/trivy-action GitHub Action?

💡 Result:

The latest stable release of aquasecurity/trivy-action is v0.35.0 (released March 7, 2026). [1]

Use it as: uses: aquasecurity/trivy-action@v0.35.0 [1]


Pin Trivy action to a specific version instead of @master.

Using @master for the Trivy action is a security risk (supply chain attack vector) and can cause unexpected breakages. Pin to a specific version tag for reproducibility.

🔒 Proposed fix
       - name: Trivy scan
-        uses: aquasecurity/trivy-action@master
+        uses: aquasecurity/trivy-action@v0.35.0
         with:
           image-ref: ghcr.io/aureliolo/ai-company-backend:${{ needs.version.outputs.app_version }}
           format: table
           exit-code: "1"
           severity: CRITICAL,HIGH
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Trivy scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/aureliolo/ai-company-backend:${{ needs.version.outputs.app_version }}
format: table
exit-code: "1"
severity: CRITICAL,HIGH
- name: Trivy scan
uses: aquasecurity/trivy-action@v0.35.0
with:
image-ref: ghcr.io/aureliolo/ai-company-backend:${{ needs.version.outputs.app_version }}
format: table
exit-code: "1"
severity: CRITICAL,HIGH
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/docker.yml around lines 85 - 91, The Trivy step "Trivy
scan" is using the floating reference aquasecurity/trivy-action@master; replace
that with a specific, pinned release tag (for example an official semver tag
like aquasecurity/trivy-action@vX.Y.Z) to avoid supply-chain and reproducibility
issues, update the workflow to reference that exact tag in place of "@master"
and optionally add a comment noting the chosen version and rationale so future
updates are explicit.

Comment on lines +162 to +168
- name: Trivy scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/aureliolo/ai-company-web:${{ needs.version.outputs.app_version }}
format: table
exit-code: "1"
severity: CRITICAL,HIGH
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Same issue: pin Trivy action version.

Apply the same version pinning as recommended for the backend job.

🔒 Proposed fix
       - name: Trivy scan
-        uses: aquasecurity/trivy-action@master
+        uses: aquasecurity/trivy-action@0.30.0
         with:
           image-ref: ghcr.io/aureliolo/ai-company-web:${{ needs.version.outputs.app_version }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Trivy scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/aureliolo/ai-company-web:${{ needs.version.outputs.app_version }}
format: table
exit-code: "1"
severity: CRITICAL,HIGH
- name: Trivy scan
uses: aquasecurity/trivy-action@0.30.0
with:
image-ref: ghcr.io/aureliolo/ai-company-web:${{ needs.version.outputs.app_version }}
format: table
exit-code: "1"
severity: CRITICAL,HIGH
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/docker.yml around lines 162 - 168, The Trivy workflow step
"Trivy scan" currently uses the floating reference
aquasecurity/trivy-action@master; change that to a specific released tag to pin
the action (match the same pinned version used in the backend job) by replacing
the `@master` reference with the chosen tag (e.g., `@vX.Y.Z`) so the step
consistently uses a fixed, reviewed release instead of the master branch.

Comment on lines +4 to +5
# Copy this file to .env and fill in values:
# cp .env.example .env
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix the copy command path.

These instructions only work after cd docker/, but the rest of the PR documents repo-root usage. From the repo root, this should point at docker/.env.example and docker/.env.

✏️ Suggested edit
-#   cp .env.example .env
+#   cp docker/.env.example docker/.env
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Copy this file to .env and fill in values:
# cp .env.example .env
# Copy this file to .env and fill in values:
# cp docker/.env.example docker/.env
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker/.env.example` around lines 4 - 5, Update the copy instruction in the
comment so it works from the repository root: replace the current "cp
.env.example .env" with "cp docker/.env.example docker/.env" (i.e., reference
docker/.env.example and docker/.env) so users can run the command from the repo
root without cd'ing into docker/.

Comment on lines +1 to +10
# Local development overrides (auto-merged by Docker Compose)
services:
backend:
environment:
AI_COMPANY_LOG_LEVEL: "debug"
# Docker socket for agent code execution sandbox.
# WARNING: Mounting the Docker socket gives the container full control
# over the Docker daemon. Only enable in trusted development environments.
# volumes:
# - /var/run/docker.sock:/var/run/docker.sock
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

When docker composeis run with-f docker/compose.yml, does Compose automatically include docker/compose.override.yml, or only the files explicitly passed with -f?

💡 Result:

Compose only auto-loads an override file when you don’t pass -f: it searches for compose.yaml (required) plus optional compose.override.yaml in the working directory (and parent dirs) and merges them. If you run docker compose -f docker/compose.yml ..., Compose uses only the file(s) you explicitly list with -f (so docker/compose.override.yml is not implicitly added—you’d need to pass it as another -f). [1]

Source
[1] Docker Docs — “Merge Compose files” (default file lookup + -f behavior) (docs.docker.com)

Citations:


Remove or update the misleading file comment; override file is not auto-merged when using -f docker/compose.yml.

Docker Compose only auto-merges compose.override.yml when file discovery is enabled (no -f flags). With the documented docker compose -f docker/compose.yml ... flow, the override file is ignored unless explicitly passed as a second -f argument. Either update the comment to reflect this, remove the override file, or switch to default file discovery to use it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker/compose.override.yml` around lines 1 - 10, The header comment in
compose.override.yml wrongly states the file is "auto-merged by Docker Compose";
update that top comment (or remove the file) to accurately state that
compose.override.yml is only auto-merged when Docker Compose file discovery is
used (no -f flags), and document the correct usage options: either (a) update
the comment to say to include it explicitly with `-f docker/compose.yml -f
docker/compose.override.yml`, (b) remove the override file if you won't rely on
discovery, or (c) switch CI/dev scripts to use file discovery so the override is
picked up; make the change in the compose.override.yml header comment so the
repository guidance is accurate.

Comment on lines +1 to +16
# Only web/ static files are needed in build context
.git/
.github/
docker/
src/
tests/
docs/
.venv/
*.md
*.db
.env*
*.log
.claude/
.idea/
.vscode/
__pycache__/
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

When Docker builds with context .and Dockerfiledocker/web/Dockerfile, which ignore file is used: ./.dockerignore, docker/web/.dockerignore, or docker/web/Dockerfile.dockerignore?

💡 Result:

With build context . and Dockerfile docker/web/Dockerfile, Docker looks for:

  1. A Dockerfile-specific ignore file next to the Dockerfile: docker/web/Dockerfile.dockerignore (takes precedence if present)
  2. Otherwise, the context-root ignore file: ./.dockerignore

docker/web/.dockerignore is not used (unless docker/web were the build context). [1]


🏁 Script executed:

# Find docker-compose files and build configuration
fd -t f "docker-compose|Dockerfile|\.dockerignore" -H

Repository: Aureliolo/ai-company

Length of output: 191


🏁 Script executed:

# Check if Dockerfile exists and what context it uses
find . -name "Dockerfile" -type f | head -20

Repository: Aureliolo/ai-company

Length of output: 143


🏁 Script executed:

# Look for build scripts or CI configuration
fd -t f "\.yml|\.yaml|\.sh" .github | head -20

Repository: Aureliolo/ai-company

Length of output: 257


🏁 Script executed:

# Check the web Dockerfile
cat -n docker/web/Dockerfile

Repository: Aureliolo/ai-company

Length of output: 1154


🏁 Script executed:

# Check if docker-compose exists
fd -t f "docker-compose" .

Repository: Aureliolo/ai-company

Length of output: 46


🏁 Script executed:

# Check the docker CI workflow
cat -n .github/workflows/docker.yml

Repository: Aureliolo/ai-company

Length of output: 6676


🏁 Script executed:

# Check if .dockerignore exists at repository root
ls -la | grep dockerignore

Repository: Aureliolo/ai-company

Length of output: 46


🏁 Script executed:

# Check the backend Dockerfile
cat -n docker/backend/Dockerfile | head -20

Repository: Aureliolo/ai-company

Length of output: 1170


🏁 Script executed:

# Check if docker/web/Dockerfile.dockerignore or docker/backend/Dockerfile.dockerignore exist
ls -la docker/web/Dockerfile.dockerignore docker/backend/Dockerfile.dockerignore 2>&1

Repository: Aureliolo/ai-company

Length of output: 231


🏁 Script executed:

# List what's in the dockerignore files
echo "=== docker/web/.dockerignore ===" && cat docker/web/.dockerignore && echo -e "\n=== docker/backend/.dockerignore ===" && cat docker/backend/.dockerignore

Repository: Aureliolo/ai-company

Length of output: 816


Move these ignore rules to .dockerignore at the repository root.

The build context for both docker/web/Dockerfile and docker/backend/Dockerfile is . (repository root), so Docker never reads docker/web/.dockerignore or docker/backend/.dockerignore. Without any ignore file, the entire repository—including .git/, tests/, src/, etc.—is sent to the Docker builder, increasing build time and exposing unnecessary files. Create a root-level .dockerignore with consolidated rules from both files, or rename each to docker/[service]/Dockerfile.dockerignore (though the root .dockerignore approach is more standard).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker/web/.dockerignore` around lines 1 - 16, Move the ignore rules from
docker/web/.dockerignore (and any docker/backend/.dockerignore) into a single
repository-root .dockerignore so the Docker build context (.) actually uses
them; consolidate the listed patterns (e.g., .git/, .github/, docker/, src/,
tests/, docs/, .venv/, *.md, *.db, .env*, *.log, .claude/, .idea/, .vscode/,
__pycache__/) into that root .dockerignore, then remove or rename the
service-local .dockerignore files (or document them as Dockerfile.dockerignore)
so they don’t give a false impression that Docker is excluding those paths.

# AI Company Web — CIS-hardened nginx container (non-root)
# =============================================================================

FROM nginxinc/nginx-unprivileged:1.29.5-alpine
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Does Dependabot support updating pinned Docker base image digests in FROM image:tag@sha256:...lines, and what digest should be used fornginxinc/nginx-unprivileged:1.29.5-alpine?

💡 Result:

Dependabot support

Yes. Dependabot supports Dockerfile FROM references pinned with digests, including the image:tag@sha256:... form, and can open PRs that update the digest (even when the tag/version stays the same). [1] It also supports updating FROM ...@sha256:... (digest-pinned) lines. [2]

Digest for nginxinc/nginx-unprivileged:1.29.5-alpine

On Docker Hub, the published 1.29.5 Alpine tags for nginxinc/nginx-unprivileged are 1.29.5-alpine-perl and 1.29.5-alpine-slim (there is no 1.29.5-alpine tag shown). [3]

If what you meant was nginxinc/nginx-unprivileged:1.29.5-alpine-perl, Docker Hub’s image digest (as shown by the tag’s “Image Layer Details” URL) is:

FROM nginxinc/nginx-unprivileged:1.29.5-alpine-perl@sha256:d4b171321d3848f48826344569ae8d7b2caf8fdfa0028a6e747328f45ca026dc

[4]

If you confirm which exact tag/variant you want (-perl, -slim, or another OS/arch), I can give the matching digest for that one too.

References: [1] https://www.bretfisher.com/blog/silent-rebuilds, [2] https://gds-way.digital.cabinet-office.gov.uk/manuals/programming-languages/docker.html, [3] https://hub.docker.com/r/nginxinc/nginx-unprivileged/tags, [4] https://hub.docker.com/layers/nginxinc/nginx-unprivileged/1.29.5-alpine-perl/images/sha256-d4b171321d3848f48826344569ae8d7b2caf8fdfa0028a6e747328f45ca026dc


🏁 Script executed:

cat -n docker/web/Dockerfile

Repository: Aureliolo/ai-company

Length of output: 1154


🏁 Script executed:

git -C . log --oneline --all -- docker/web/Dockerfile | head -20

Repository: Aureliolo/ai-company

Length of output: 134


🏁 Script executed:

rg "nginx-unprivileged" --type-list | head -20

Repository: Aureliolo/ai-company

Length of output: 453


🏁 Script executed:

rg "nginx-unprivileged" -t dockerfile

Repository: Aureliolo/ai-company

Length of output: 98


🏁 Script executed:

cat docker/web/nginx.conf 2>/dev/null || echo "File not found"

Repository: Aureliolo/ai-company

Length of output: 78


🏁 Script executed:

find . -name "*.md" -o -name "*.txt" | xargs grep -l "nginx" 2>/dev/null || echo "No matches"

Repository: Aureliolo/ai-company

Length of output: 87


🏁 Script executed:

rg "1\.29" -A 2 -B 2

Repository: Aureliolo/ai-company

Length of output: 520


🏁 Script executed:

find . -path ./node_modules -prune -o -name "nginx.conf" -type f -print

Repository: Aureliolo/ai-company

Length of output: 80


🏁 Script executed:

ls -la docker/web/

Repository: Aureliolo/ai-company

Length of output: 293


🏁 Script executed:

cat web/nginx.conf

Repository: Aureliolo/ai-company

Length of output: 1547


Fix the base image tag; 1.29.5-alpine does not exist on Docker Hub.

The tag nginxinc/nginx-unprivileged:1.29.5-alpine is not available. Docker Hub only publishes 1.29.5-alpine-perl and 1.29.5-alpine-slim for this version. Since the nginx configuration here is standard (no Perl regex features), use -slim. Additionally, pin the image by digest for reproducibility and Dependabot-managed updates.

🔐 Suggested change
-FROM nginxinc/nginx-unprivileged:1.29.5-alpine
+FROM nginxinc/nginx-unprivileged:1.29.5-alpine-slim@sha256:0b5d2d1d2b8a9f7e7b8d5f4e3c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b

(Exact digest should be pulled from Docker Hub's image manifest for 1.29.5-alpine-slim.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker/web/Dockerfile` at line 7, The FROM line uses a non-existent tag;
update the base image reference in the Dockerfile to use the published variant
and pin it by digest: replace "nginxinc/nginx-unprivileged:1.29.5-alpine" with
the official "nginxinc/nginx-unprivileged:1.29.5-alpine-slim@<digest>" (fetch
the exact SHA256 digest for the 1.29.5-alpine-slim manifest from Docker Hub) so
the Dockerfile's FROM instruction uses the -slim tag and is immutable for
reproducible builds and Dependabot updates.

Comment on lines +74 to +80
fetch("/api/v1/health")
.then(function (r) { return r.json(); })
.then(function (data) {
var s = data.data && data.data.status;
el.className = "status status-connected";
text.textContent = "Backend connected (v" + (data.data && data.data.version || "?") + " \u2014 " + (s || "ok") + ")";
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does fetch() reject on HTTP 4xx/5xx responses, or only on network failures?

💡 Result:

fetch() does not reject its Promise just because the server returned an HTTP error status (4xx/5xx). It resolves with a Response object (where response.ok is false for non‑2xx statuses), and it rejects only for failures that prevent getting a response at all (e.g., network errors, bad URL, some CORS-related failures). [1]

To treat 4xx/5xx as errors, you must check and throw yourself:

const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);

(Also, a “network error” Response can be created via Response.error(), which is meant to cause fetch() to reject—commonly in service workers.) [2]

Sources: [1] (developer.mozilla.org), [2] (developer.mozilla.org)

Citations:


🏁 Script executed:

cd web && find . -name "index.html" -type f

Repository: Aureliolo/ai-company

Length of output: 76


🏁 Script executed:

cd web && sed -n '70,85p' index.html

Repository: Aureliolo/ai-company

Length of output: 725


Don't mark non-2xx health responses as connected.

fetch() only rejects on network errors. A 502/503 JSON response will still hit the success path here, and line 78 will show "Backend connected" for an unhealthy backend. Check r.ok before parsing.

🩹 Suggested fix
-        fetch("/api/v1/health")
-          .then(function (r) { return r.json(); })
+        fetch("/api/v1/health", { cache: "no-store" })
+          .then(function (r) {
+            if (!r.ok) {
+              throw new Error("Health check failed: " + r.status);
+            }
+            return r.json();
+          })
           .then(function (data) {
             var s = data.data && data.data.status;
             el.className = "status status-connected";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fetch("/api/v1/health")
.then(function (r) { return r.json(); })
.then(function (data) {
var s = data.data && data.data.status;
el.className = "status status-connected";
text.textContent = "Backend connected (v" + (data.data && data.data.version || "?") + " \u2014 " + (s || "ok") + ")";
})
fetch("/api/v1/health", { cache: "no-store" })
.then(function (r) {
if (!r.ok) {
throw new Error("Health check failed: " + r.status);
}
return r.json();
})
.then(function (data) {
var s = data.data && data.data.status;
el.className = "status status-connected";
text.textContent = "Backend connected (v" + (data.data && data.data.version || "?") + " \u2014 " + (s || "ok") + ")";
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/index.html` around lines 74 - 80, The health-check success path currently
treats any HTTP response (including 502/503) as "connected"; update the fetch
handling to first check the Response.ok flag on the Response object (the r in
the existing then callback) before calling r.json(), and if !r.ok set
el.className to "status status-disconnected" and set text.textContent to include
the HTTP status (e.g., r.status and r.statusText or a short "unhealthy" message)
instead of "Backend connected"; only parse JSON and show the connected message
(using data.data.version and status) when r.ok is true, and ensure any JSON
parse/network errors fall through to the disconnected path as well.

Comment on lines +14 to +16
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider modernizing security headers.

  • X-XSS-Protection (line 16) is deprecated and ignored by modern browsers; consider removing it or replacing with a Content-Security-Policy header.
  • A Content-Security-Policy header is recommended for defense-in-depth, even for a placeholder UI.
🛡️ Suggested header improvements
     # Security headers
     add_header X-Content-Type-Options "nosniff" always;
     add_header X-Frame-Options "DENY" always;
-    add_header X-XSS-Protection "1; mode=block" always;
     add_header Referrer-Policy "strict-origin-when-cross-origin" always;
     add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
+    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self';" always;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/nginx.conf` around lines 14 - 16, Remove the deprecated X-XSS-Protection
header and add a Content-Security-Policy header for defense-in-depth: replace
the add_header X-XSS-Protection "1; mode=block" always; line with an add_header
Content-Security-Policy "<policy>" always; entry (e.g. a minimal policy like
default-src 'self'; script-src 'self' 'unsafe-inline' if needed for the UI) and
keep the existing X-Content-Type-Options and X-Frame-Options lines; ensure the
CSP value is tuned to the app's resources and use the same add_header directive
style as the other headers.

@Aureliolo
Copy link
Copy Markdown
Owner Author

Closing in favor of a new PR with all review fixes applied.

@Aureliolo Aureliolo closed this Mar 10, 2026
Aureliolo added a commit that referenced this pull request Mar 10, 2026
## Summary

- **Backend container**: Three-stage Dockerfile (builder → setup →
Chainguard Python distroless runtime). CIS Docker Benchmark v1.6.0
hardened: non-root UID 65532, `cap_drop: ALL`, `no-new-privileges`,
`read_only` rootfs, no shell/uv/package-manager in final image. All base
images version-pinned with Dependabot auto-updates for digests.
- **Web UI scaffold**: `nginxinc/nginx-unprivileged:1.29.5-alpine` with
SPA routing, `/api/` and `/ws` reverse proxy to backend, security
headers (X-Content-Type-Options, X-Frame-Options, Referrer-Policy,
Permissions-Policy, Content-Security-Policy), gzip compression.
Placeholder `index.html` with live backend health status.
- **Docker Compose**: CIS-hardened orchestration with named volumes,
container-level healthchecks, `tmpfs` for writable paths, configurable
ports via env vars (`BACKEND_PORT`, `WEB_PORT`).
- **CI pipeline** (`.github/workflows/docker.yml`): Build → Trivy +
Grype vulnerability scan → push to GHCR → cosign signing with SBOM +
SLSA provenance. All actions pinned to full-length commit SHAs. Images
only published after scans pass.
- **Path-aware CSP middleware**: Strict `default-src 'self'` for API
routes; relaxed policy for `/docs/` allowing Scalar UI resources from
`cdn.jsdelivr.net`, `fonts.scalar.com`, `proxy.scalar.com`.
- **Single root `.dockerignore`**: Consolidated from per-service files
(which were ignored by Docker when context is repo root).
- **All GitHub Actions SHA-pinned** across `docker.yml`, `ci.yml`,
`dependency-review.yml`, `secret-scan.yml`.
- **Documentation updates**: DESIGN_SPEC.md §15.2 (containerization
row), §15.3 (project structure), §15.4 (Container Packaging decision).
Fixed broken links in CONTRIBUTING.md and getting_started.md after
`.github/` move.

## Test plan

- [x] `docker compose -f docker/compose.yml build` — both images build
successfully
- [x] `docker compose -f docker/compose.yml up -d` — containers start,
backend healthy
- [x] `curl http://localhost:8000/api/v1/health` — backend responds with
version 0.1.0
- [x] `curl http://localhost:3000/api/v1/health` — nginx proxies to
backend correctly
- [x] `curl -sI http://localhost:3000/` — CSP and security headers
present
- [x] `http://localhost:8000/docs/api` — Scalar UI renders with relaxed
CSP
- [x] Ruff lint + mypy type-check pass (846 files, 0 issues)
- [x] 6637 tests pass, 94.80% coverage

## Review coverage

- Pre-reviewed in quick mode (infra/docs + 2 Python files)
- Previous round (PR #198) reviewed by 4 external reviewers (CodeRabbit,
Copilot, Greptile, Gemini) + 2 local agents — all 17 valid findings
implemented

Closes #267
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add container packaging, Docker Compose, and CI pipeline

2 participants