diff --git a/.env.cloud.example b/.env.cloud.example new file mode 100644 index 0000000..c5ebcb7 --- /dev/null +++ b/.env.cloud.example @@ -0,0 +1,18 @@ +# pagepiper cloud environment — copy to .env and fill in secrets +# Used by: docker compose -f compose.cloud.yml -p pagepiper-cloud ... + +# Data directories (host paths, bind-mounted into the api container) +PAGEPIPER_DATA_DIR=/devl/pagepiper-cloud-data +PAGEPIPER_BOOKS_DIR=/devl/pagepiper-cloud-data/books + +# BYOK gate — set to enable hybrid search and RAG chat (BSL feature) +# Leave blank to run BM25-only mode (MIT, no Ollama required) +PAGEPIPER_OLLAMA_URL= + +# Embedding and chat model selection (only used when PAGEPIPER_OLLAMA_URL is set) +PAGEPIPER_EMBED_MODEL=nomic-embed-text +PAGEPIPER_CHAT_MODEL=mistral:7b + +# Heimdall license server (optional — for per-user tier validation) +HEIMDALL_URL=https://license.circuitforge.tech +HEIMDALL_ADMIN_TOKEN= diff --git a/compose.cloud.yml b/compose.cloud.yml new file mode 100644 index 0000000..cb1b884 --- /dev/null +++ b/compose.cloud.yml @@ -0,0 +1,44 @@ +# Pagepiper — cloud managed instance +# Project: pagepiper-cloud (docker compose -f compose.cloud.yml -p pagepiper-cloud ...) +# Web: http://127.0.0.1:8533 → pagepiper.circuitforge.tech (primary) +# → menagerie.circuitforge.tech/pagepiper (secondary) +# API: internal only on pagepiper-cloud-net (nginx proxies /api/ → api:8522) + +services: + api: + build: + context: .. + dockerfile: pagepiper/Dockerfile + restart: unless-stopped + env_file: .env + environment: + CLOUD_MODE: "true" + PAGEPIPER_DATA_DIR: /devl/pagepiper-cloud-data + PAGEPIPER_BOOKS_DIR: /devl/pagepiper-cloud-data/books + # PAGEPIPER_OLLAMA_URL — set in .env (BYOK gate for hybrid search + RAG) + # HEIMDALL_URL, HEIMDALL_ADMIN_TOKEN — set in .env for license validation + volumes: + - /devl/pagepiper-cloud-data:/devl/pagepiper-cloud-data + - ${HOME}/.config/circuitforge:/root/.config/circuitforge:ro + networks: + - pagepiper-cloud-net + + web: + build: + context: . + dockerfile: docker/web/Dockerfile + args: + VITE_BASE_URL: /pagepiper + VITE_API_BASE: /pagepiper + NGINX_CONF: docker/web/nginx.cloud.conf + restart: unless-stopped + ports: + - "8533:80" + networks: + - pagepiper-cloud-net + depends_on: + - api + +networks: + pagepiper-cloud-net: + driver: bridge diff --git a/docker/web/Dockerfile b/docker/web/Dockerfile index e164057..039536e 100644 --- a/docker/web/Dockerfile +++ b/docker/web/Dockerfile @@ -14,6 +14,7 @@ RUN npm run build # Stage 2: serve via nginx FROM nginx:alpine -COPY docker/web/nginx.conf /etc/nginx/conf.d/default.conf +ARG NGINX_CONF=docker/web/nginx.conf +COPY ${NGINX_CONF} /etc/nginx/conf.d/default.conf COPY --from=build /app/dist /usr/share/nginx/html EXPOSE 80 diff --git a/docker/web/nginx.cloud.conf b/docker/web/nginx.cloud.conf new file mode 100644 index 0000000..f026071 --- /dev/null +++ b/docker/web/nginx.cloud.conf @@ -0,0 +1,59 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # API requests when accessed via Caddy (prefix already stripped by handle_path) + location /api/ { + proxy_pass http://api:8522; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $http_x_real_ip; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + proxy_set_header X-CF-Session $http_x_cf_session; + client_max_body_size 50m; + # PDF uploads and LLM inference can be slow + proxy_read_timeout 300s; + proxy_send_timeout 300s; + } + + # API requests when accessed directly via pagepiper.circuitforge.tech + # VITE_API_BASE=/pagepiper means frontend builds calls as /pagepiper/api/... + # Caddy passes these unchanged; nginx strips /pagepiper prefix here. + location /pagepiper/api/ { + rewrite ^/pagepiper(/api/.*)$ $1 break; + proxy_pass http://api:8522; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $http_x_real_ip; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + proxy_set_header X-CF-Session $http_x_cf_session; + client_max_body_size 50m; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + } + + # Static assets at the /pagepiper/ base — used when Caddy does NOT strip the prefix + # (i.e., pagepiper.circuitforge.tech routes, where /pagepiper is the Vite base URL). + # ^~ prevents regex asset location from matching first. + location ^~ /pagepiper/ { + alias /usr/share/nginx/html/; + try_files $uri $uri/ /index.html; + } + + location = /index.html { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + try_files $uri /index.html; + } + + location / { + try_files $uri $uri/ /index.html; + } + + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/manage.sh b/manage.sh index f67a533..f278e5d 100755 --- a/manage.sh +++ b/manage.sh @@ -3,13 +3,17 @@ set -euo pipefail SERVICE=pagepiper WEB_PORT=8521 +CLOUD_WEB_PORT=8533 COMPOSE_FILE="compose.yml" +COMPOSE_CLOUD_FILE="compose.cloud.yml" +CLOUD_PROJECT="pagepiper-cloud" OVERRIDE_ARGS=() [[ -f "compose.override.yml" ]] && OVERRIDE_ARGS=(-f compose.override.yml) usage() { - echo "Usage: $0 {start|stop|restart|status|logs [svc]|open|build|test}" + echo "Usage: $0 {start|stop|restart|status|logs [svc]|open|build|test" + echo " |cloud:start|cloud:stop|cloud:restart|cloud:status|cloud:logs [svc]|cloud:build}" exit 1 } @@ -44,6 +48,27 @@ case "$cmd" in test) conda run -n cf pytest tests/ -v ;; + cloud:start) + docker compose -f "$COMPOSE_CLOUD_FILE" -p "$CLOUD_PROJECT" up -d --build + echo "Pagepiper cloud running → http://localhost:${CLOUD_WEB_PORT}" + ;; + cloud:stop) + docker compose -f "$COMPOSE_CLOUD_FILE" -p "$CLOUD_PROJECT" down + ;; + cloud:restart) + docker compose -f "$COMPOSE_CLOUD_FILE" -p "$CLOUD_PROJECT" down + docker compose -f "$COMPOSE_CLOUD_FILE" -p "$CLOUD_PROJECT" up -d --build + echo "Pagepiper cloud running → http://localhost:${CLOUD_WEB_PORT}" + ;; + cloud:status) + docker compose -f "$COMPOSE_CLOUD_FILE" -p "$CLOUD_PROJECT" ps + ;; + cloud:logs) + docker compose -f "$COMPOSE_CLOUD_FILE" -p "$CLOUD_PROJECT" logs -f "${1:-}" + ;; + cloud:build) + docker compose -f "$COMPOSE_CLOUD_FILE" -p "$CLOUD_PROJECT" build --no-cache + ;; *) usage ;;