Deploying the Dashboard to Cloud Run¶
The ScamShield AI dashboard is a Streamlit application deployed on Google Cloud Run. It provides session monitoring, evidence browsing, analytics, and interactive testing.
Stack Overview¶
| Component | Technology |
|---|---|
| Framework | Streamlit |
| Runtime | Cloud Run (managed) |
| Container | Python 3.11-slim Docker image |
| Auth | 6-digit PIN with HMAC-signed cookie + Firestore lockout |
| Proxy | Cloudflare Worker (optional, for custom domain) |
Prerequisites¶
- Google Cloud CLI authenticated to your project
- Docker (for local testing only; Cloud Run builds from source)
- Secrets stored in GCP Secret Manager (see step 2)
1. Project Structure¶
dashboard/
app.py # Home page and navigation hub
Dockerfile # Container definition
entrypoint.sh # Startup script: env vars -> secrets.toml
requirements.txt # Python dependencies
.streamlit/
config.toml # Streamlit configuration (committed)
secrets.toml # Local dev secrets (gitignored)
pages/
00_testing.py # Interactive chat testing
01_live_sessions.py # Real-time session feed
02_evidence.py # Evidence search and filtering
03_analytics.py # Session funnel and charts
04_intelligence.py # Network graph visualization
...
utils/
auth.py # PIN auth with HMAC cookies
display.py # Shared rendering functions
firestore_client.py # All Firestore read operations
2. Store Secrets in Secret Manager¶
The dashboard requires two secrets:
# Dashboard login PIN (6 digits)
echo -n "123456" | gcloud secrets create DASHBOARD_PIN \
--data-file=- \
--project=your-gcp-project-id
# API key for calling the honeypot endpoint from the Testing page
echo -n "your-api-key-here" | gcloud secrets create SCAMSHIELD_API_KEY \
--data-file=- \
--project=your-gcp-project-id
If secrets already exist, create a new version:
3. How entrypoint.sh Works¶
Cloud Run injects secrets as environment variables. The entrypoint.sh script converts them to Streamlit's secrets.toml format at container startup:
#!/bin/sh
mkdir -p /app/.streamlit
cat > /app/.streamlit/secrets.toml <<EOF
DASHBOARD_PIN = "${DASHBOARD_PIN}"
[api]
SCAMSHIELD_API_KEY = "${SCAMSHIELD_API_KEY}"
EOF
exec streamlit run app.py --server.port=8080 --server.address=0.0.0.0 \
--server.fileWatcherType=none --client.showErrorDetails=false \
--browser.gatherUsageStats=false
This approach avoids baking secrets into the Docker image.
4. Deploy to Cloud Run¶
gcloud run deploy scamshield-dashboard \
--source dashboard/ \
--project your-gcp-project-id \
--region asia-south1 \
--allow-unauthenticated \
--service-account firebase-deploy@your-gcp-project-id.iam.gserviceaccount.com \
--memory 512Mi \
--cpu 1 \
--min-instances 0 \
--max-instances 2 \
--set-secrets="DASHBOARD_PIN=DASHBOARD_PIN:latest,SCAMSHIELD_API_KEY=SCAMSHIELD_API_KEY:latest"
Flag breakdown:
| Flag | Purpose |
|---|---|
--source dashboard/ |
Build from the dashboard/ directory (Cloud Build handles Docker) |
--allow-unauthenticated |
Public access (PIN auth is handled in-app) |
--memory 512Mi |
Sufficient for Streamlit + Firestore reads |
--min-instances 0 |
Scale to zero when idle (cost savings) |
--max-instances 2 |
Cap scaling (dashboard is low-traffic) |
--set-secrets |
Map Secret Manager secrets to environment variables |
5. Health Checks and Scaling¶
Cloud Run automatically health-checks the container on port 8080. The Streamlit server responds to HTTP requests on / once started.
Scaling behavior:
- Min instances: 0 -- The dashboard scales to zero when no one is using it. First request after idle incurs a cold start (5-10 seconds for Streamlit).
- Max instances: 2 -- Caps cost. The dashboard is typically used by 1-2 operators.
- Concurrency: default (80) -- Streamlit is single-threaded per session, but Cloud Run handles multiple connections to the same instance.
6. PIN Auth Setup¶
The dashboard uses a 6-digit PIN stored in DASHBOARD_PIN:
- User enters PIN on the login screen
- PIN is verified against
st.secrets["DASHBOARD_PIN"] - On success, an HMAC-signed cookie (
scamshield_auth) is set with a 1-hour TTL - Subsequent requests validate the cookie without re-entering the PIN
- After 5 failed attempts, Firestore-backed lockout blocks login for 15 minutes
Auth flow in code (utils/auth.py):
Request arrives
→ Check st.session_state (already authed this session?)
→ Check cookie (HMAC-signed, 1-hour TTL)
→ Show PIN login screen
→ On failure: increment lockout counter
→ On success: set cookie + session_state
Every dashboard page calls require_auth() immediately after st.set_page_config():
st.set_page_config(page_title="Evidence", page_icon="...", layout="wide")
require_auth() # Must be called here -- no exceptions
7. Custom Domain Setup (Optional)¶
To serve the dashboard on a custom domain, you have two options:
Option A: Cloud Run Domain Mapping¶
gcloud run domain-mappings create \
--service scamshield-dashboard \
--domain dashboard.example.com \
--region asia-south1
Follow the DNS verification instructions printed by the command.
Option B: Cloudflare Worker Proxy¶
See Cloudflare Proxy Setup for the recommended approach using a Cloudflare Worker. This provides SSL, caching, and DDoS protection.
8. Local Development¶
cd dashboard/
# Create local secrets file
cat > .streamlit/secrets.toml <<EOF
DASHBOARD_PIN = "123456"
[api]
SCAMSHIELD_API_KEY = "your-local-api-key"
EOF
# Install dependencies
pip install -r requirements.txt
# Run
streamlit run app.py
The dashboard reads Firestore using Application Default Credentials. Make sure you are authenticated:
Troubleshooting¶
Dashboard Shows Blank Page¶
If the dashboard loads but renders nothing:
- Check Cloudflare Rocket Loader -- Must be OFF (see Cloudflare Proxy)
- Check browser console for JavaScript errors
- Verify the container started correctly:
Secrets Not Available¶
If st.secrets["DASHBOARD_PIN"] throws KeyError:
- Verify
entrypoint.shruns before Streamlit (check theCMDin Dockerfile) - Verify secrets are mapped in Cloud Run:
- Verify secrets exist in Secret Manager:
Cookie Auth Not Persisting¶
The HMAC cookie key is derived from SHA256(salt + PIN). Changing the PIN invalidates all existing cookie sessions. This is by design.
If cookies are not persisting across page loads, check that:
- The
extra-streamlit-componentspackage is installed - The browser allows cookies from the dashboard domain
- No proxy is stripping
Set-Cookieheaders