diff --git a/.dockerignore b/.dockerignore index ca722675..7b5d398f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -78,7 +78,6 @@ conf/*.conf conf/*_functions.conf conf/__pycache__/ conf/backups/ -conf/docker-compose.yaml conf/conda_environment*.yaml conf/requirements*.txt conf/setup-miniconda-patched-conda_environment.yaml diff --git a/.github/instructions/copilot-instructions.md b/.github/instructions/copilot-instructions.md index 43ad28aa..7b42e38e 100644 --- a/.github/instructions/copilot-instructions.md +++ b/.github/instructions/copilot-instructions.md @@ -293,7 +293,7 @@ class RedisClient: **4. Deployment**: - Standalone: `redis-server --port 6379 --appendonly yes` -- Docker: `docker-compose up redis` (see `conf/docker-compose.yaml`) +- Docker: `docker-compose up redis` (see `../juniper-deploy/docker-compose.yml`) - Ubuntu systemd: `sudo systemctl start redis-server` **5. Testing Requirements**: @@ -670,13 +670,13 @@ if __name__ == "__main__": ## Docker Deployment ### Multi-Stage Build Strategy -The `conf/Dockerfile` uses Python 3.9 slim base. For production: +The container image builds from the repo-root `Dockerfile` (the legacy `conf/Dockerfile` was removed; `juniper-deploy` builds the maintained image). For production: **Build image**: ```bash cd /path/to/juniper_canopy -docker build -f conf/Dockerfile -t juniper_canopy:latest . +docker build -t juniper-canopy:latest . ``` **Standalone run**: @@ -698,7 +698,7 @@ docker run -d \ ```bash cd /path/to/juniper_canopy -docker-compose -f conf/docker-compose.yaml up -d +docker-compose -f ../juniper-deploy/docker-compose.yml up -d ``` **Stack includes**: @@ -720,10 +720,10 @@ JWT_SECRET_KEY=your-secret-key-here **Service management**: ```bash -docker-compose -f conf/docker-compose.yaml ps # Status -docker-compose -f conf/docker-compose.yaml logs -f # Follow logs -docker-compose -f conf/docker-compose.yaml down # Stop all -docker-compose -f conf/docker-compose.yaml restart juniper_canopy # Restart service +docker-compose -f ../juniper-deploy/docker-compose.yml ps # Status +docker-compose -f ../juniper-deploy/docker-compose.yml logs -f # Follow logs +docker-compose -f ../juniper-deploy/docker-compose.yml down # Stop all +docker-compose -f ../juniper-deploy/docker-compose.yml restart juniper_canopy # Restart service ``` **Volume persistence**: diff --git a/AGENTS.md b/AGENTS.md index 59aafb1a..b998eef0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,7 @@ **Author**: Paul Calnon **License**: MIT License **Version**: 0.5.0 -**Last Updated**: 2026-07-04 +**Last Updated**: 2026-07-07 --- @@ -540,29 +540,26 @@ cd src && uvicorn main:app --host 0.0.0.0 --port 8050 ## Docker and Local Stack -Containerization configs live under `conf/`: - -- `conf/Dockerfile` – Builds a Juniper Canopy image (FastAPI + Dash + demo/backend integration) -- `conf/docker-compose.yaml` – Optional local stack (app + supporting services like Redis) +The container image builds from the **repo-root `Dockerfile`** (the image `juniper-deploy` builds and publishes). Full-stack local orchestration lives in `../juniper-deploy` (Docker Compose for the whole Juniper stack). The legacy `conf/Dockerfile` + `conf/docker-compose.yaml` were removed — they were superseded by `juniper-deploy`. ### Basic Docker Usage ```bash -# Build image -docker build -f conf/Dockerfile -t juniper_canopy . +# Build the image (repo-root Dockerfile) +docker build -t juniper-canopy . -# Run container -docker run --rm -p 8050:8050 juniper_canopy +# Run the container -- publish loopback on the host (SEC-F22 posture) +docker run --rm -p 127.0.0.1:8050:8050 juniper-canopy -# Or with docker-compose -docker compose -f conf/docker-compose.yaml up --build +# Full stack (canopy + cascor + data + redis) via juniper-deploy +cd ../juniper-deploy && docker compose up --build ``` ### Agent Guidance for Docker - Keep ports and environment variables consistent with `app_config.yaml` and `ServerConstants` -- If you change API paths or WebSocket endpoints, update both FastAPI routes and Docker/docker-compose health checks -- The canonical entrypoint is `uvicorn main:app`—if this changes, update `conf/Dockerfile` +- If you change API paths or WebSocket endpoints, update both FastAPI routes and the Docker health checks +- The canonical entrypoint is `python src/main.py` (settings-driven; the SEC-F22 bind guard evaluates `settings.server.host`)—if this changes, update the repo-root `Dockerfile` ## Constants Management @@ -1142,11 +1139,11 @@ cd src # Build image (root Dockerfile) docker build -t juniper-canopy . -# Run container -docker run --rm -p 8050:8050 juniper-canopy +# Run container -- publish loopback on the host (SEC-F22 posture) +docker run --rm -p 127.0.0.1:8050:8050 juniper-canopy -# Or with docker-compose (conf/ directory) -docker compose -f conf/docker-compose.yaml up --build +# Full stack (canopy + cascor + data + redis) via juniper-deploy +cd ../juniper-deploy && docker compose up --build ``` ## API and WebSocket Contracts diff --git a/conf/Dockerfile b/conf/Dockerfile deleted file mode 100755 index c997b713..00000000 --- a/conf/Dockerfile +++ /dev/null @@ -1,81 +0,0 @@ -# ============================================================================= -# JuniperCanopy — Real-time Monitoring Dashboard -# Multi-stage Dockerfile for production deployment -# ============================================================================= -# Build: docker build -f conf/Dockerfile -t juniper-canopy:latest . -# Run: docker run -p 8050:8050 juniper-canopy:latest -# ============================================================================= - -# ----------------------------------------------------------------------------- -# Stage 1: Builder — Install dependencies -# ----------------------------------------------------------------------------- -FROM python:3.12-slim AS builder - -WORKDIR /build - -# Install build tools -RUN pip install --no-cache-dir --upgrade pip wheel setuptools - -# Install pinned dependencies from lockfile (best layer caching) -COPY requirements.lock ./ -RUN pip install --no-cache-dir -r requirements.lock - -# Copy project files and install without deps (already installed above) -COPY pyproject.toml README.md ./ -COPY src/ ./src/ -RUN pip install --no-cache-dir --no-deps . - -# ----------------------------------------------------------------------------- -# Stage 2: Runtime — Minimal production image -# ----------------------------------------------------------------------------- -FROM python:3.12-slim AS runtime - -LABEL org.opencontainers.image.title="JuniperCanopy" -LABEL org.opencontainers.image.description="Real-time monitoring dashboard for the Juniper ecosystem" -LABEL org.opencontainers.image.authors="Paul Calnon" -LABEL org.opencontainers.image.licenses="MIT" -LABEL org.opencontainers.image.source="https://github.com/pcalnon/juniper-canopy" - -# Install curl for lightweight health checks (avoids spawning Python interpreter) -RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/* - -# Create non-root user -RUN groupadd --gid 1000 juniper && \ - useradd --uid 1000 --gid juniper --shell /bin/bash --create-home juniper - -WORKDIR /app - -# Copy installed packages from builder -COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages -COPY --from=builder /usr/local/bin /usr/local/bin - -# Copy source code and configuration -COPY --chown=juniper:juniper src/ ./src/ -COPY --chown=juniper:juniper conf/ ./conf/ - -# Create required directories -RUN mkdir -p logs data images && chown -R juniper:juniper /app - -USER juniper - -# PYTHONPATH so imports from src/ resolve correctly -ENV PYTHONPATH=/app/src - -# Service configuration — uses JUNIPER_CANOPY_ prefix for pydantic-settings. -# Legacy CASCOR_ vars retained for backward compatibility with deployments -# that have not yet migrated to JUNIPER_CANOPY_ naming. -ENV CASCOR_ENV=production -ENV CASCOR_DEBUG=false -ENV JUNIPER_CANOPY_DEMO_MODE=false -ENV JUNIPER_CANOPY_LOG_LEVEL=INFO -ENV JUNIPER_DATA_URL=http://juniper-data:8100 -ENV CASCOR_SERVICE_URL=http://juniper-cascor:8200 - -EXPOSE 8050 - -# Health check for container orchestration (liveness + readiness) -# curl-based: lightweight, avoids spawning Python interpreter on every probe -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD curl --fail --silent --max-time 5 http://localhost:8050/v1/health || exit 1 - -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8050"] diff --git a/conf/docker-compose.yaml b/conf/docker-compose.yaml deleted file mode 100644 index c8bb106f..00000000 --- a/conf/docker-compose.yaml +++ /dev/null @@ -1,93 +0,0 @@ -# DEPRECATED: This per-service Compose file is superseded by the unified -# orchestration in juniper-deploy/docker-compose.yml. -# Use: cd juniper-deploy && make up -# See: https://github.com/pcalnon/juniper-deploy -# -# Docker Compose configuration for Juniper Canopy with dependencies -version: '3.8' - -services: - # JuniperData dataset generation service - juniper-data: - # NOTE: The JuniperData source is expected to live outside this repository. - # Expected workspace layout: - # / - # this-repo/ (repository containing this file) - # conf/docker-compose.yaml (current file) - # JuniperData/ - # juniper_data/ (build context used below) - # Run `docker compose -f conf/docker-compose.yaml up` from the root of this repo - # so that the relative build context path resolves correctly. - build: - context: ../../../JuniperData/juniper_data - dockerfile: Dockerfile - ports: - - 8100:8100 - environment: - - JUNIPER_DATA_STORAGE_TYPE=memory - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8100/v1/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 5s - restart: unless-stopped - networks: - - cascor-network - - # Main application - juniper-canopy: - build: - context: .. - dockerfile: conf/Dockerfile - ports: - - 8050:8050 - environment: - - CASCOR_ENV=production - - REDIS_URL=redis://redis:6379/0 - - CASCOR_BACKEND_URL=http://cascor-backend:8000 - - JUNIPER_DATA_URL=http://juniper-data:8100 - volumes: - - ../logs:/app/logs - - ../data:/app/data - - ../images:/app/images - depends_on: - - redis - - juniper-data - restart: unless-stopped - networks: - - cascor-network - - # Redis cache - redis: - image: redis:7-alpine - ports: - - 6379:6379 - volumes: - - redis-data:/data - restart: unless-stopped - networks: - - cascor-network - command: redis-server --appendonly yes - - # Optional: CasCor backend placeholder - # cascor-backend: - # build: - # context: ../../cascor - # dockerfile: Dockerfile - # ports: - # - "8000:8000" - # environment: - # - CASCOR_ENV=production - # volumes: - # - ../data:/app/data - # restart: unless-stopped - # networks: - # - cascor-network - -volumes: - redis-data: - -networks: - cascor-network: - driver: bridge diff --git a/docs/DEVELOPER_CHEATSHEET.md b/docs/DEVELOPER_CHEATSHEET.md index b6387e81..bad62201 100644 --- a/docs/DEVELOPER_CHEATSHEET.md +++ b/docs/DEVELOPER_CHEATSHEET.md @@ -22,8 +22,8 @@ |-----------------------------|------------------------------------------------------------------------------------------------------| | Run in demo mode | `./demo` | | Run natively (real backend) | `conda activate JuniperCanopy1 && cd src && uvicorn main:app --port 8050` | -| Run via Docker | `docker build -f conf/Dockerfile -t juniper_canopy . && docker run --rm -p 8050:8050 juniper_canopy` | -| Run via Docker Compose | `docker compose -f conf/docker-compose.yaml up --build` | +| Run via Docker | `docker build -t juniper-canopy . && docker run --rm -p 127.0.0.1:8050:8050 juniper-canopy` | +| Run full stack (Compose) | `cd ../juniper-deploy && docker compose up --build` | | Health check | `curl -s http://localhost:8050/v1/health \| python -m json.tool` | | Liveness / readiness | `curl -s http://localhost:8050/v1/health/live` / `.../v1/health/ready` | | Run all tests | `cd src && pytest tests/ -v` | @@ -102,7 +102,7 @@ Existing components: `training_metrics`, `metrics_panel`, `network_visualizer`, 1. Add to `conf/requirements.txt` (and `conf/conda_environment.yaml` if conda-installable) 2. Run `pip install -r conf/requirements.txt` in the `JuniperCanopy1` conda env -3. Update `conf/Dockerfile` if needed for Docker builds +3. Update the repo-root `Dockerfile` if needed for Docker builds > See: [ENVIRONMENT_SETUP.md](ENVIRONMENT_SETUP.md) diff --git a/docs/deployment/KUBERNETES_DEPLOYMENT_PLAN.md b/docs/deployment/KUBERNETES_DEPLOYMENT_PLAN.md index 53b983af..f00330ad 100644 --- a/docs/deployment/KUBERNETES_DEPLOYMENT_PLAN.md +++ b/docs/deployment/KUBERNETES_DEPLOYMENT_PLAN.md @@ -66,7 +66,7 @@ This document provides a comprehensive plan to containerize and deploy the Junip ### 2.2 Current Docker Configuration -**Location:** `conf/Dockerfile` +**Location:** `Dockerfile` (repo root — the legacy `conf/Dockerfile` was removed) **Issues Identified:** @@ -912,7 +912,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - file: ./conf/Dockerfile + file: ./Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/notes/juniper-canopy_OTHER_DEPENDENCIES.md b/notes/juniper-canopy_OTHER_DEPENDENCIES.md index de136104..8a573b2d 100644 --- a/notes/juniper-canopy_OTHER_DEPENDENCIES.md +++ b/notes/juniper-canopy_OTHER_DEPENDENCIES.md @@ -72,5 +72,5 @@ For conda/mamba-managed dependencies, see `conf/conda_environment_ci.yaml`. - CUDA/cuDNN are optional; the dashboard can operate in demo mode without a GPU backend. - The shared `JuniperPython` conda environment is managed at the ecosystem level. -- Docker is used for optional containerized deployment via `conf/docker-compose.yaml`. +- Docker is used for optional containerized deployment via `juniper-deploy` (Docker Compose for the full stack; the legacy `conf/docker-compose.yaml` was removed). - The Dash/Plotly web framework is pip-managed and listed in `conf/requirements_ci.txt`. diff --git a/src/tests/regression/test_docker_demo_mode_default.py b/src/tests/regression/test_docker_demo_mode_default.py index 159ce564..d92bb729 100644 --- a/src/tests/regression/test_docker_demo_mode_default.py +++ b/src/tests/regression/test_docker_demo_mode_default.py @@ -43,10 +43,6 @@ def test_root_dockerfile_does_not_force_demo_mode(self): dockerfile = (_REPO_ROOT / "Dockerfile").read_text(encoding="utf-8") assert "JUNIPER_CANOPY_DEMO_MODE=1" not in dockerfile - def test_conf_dockerfile_does_not_force_demo_mode(self): - dockerfile = (_REPO_ROOT / "conf" / "Dockerfile").read_text(encoding="utf-8") - assert "JUNIPER_CANOPY_DEMO_MODE=1" not in dockerfile - def test_repo_root_resolution_finds_dockerfiles(self): """Locks in the parents[3] path math used by the tests above. @@ -57,6 +53,5 @@ def test_repo_root_resolution_finds_dockerfiles(self): evaluated. This sanity check makes that class of bug loud. """ assert (_REPO_ROOT / "Dockerfile").is_file(), f"Repo-root Dockerfile not found at {_REPO_ROOT}" - assert (_REPO_ROOT / "conf" / "Dockerfile").is_file(), f"conf/Dockerfile not found under {_REPO_ROOT}" # Sanity: the resolved root is the repo, not src/ or tests/. assert (_REPO_ROOT / "src").is_dir(), f"Expected src/ under {_REPO_ROOT}"