Skip to content

Commit 6c74aa9

Browse files
authored
Merge pull request #180 from andrewm4894/add-deploy-to-flyio
Add deploy to flyio
2 parents 868b77b + 2c90bd3 commit 6c74aa9

31 files changed

+2567
-143
lines changed

.example.env

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,15 @@ ANOMSTACK_SNOWFLAKE_WAREHOUSE=
1818
ANOMSTACK_AWS_ACCESS_KEY_ID=
1919
ANOMSTACK_AWS_SECRET_ACCESS_KEY=
2020

21+
# admin authentication (for Fly.io deployment and admin access)
22+
ANOMSTACK_ADMIN_USERNAME=admin
23+
ANOMSTACK_ADMIN_PASSWORD=anomstack2024
24+
2125
# local duckdb path for testing/dev quickstart
22-
# For local development (outside Docker)
26+
# For local development (outside containers)
2327
# ANOMSTACK_DUCKDB_PATH=tmpdata/anomstack-duckdb.db
24-
# For Docker containers (uses named volume)
25-
ANOMSTACK_DUCKDB_PATH=/metrics_db/duckdb/anomstack.db
28+
# For container deployments (Docker Compose, Fly.io, etc.)
29+
ANOMSTACK_DUCKDB_PATH=/data/anomstack.db
2630
# example using motherduck
2731
# https://motherduck.com/docs/getting-started/connect-query-from-python/choose-database/
2832
# ANOMSTACK_DUCKDB_PATH=md:anomstack
@@ -100,6 +104,10 @@ ANOMSTACK_LLM_PLATFORM=openai
100104
# some dagster env vars
101105
DAGSTER_LOG_LEVEL=DEBUG
102106
DAGSTER_CONCURRENCY=4
107+
# Code server host for workspace configuration
108+
# For Docker Compose: leave blank (defaults to "anomstack_code")
109+
# For Fly.io single container: set to "localhost"
110+
DAGSTER_CODE_SERVER_HOST=
103111

104112
# max runtime for a job in dagster
105113
# https://docs.dagster.io/deployment/run-monitoring#general-run-timeouts

DOCKER.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ make docker
4343
- **Restart Policy**: `always`
4444
- **Volumes**:
4545
- `./tmp:/opt/dagster/app/tmp` (temporary files)
46-
- `anomstack_metrics_duckdb:/metrics_db/duckdb` (DuckDB data)
46+
- `anomstack_metrics_duckdb:/data` (DuckDB data)
4747

4848
### 3. Dagster UI (`anomstack_dagit`)
4949
- **Image**: `andrewm4894/anomstack_dagster:latest`
@@ -70,7 +70,7 @@ make docker
7070

7171
### Named Volumes
7272
- **`anomstack_metrics_duckdb`**: Persistent storage for DuckDB metrics database
73-
- **Mount Point**: `/metrics_db/duckdb/`
73+
- **Mount Point**: `/data/`
7474
- **Purpose**: Stores time-series metrics data
7575
- **Persistence**: Data survives container restarts
7676

@@ -86,7 +86,7 @@ Create a `.env` file based on `.example.env`:
8686

