Add idle auto-shutdown configuration to deployment
- IDLE_TIMEOUT_MINUTES env var (default 30 min) - restart: no policy so container stays stopped - Optional wakeup service for auto-restart - Document three restart options in readme
This commit is contained in:
8
deploy/.env.example
Normal file
8
deploy/.env.example
Normal file
@@ -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
|
||||
64
deploy/docker-compose.yml
Normal file
64
deploy/docker-compose.yml
Normal file
@@ -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
|
||||
45
deploy/nginx.conf
Normal file
45
deploy/nginx.conf
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
184
deploy/readme.md
Normal file
184
deploy/readme.md
Normal file
@@ -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
|
||||
```
|
||||
Reference in New Issue
Block a user