feat(deploy): add cloud deploy config for pagepiper.circuitforge.tech

- compose.cloud.yml: pagepiper-cloud project on port 8533 (avoids
  conflict with Linnet dev on 8521/Magpie on 8531)
- docker/web/nginx.cloud.conf: handles both /pagepiper/* path (primary
  domain, no Caddy strip) and / path (menagerie, Caddy strips prefix)
- docker/web/Dockerfile: NGINX_CONF build arg to select dev vs cloud conf
- .env.cloud.example: cloud env template with BYOK gate vars
- manage.sh: cloud:start|stop|restart|status|logs|build commands

Caddy config updated separately (not in this repo).
DNS record needed: pagepiper.circuitforge.tech → Heimdall edge IP.
This commit is contained in:
pyr0ball 2026-05-05 07:12:48 -07:00
parent 6fc8e7faa6
commit c24bd33478
5 changed files with 149 additions and 2 deletions

18
.env.cloud.example Normal file
View file

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

44
compose.cloud.yml Normal file
View file

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

View file

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

View file

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

View file

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