8787
```bash
8888
# Database paths
89-
ANOMSTACK_DUCKDB_PATH=/metrics_db/duckdb/anomstack.db
89+
ANOMSTACK_DUCKDB_PATH=/data/anomstack.db
9090
ANOMSTACK_SQLITE_PATH=tmpdata/anomstack-sqlite.db
9191

9292
# PostgreSQL
@@ -191,7 +191,7 @@ make docker-restart-[service]
191191
## Data Persistence
192192

193193
### DuckDB Database
194-
- **Location**: `/metrics_db/duckdb/anomstack.db` (inside containers)
194+
- **Location**: `/data/anomstack.db` (inside containers)
195195
- **Volume**: `anomstack_metrics_duckdb`
196196
- **Backup**: Use `docker volume` commands to backup
197197

Makefile

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ docker-restart:
131131
# reload configuration without restarting containers (hot reload)
132132
reload-config:
133133
@echo "🔄 Reloading Anomstack configuration..."
134-
python3 scripts/reload_config.py
134+
python3 scripts/configuration/reload_config.py
135135

136136
# enable automatic config reloading via Dagster scheduled job
137137
enable-auto-reload:
@@ -159,6 +159,39 @@ docker-prune:
159159
docker compose down -v --remove-orphans
160160
docker system prune -a -f
161161

162+
# =============================================================================
163+
# FLY.IO DEPLOYMENT
164+
# =============================================================================
165+
166+
.PHONY: fly-validate fly-preview fly-deploy fly-status fly-logs fly-ssh
167+
168+
# validate fly.io configuration
169+
fly-validate:
170+
./scripts/deployment/validate_fly_config.sh
171+
172+
# preview what environment variables will be set as Fly secrets
173+
fly-preview:
174+
./scripts/deployment/preview_fly_secrets.sh
175+
176+
# deploy to fly.io (reads .env file automatically)
177+
fly-deploy:
178+
./scripts/deployment/deploy_fly.sh
179+
180+
# check fly.io app status (requires app name as FLY_APP env var)
181+
fly-status:
182+
@if [ -z "$$FLY_APP" ]; then echo "Set FLY_APP environment variable"; exit 1; fi
183+
fly status -a $$FLY_APP
184+
185+
# view fly.io app logs (requires app name as FLY_APP env var)
186+
fly-logs:
187+
@if [ -z "$$FLY_APP" ]; then echo "Set FLY_APP environment variable"; exit 1; fi
188+
fly logs -f -a $$FLY_APP
189+
190+
# ssh into fly.io app (requires app name as FLY_APP env var)
191+
fly-ssh:
192+
@if [ -z "$$FLY_APP" ]; then echo "Set FLY_APP environment variable"; exit 1; fi
193+
fly ssh console -a $$FLY_APP
194+
162195
# =============================================================================
163196
# RESET OPERATIONS
164197
# =============================================================================
@@ -307,11 +340,11 @@ requirements-install:
307340

308341
# run the PostHog example ingest function
309342
posthog-example:
310-
python scripts/posthog_example.py
343+
python scripts/examples/posthog_example.py
311344

312345
# kill any dagster runs exceeding configured timeout
313346
kill-long-runs:
314-
python scripts/kill_long_running_tasks.py
347+
python scripts/maintenance/kill_long_running_tasks.py
315348

316349
# run docker in dev mode with correct environment
317350
docker-dev-env:

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Anomaly list view:
4646
- [Quickstart](#quickstart)
4747
- [GitHub Codespaces](#github-codespaces)
4848
- [Dagster Cloud](#dagster-cloud)
49+
- [Fly.io Deployment](#flyio-deployment)
4950
- [Docker](#docker)
5051
- [Replit](#replit)
5152
- [Local Python env](#local-python-env)
@@ -132,6 +133,7 @@ Supported ways to run this project:
132133
<tr>
133134
<th align="center"><a href="#local-python-env">Python Env</a></th>
134135
<th align="center"><a href="#docker">Docker</a></th>
136+
<th align="center"><a href="#fly-io-deployment">Fly.io</a></th>
135137
<th align="center"><a href="#dagster-cloud">Dagster Cloud</a></th>
136138
<th align="center"><a href="#github-codespaces">GitHub Codespaces</a></th>
137139
<th align="center"><a href="#replit">Replit</a></th>
@@ -141,6 +143,7 @@ Supported ways to run this project:
141143
<tr>
142144
<td align="center">✅</td>
143145
<td align="center">✅</td>
146+
<td align="center">🆕</td>
144147
<td align="center">✅</td>
145148
<td align="center">✅</td>
146149
<td align="center">✅</td>
@@ -451,6 +454,32 @@ You can run this project in Dagster Cloud. Fork the repo (or make a completely n
451454

452455
You can then manage you metrics via PR's in your GitHub repo ([here](https://github.com/andrewm4894/anomstack/pull/40/files) is a PR to add Google Trends metrics) and run them in Dagster Cloud which will just sync with your repo.
453456

457+
### Fly.io Deployment
458+
459+
Deploy to [Fly.io](https://fly.io) for production-ready, globally distributed anomaly detection.
460+
461+
**🚀 Live Demo**: https://anomstack-demo.fly.dev
462+
463+
```bash
464+
# New! Automatic .env integration 🎉
465+
cp .example.env .env # Edit with your secrets
466+
make fly-preview # Preview what will be deployed
467+
make fly-deploy # Deploy with your environment variables
468+
469+
# Or using scripts directly
470+
./scripts/deployment/preview_fly_secrets.sh
471+
./scripts/deployment/deploy_fly.sh
472+
```
473+
474+
**✨ Features:**
475+
- 🔒 **Secure secret management** via Fly secrets
476+
- 🧠 **Smart filtering** (skips local-only variables)
477+
- 🔐 **Configurable admin credentials**
478+
- 📊 **Public dashboard** + protected admin interface
479+
- 🌐 **Global edge deployment** with auto-scaling
480+
481+
See [full deployment docs](docs/docs/deployment/fly.md) for detailed instructions.
482+
454483
### Docker
455484

456485
To get started with Anomstack, you can run it via docker compose.

dagster_docker.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ run_launcher:
3737
- ${ANOMSTACK_HOME}/tmp:/opt/dagster/app/tmp
3838
- ${ANOMSTACK_HOME}/dagster_home:/opt/dagster/dagster_home
3939
- ${ANOMSTACK_HOME}/metrics:/opt/dagster/app/metrics
40-
- anomstack_metrics_duckdb:/metrics_db/duckdb
40+
- anomstack_metrics_duckdb:/data
4141

4242
run_storage:
4343
module: dagster_postgres.run_storage

dagster_fly.yaml

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
scheduler:
2+
module: dagster.core.scheduler
3+
class: DagsterDaemonScheduler
4+
5+
run_coordinator:
6+
module: dagster.core.run_coordinator
7+
class: QueuedRunCoordinator
8+
config:
9+
max_concurrent_runs: 12 # Increased for 8GB RAM + 4 performance CPUs (prevents queue backup)
10+
tag_concurrency_limits:
11+
- key: "dagster/concurrency_key"
12+
value: "database"
13+
limit: 2
14+
- key: "dagster/concurrency_key"
15+
value: "ml_training"
16+
limit: 1
17+
18+
# Use DefaultRunLauncher instead of DockerRunLauncher for Fly.io
19+
run_launcher:
20+
module: dagster.core.launcher.default_run_launcher
21+
class: DefaultRunLauncher
22+
23+
run_storage:
24+
module: dagster_postgres.run_storage
25+
class: PostgresRunStorage
26+
config:
27+
postgres_url:
28+
env: DATABASE_URL
29+
30+
run_retries:
31+
enabled: true
32+
max_retries: 1
33+
34+
# Aggressive retention policies optimized for Fly.io disk usage
35+
retention:
36+
schedule:
37+
purge_after_days: 2 # Keep for 2 days
38+
sensor:
39+
purge_after_days:
40+
skipped: 1
41+
failure: 2
42+
success: 1
43+
44+
# Run monitoring for Fly.io environment
45+
run_monitoring:
46+
enabled: true
47+
start_timeout_seconds: 180 # 3 minutes to start
48+
cancel_timeout_seconds: 120 # 2 minutes to cancel
49+
max_runtime_seconds: 2700 # 45 minutes max runtime per run (increased with better resources)
50+
poll_interval_seconds: 60 # Check every minute
51+
52+
# Disable telemetry
53+
telemetry:
54+
enabled: false
55+
56+
schedules:
57+
use_threads: true
58+
num_workers: 4 # Conservative for Fly.io
59+
60+
sensors:
61+
use_threads: true
62+
num_workers: 2 # Conservative for Fly.io
63+
64+
schedule_storage:
65+
module: dagster_postgres.schedule_storage
66+
class: PostgresScheduleStorage
67+
config:
68+
postgres_url:
69+
env: DATABASE_URL
70+
71+
event_log_storage:
72+
module: dagster_postgres.event_log
73+
class: PostgresEventLogStorage
74+
config:
75+
postgres_url:
76+
env: DATABASE_URL
77+
78+
compute_logs:
79+
module: dagster.core.storage.local_compute_log_manager
80+
class: LocalComputeLogManager
81+
config:
82+
base_dir: "/tmp/dagster/compute_logs"

dagster_home/workspace.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
load_from:
22
# Each entry here corresponds to a service in the docker-compose file that exposes user code.
3+
# For Fly.io deployment, DAGSTER_CODE_SERVER_HOST=localhost
4+
# For Docker Compose, DAGSTER_CODE_SERVER_HOST=anomstack_code (default)
35
- grpc_server:
4-
host: anomstack_code
6+
host:
7+
env: DAGSTER_CODE_SERVER_HOST
58
port: 4000
69
location_name: "anomstack_code"

dashboard/state.py

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@ class AppState:
2121
State manager for the dashboard.
2222
"""
2323

24-
def __init__(self):
25-
self._connection = None
26-
2724
def get_connection(self):
2825
"""Get database connection with MotherDuck fallback"""
2926
import os
@@ -79,34 +76,84 @@ def __init__(self):
7976
# Lazy initialization flags
8077
self._specs_loaded = False
8178
self._metric_batches_loaded = False
79+
self._specs = None
80+
self._metric_batches = None
81+
self._specs_enabled = None
8282

8383
print("AppState initialized with lazy loading")
84+
85+
@property
86+
def specs(self):
87+
"""Lazy-loaded specs property"""
88+
if not self._specs_loaded:
89+
self._ensure_specs_loaded()
90+
return self._specs
91+
92+
@specs.setter
93+
def specs(self, value):
94+
"""Setter for specs"""
95+
self._specs = value
96+
self._specs_loaded = True
97+
98+
@property
99+
def metric_batches(self):
100+
"""Lazy-loaded metric batches property"""
101+
if not self._metric_batches_loaded:
102+
self._ensure_metric_batches_loaded()
103+
return self._metric_batches
104+
105+
@metric_batches.setter
106+
def metric_batches(self, value):
107+
"""Setter for metric_batches"""
108+
self._metric_batches = value
109+
110+
@property
111+
def specs_enabled(self):
112+
"""Lazy-loaded specs_enabled property"""
113+
if not self._metric_batches_loaded:
114+
self._ensure_metric_batches_loaded()
115+
return self._specs_enabled
116+
117+
@specs_enabled.setter
118+
def specs_enabled(self, value):
119+
"""Setter for specs_enabled"""
120+
self._specs_enabled = value
84121

85122
def _ensure_specs_loaded(self):
86123
"""Lazy load specs and metric batches"""
87124
if not self._specs_loaded:
88125
try:
89-
self.specs = get_specs()
126+
self._specs = get_specs()
90127
self._specs_loaded = True
91128
print("Specs loaded successfully")
92129
except Exception as e:
93130
log.error(f"Error loading specs: {e}")
94-
self.specs = {}
131+
self._specs = {}
95132

96133
def _ensure_metric_batches_loaded(self):
97134
"""Lazy load metric batches"""
98135
if not self._metric_batches_loaded:
99136
try:
100-
self.metric_batches = get_metric_batches(source="all")
101-
if not self.metric_batches:
137+
# Ensure specs are loaded first
138+
if not self._specs_loaded:
139+
self._ensure_specs_loaded()
140+
141+
self._metric_batches = get_metric_batches(source="all")
142+
if not self._metric_batches:
102143
log.warning("No metric batches found.")
103-
self.specs_enabled = {batch: self.specs[batch] for batch in self.metric_batches}
144+
self._metric_batches = []
145+
146+
if self._specs and self._metric_batches:
147+
self._specs_enabled = {batch: self._specs[batch] for batch in self._metric_batches if batch in self._specs}
148+
else:
149+
self._specs_enabled = {}
150+
104151
self._metric_batches_loaded = True
105152
print("Metric batches loaded successfully")
106153
except Exception as e:
107154
log.error(f"Error loading metric batches: {e}")
108-
self.metric_batches = []
109-
self.specs_enabled = {}
155+
self._metric_batches = []
156+
self._specs_enabled = {}
110157

111158
def clear_batch_cache(self, batch_name):
112159
"""

0 commit comments

Comments
 (0)