diff --git a/deploy/.env.example b/deploy/.env.example new file mode 100644 index 0000000..f6e5b4f --- /dev/null +++ b/deploy/.env.example @@ -0,0 +1,8 @@ +# Cloudflare Tunnel token +# Get this from: Cloudflare Zero Trust > Networks > Tunnels > Create > Docker +CLOUDFLARE_TUNNEL_TOKEN=your-tunnel-token-here + +# Idle auto-shutdown (minutes) +# App exits after this many minutes with no active viewers +# Set to 0 to disable (app runs forever) +IDLE_TIMEOUT_MINUTES=30 diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000..9b86557 --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,64 @@ +services: + streamlit: + build: + context: .. + dockerfile: Dockerfile + container_name: py-dvt-ate-streamlit + restart: "no" # Don't auto-restart - we want it to stay stopped when idle + environment: + - IDLE_TIMEOUT_MINUTES=${IDLE_TIMEOUT_MINUTES:-30} + expose: + - "8080" + volumes: + - ./data:/app/data + networks: + - dvt-ate + + nginx: + image: nginx:alpine + container_name: py-dvt-ate-nginx + restart: unless-stopped + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + expose: + - "80" + depends_on: + - streamlit + networks: + - dvt-ate + + cloudflared: + image: cloudflare/cloudflared:latest + container_name: py-dvt-ate-tunnel + restart: unless-stopped + command: tunnel run + environment: + - TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN} + depends_on: + - nginx + networks: + - dvt-ate + + # Optional: Auto-start streamlit when stopped (checks every 30s) + # Uncomment if you want fully automatic restart on visit + # wakeup: + # image: docker:cli + # container_name: py-dvt-ate-wakeup + # restart: unless-stopped + # entrypoint: /bin/sh + # command: + # - -c + # - | + # while true; do + # sleep 30 + # if ! docker inspect -f '{{.State.Running}}' py-dvt-ate-streamlit 2>/dev/null | grep -q true; then + # echo "[$(date)] Starting streamlit..." + # docker start py-dvt-ate-streamlit + # fi + # done + # volumes: + # - /var/run/docker.sock:/var/run/docker.sock:ro + +networks: + dvt-ate: + driver: bridge diff --git a/deploy/nginx.conf b/deploy/nginx.conf new file mode 100644 index 0000000..96336f4 --- /dev/null +++ b/deploy/nginx.conf @@ -0,0 +1,45 @@ +events { + worker_connections 1024; +} + +http { + upstream streamlit { + server streamlit:8080; + } + + server { + listen 80; + server_name _; + + # Remove X-Frame-Options to allow iframe embedding + proxy_hide_header X-Frame-Options; + + # Allow embedding from your domain + add_header Content-Security-Policy "frame-ancestors 'self' https://kschappell.com https://*.kschappell.com"; + + location / { + proxy_pass http://streamlit; + proxy_http_version 1.1; + + # WebSocket support (required for Streamlit) + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # Standard proxy headers + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Timeouts for long-running connections + proxy_read_timeout 86400; + proxy_send_timeout 86400; + } + + # Health check endpoint + location /health { + return 200 'ok'; + add_header Content-Type text/plain; + } + } +} diff --git a/deploy/readme.md b/deploy/readme.md new file mode 100644 index 0000000..33b186e --- /dev/null +++ b/deploy/readme.md @@ -0,0 +1,184 @@ +# py-dvt-ate Deployment + +Deploy the DVT Simulation Platform dashboard with Cloudflare Tunnel for public access. + +## Idle Auto-Shutdown + +The app automatically shuts down after a period of inactivity to save resources. + +**Configuration:** +```bash +# In .env or docker-compose.yml +IDLE_TIMEOUT_MINUTES=30 # Shutdown after 30 min idle (0 = disabled) +``` + +**Behaviour:** +- App tracks activity when someone has the dashboard open +- After `IDLE_TIMEOUT_MINUTES` with no viewers, the container exits +- nginx will show a 502 error until the container is restarted + +**Restart Options:** + +1. **Manual restart** (simplest): + ```bash + docker start py-dvt-ate-streamlit + ``` + +2. **Auto-restart on poll** (uncomment `wakeup` service in docker-compose.yml): + - Checks every 30 seconds if streamlit is stopped + - Automatically restarts it + - Adds ~30 second delay before app is available + +3. **Always running** (set `IDLE_TIMEOUT_MINUTES=0`): + - App never auto-shuts down + - Uses minimal CPU when idle (~0.1%) + - Memory stays allocated (~300-400MB) + +## Architecture + +``` +Internet + ↓ +Cloudflare Edge (dvt-demo.kschappell.com) + ↓ (tunnel) +cloudflared container + ↓ +nginx container (WebSocket proxy + header handling) + ↓ +streamlit container (port 8080) +``` + +## Directory Structure + +``` +/mnt/fast-pool/apps/portfolio-demos/py-dvt-ate/ +├── docker-compose.yml +├── nginx.conf +├── .env +└── data/ # Persistent storage (created automatically) + ├── py_dvt_ate.db # SQLite database + ├── measurements/ # Test measurement files + └── reports/ # Generated PDFs +``` + +## Setup + +### 1. Create Cloudflare Tunnel + +1. Go to [Cloudflare Zero Trust Dashboard](https://one.dash.cloudflare.com/) +2. Navigate to **Networks** → **Tunnels** +3. Click **Create a tunnel** +4. Select **Cloudflared** as the connector +5. Name it `py-dvt-ate` (or similar) +6. Copy the tunnel token (long string starting with `eyJ...`) + +### 2. Configure Public Hostname + +Still in the tunnel configuration: + +1. Go to the **Public Hostname** tab +2. Add a public hostname: + - **Subdomain:** `dvt-demo` + - **Domain:** `kschappell.com` + - **Service Type:** `HTTP` + - **URL:** `nginx:80` + +This routes `dvt-demo.kschappell.com` → nginx container → Streamlit app. + +### 3. Deploy + +The build requires access to the full py-dvt-ate source code. Two options: + +**Option A: Clone repo to TrueNAS (recommended)** + +```bash +# Clone repo to apps directory +cd /mnt/fast-pool/apps/portfolio-demos +git clone https://gitea.kschappell.com/kschappell/py-dvt-ate.git + +# Create data directory and .env +cd py-dvt-ate/deploy +mkdir -p data +cp .env.example .env +nano .env # Add your CLOUDFLARE_TUNNEL_TOKEN + +# Build and start +docker compose up -d --build +``` + +**Option B: Build image locally, transfer to TrueNAS** + +```bash +# On development machine +cd /path/to/py-dvt-ate +docker build -t py-dvt-ate:latest . +docker save py-dvt-ate:latest | gzip > py-dvt-ate.tar.gz + +# Transfer to TrueNAS, then: +docker load < py-dvt-ate.tar.gz + +# Update docker-compose.yml to use image instead of build: +# streamlit: +# image: py-dvt-ate:latest +``` + +**Check logs:** + +```bash +docker compose logs -f +``` + +### 4. Verify + +```bash +# Check tunnel is connected (in Cloudflare dashboard, tunnel should show "Healthy") + +# Test the endpoint +curl https://dvt-demo.kschappell.com/health + +# Test iframe headers +curl -I https://dvt-demo.kschappell.com | grep -i frame +# Should NOT show X-Frame-Options (we strip it) +``` + +## Troubleshooting + +### Tunnel not connecting + +Check cloudflared logs: +```bash +docker compose logs cloudflared +``` + +Common issues: +- Invalid token (regenerate in Cloudflare dashboard) +- Network/firewall blocking outbound connections + +### Streamlit not loading in iframe + +Check nginx is stripping headers: +```bash +curl -I https://dvt-demo.kschappell.com +``` + +Should see: +- No `X-Frame-Options` header +- `Content-Security-Policy: frame-ancestors 'self' https://kschappell.com ...` + +### WebSocket errors + +Check browser console for WebSocket connection failures. Ensure nginx WebSocket config is correct and timeouts are sufficient. + +## Updating + +```bash +cd /mnt/fast-pool/apps/portfolio-demos/py-dvt-ate/deploy +git pull +docker compose up -d --build +``` + +## Stopping + +```bash +docker compose down +```