Skip to content

Commit f09e89d

Browse files
Merge pull request #9 from ipnet-mesh/claude/add-data-home-config-01WnK1Q9vczq5atX7U5P3yJz
Add configurable data directory for services
2 parents a4a3a72 + 6bb134e commit f09e89d

10 files changed

Lines changed: 363 additions & 76 deletions

File tree

.env.example

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,23 @@
1111
# MESHCORE_IMAGE=ghcr.io/ipnet-mesh/meshcore-hub:v1.0.0
1212
MESHCORE_IMAGE=
1313

14+
# ===================
15+
# Data Directory
16+
# ===================
17+
18+
# Base directory for all service data (collector DB, tags, members, etc.)
19+
# Default: ./data (relative to docker-compose.yml location)
20+
# Inside containers this is mapped to /data
21+
#
22+
# Structure:
23+
# ${DATA_HOME}/
24+
# ├── collector/
25+
# │ ├── meshcore.db # SQLite database
26+
# │ └── tags.json # Node tags for import
27+
# └── web/
28+
# └── members.json # Network members list
29+
DATA_HOME=./data
30+
1431
# ===================
1532
# Common Settings
1633
# ===================
@@ -72,10 +89,10 @@ NETWORK_RADIO_CONFIG=
7289
NETWORK_CONTACT_EMAIL=
7390
NETWORK_CONTACT_DISCORD=
7491

75-
# Path to members JSON file (mounted into container)
76-
# For production: use ./data/members.json
77-
# For testing with example data: use ./example/data/members.json
78-
MEMBERS_FILE_PATH=./example/data/members.json
92+
# Members file location (optional override)
93+
# Default: ${DATA_HOME}/web/members.json
94+
# Only set this if you want to use a different location
95+
# MEMBERS_FILE=/custom/path/to/members.json
7996

8097
# ===================
8198
# Webhook Settings

AGENTS.md

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -283,9 +283,12 @@ meshcore-hub/
283283
│ │ └── tags.json # Example node tags data
284284
│ └── web/
285285
│ └── members.json # Example network members data
286-
├── data/ # Runtime data (gitignored)
287-
│ ├── collector/ # Collector data (tags.json)
288-
│ └── web/ # Web data (members.json)
286+
├── data/ # Runtime data (gitignored, DATA_HOME default)
287+
│ ├── collector/ # Collector data
288+
│ │ ├── meshcore.db # SQLite database
289+
│ │ └── tags.json # Node tags for import
290+
│ └── web/ # Web data
291+
│ └── members.json # Network members list
289292
├── Dockerfile # Docker build configuration
290293
├── docker-compose.yml # Docker Compose services (gitignored)
291294
└── docker-compose.yml.example # Docker Compose template
@@ -433,11 +436,26 @@ meshcore-hub interface --mode receiver --mock
433436
See [PLAN.md](PLAN.md#configuration-environment-variables) for complete list.
434437

435438
Key variables:
439+
- `DATA_HOME` - Base directory for all service data (default: `./data`)
436440
- `MQTT_HOST`, `MQTT_PORT`, `MQTT_PREFIX` - MQTT broker connection
437-
- `DATABASE_URL` - SQLAlchemy database URL
441+
- `DATABASE_URL` - SQLAlchemy database URL (default: `sqlite:///{DATA_HOME}/collector/meshcore.db`)
438442
- `API_READ_KEY`, `API_ADMIN_KEY` - API authentication keys
439443
- `LOG_LEVEL` - Logging verbosity
440444

445+
### Data Directory Structure
446+
447+
The `DATA_HOME` environment variable controls where all service data is stored:
448+
```
449+
${DATA_HOME}/
450+
├── collector/
451+
│ ├── meshcore.db # SQLite database
452+
│ └── tags.json # Node tags for import
453+
└── web/
454+
└── members.json # Network members list
455+
```
456+
457+
Services automatically create their subdirectories if they don't exist.
458+
441459
### Webhook Configuration
442460

443461
The collector supports forwarding events to external HTTP endpoints:

PLAN.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -456,13 +456,25 @@ meshcore/+/command/request_telemetry
456456
### Common
457457
| Variable | Default | Description |
458458
|----------|---------|-------------|
459+
| DATA_HOME | ./data | Base directory for service data |
459460
| LOG_LEVEL | INFO | Logging level |
460461
| MQTT_HOST | localhost | MQTT broker host |
461462
| MQTT_PORT | 1883 | MQTT broker port |
462463
| MQTT_USERNAME | | MQTT username (optional) |
463464
| MQTT_PASSWORD | | MQTT password (optional) |
464465
| MQTT_PREFIX | meshcore | Topic prefix |
465466

467+
### Data Directory Structure
468+
The `DATA_HOME` environment variable controls where all service data is stored:
469+
```
470+
${DATA_HOME}/
471+
├── collector/
472+
│ ├── meshcore.db # SQLite database
473+
│ └── tags.json # Node tags for import
474+
└── web/
475+
└── members.json # Network members list
476+
```
477+
466478
### Interface
467479
| Variable | Default | Description |
468480
|----------|---------|-------------|
@@ -474,7 +486,8 @@ meshcore/+/command/request_telemetry
474486
### Collector
475487
| Variable | Default | Description |
476488
|----------|---------|-------------|
477-
| DATABASE_URL | sqlite:///./meshcore.db | SQLAlchemy URL |
489+
| DATABASE_URL | sqlite:///{DATA_HOME}/collector/meshcore.db | SQLAlchemy URL |
490+
| TAGS_FILE | {DATA_HOME}/collector/tags.json | Path to tags JSON file |
478491

479492
### API
480493
| Variable | Default | Description |
@@ -483,7 +496,7 @@ meshcore/+/command/request_telemetry
483496
| API_PORT | 8000 | API bind port |
484497
| API_READ_KEY | | Read-only API key |
485498
| API_ADMIN_KEY | | Admin API key |
486-
| DATABASE_URL | sqlite:///./meshcore.db | SQLAlchemy URL |
499+
| DATABASE_URL | sqlite:///{DATA_HOME}/collector/meshcore.db | SQLAlchemy URL |
487500

488501
### Web Dashboard
489502
| Variable | Default | Description |
@@ -500,7 +513,7 @@ meshcore/+/command/request_telemetry
500513
| NETWORK_RADIO_CONFIG | | Radio config details |
501514
| NETWORK_CONTACT_EMAIL | | Contact email |
502515
| NETWORK_CONTACT_DISCORD | | Discord link |
503-
| MEMBERS_FILE | members.json | Path to members JSON |
516+
| MEMBERS_FILE | {DATA_HOME}/web/members.json | Path to members JSON |
504517

505518
---
506519

alembic/env.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,22 @@
2121

2222
def get_database_url() -> str:
2323
"""Get database URL from environment or config."""
24-
# First try environment variable
24+
from pathlib import Path
25+
26+
# First try explicit DATABASE_URL environment variable
2527
url = os.environ.get("DATABASE_URL")
2628
if url:
29+
# Ensure directory exists for sqlite URLs
30+
if url.startswith("sqlite:///"):
31+
db_path = Path(url.replace("sqlite:///", ""))
32+
db_path.parent.mkdir(parents=True, exist_ok=True)
2733
return url
34+
# Try DATA_HOME environment variable
35+
data_home = os.environ.get("DATA_HOME")
36+
if data_home:
37+
db_path = Path(data_home) / "collector" / "meshcore.db"
38+
db_path.parent.mkdir(parents=True, exist_ok=True)
39+
return f"sqlite:///{db_path}"
2840
# Fall back to alembic.ini
2941
return config.get_main_option("sqlalchemy.url", "sqlite:///./meshcore.db")
3042

docker-compose.yml.example

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,14 @@
1717
# - migrate: Run database migrations
1818
# - import-tags: Import node tags from JSON file
1919
#
20-
# Data Directory Structure:
21-
# ./data/
20+
# Data Directory (DATA_HOME):
21+
# The DATA_HOME environment variable controls where all service data is stored.
22+
# Default: ./data (local) or /data (in containers)
23+
#
24+
# Structure:
25+
# ${DATA_HOME}/
2226
# ├── collector/
27+
# │ ├── meshcore.db # SQLite database
2328
# │ └── tags.json # Node tags for import
2429
# └── web/
2530
# └── members.json # Network members list
@@ -175,17 +180,16 @@ services:
175180
mqtt:
176181
condition: service_healthy
177182
volumes:
178-
- meshcore_data:/data
179-
# Mount collector data directory for tags.json (optional)
180-
- ${COLLECTOR_DATA_PATH:-./data/collector}:/app/collector-data:ro
183+
# Mount data directory (contains collector/meshcore.db and collector/tags.json)
184+
- ${DATA_HOME:-./data}:/data
181185
environment:
182186
- LOG_LEVEL=${LOG_LEVEL:-INFO}
183187
- MQTT_HOST=${MQTT_HOST:-mqtt}
184188
- MQTT_PORT=${MQTT_PORT:-1883}
185189
- MQTT_USERNAME=${MQTT_USERNAME:-}
186190
- MQTT_PASSWORD=${MQTT_PASSWORD:-}
187191
- MQTT_PREFIX=${MQTT_PREFIX:-meshcore}
188-
- DATABASE_URL=sqlite:////data/meshcore.db
192+
- DATA_HOME=/data
189193
# Webhook configuration
190194
- WEBHOOK_ADVERTISEMENT_URL=${WEBHOOK_ADVERTISEMENT_URL:-}
191195
- WEBHOOK_ADVERTISEMENT_SECRET=${WEBHOOK_ADVERTISEMENT_SECRET:-}
@@ -227,15 +231,16 @@ services:
227231
ports:
228232
- "${API_PORT:-8000}:8000"
229233
volumes:
230-
- meshcore_data:/data
234+
# Mount data directory (uses collector/meshcore.db)
235+
- ${DATA_HOME:-./data}:/data
231236
environment:
232237
- LOG_LEVEL=${LOG_LEVEL:-INFO}
233238
- MQTT_HOST=${MQTT_HOST:-mqtt}
234239
- MQTT_PORT=${MQTT_PORT:-1883}
235240
- MQTT_USERNAME=${MQTT_USERNAME:-}
236241
- MQTT_PASSWORD=${MQTT_PASSWORD:-}
237242
- MQTT_PREFIX=${MQTT_PREFIX:-meshcore}
238-
- DATABASE_URL=sqlite:////data/meshcore.db
243+
- DATA_HOME=/data
239244
- API_HOST=0.0.0.0
240245
- API_PORT=8000
241246
- API_READ_KEY=${API_READ_KEY:-}
@@ -266,23 +271,23 @@ services:
266271
condition: service_healthy
267272
ports:
268273
- "${WEB_PORT:-8080}:8080"
274+
volumes:
275+
# Mount data directory (uses web/members.json)
276+
- ${DATA_HOME:-./data}:/data
269277
environment:
270278
- LOG_LEVEL=${LOG_LEVEL:-INFO}
271279
- API_BASE_URL=http://api:8000
272280
- API_KEY=${API_READ_KEY:-}
273281
- WEB_HOST=0.0.0.0
274282
- WEB_PORT=8080
283+
- DATA_HOME=/data
275284
- NETWORK_NAME=${NETWORK_NAME:-MeshCore Network}
276285
- NETWORK_CITY=${NETWORK_CITY:-}
277286
- NETWORK_COUNTRY=${NETWORK_COUNTRY:-}
278287
- NETWORK_LOCATION=${NETWORK_LOCATION:-}
279288
- NETWORK_RADIO_CONFIG=${NETWORK_RADIO_CONFIG:-}
280289
- NETWORK_CONTACT_EMAIL=${NETWORK_CONTACT_EMAIL:-}
281290
- NETWORK_CONTACT_DISCORD=${NETWORK_CONTACT_DISCORD:-}
282-
- MEMBERS_FILE=${MEMBERS_FILE:-/app/web-data/members.json}
283-
volumes:
284-
# Mount web data directory for members.json (optional)
285-
- ${WEB_DATA_PATH:-./data/web}:/app/web-data:ro
286291
command: ["web"]
287292
healthcheck:
288293
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')"]
@@ -302,9 +307,10 @@ services:
302307
profiles:
303308
- migrate
304309
volumes:
305-
- meshcore_data:/data
310+
# Mount data directory (uses collector/meshcore.db)
311+
- ${DATA_HOME:-./data}:/data
306312
environment:
307-
- DATABASE_URL=sqlite:////data/meshcore.db
313+
- DATA_HOME=/data
308314
command: ["db", "upgrade"]
309315

310316
# ==========================================================================
@@ -318,12 +324,13 @@ services:
318324
profiles:
319325
- import-tags
320326
volumes:
321-
- meshcore_data:/data
322-
- ${COLLECTOR_DATA_PATH:-./data/collector}:/app/collector-data:ro
327+
# Mount data directory (uses collector/tags.json and collector/meshcore.db)
328+
- ${DATA_HOME:-./data}:/data
323329
environment:
324-
- DATABASE_URL=sqlite:////data/meshcore.db
330+
- DATA_HOME=/data
325331
- LOG_LEVEL=${LOG_LEVEL:-INFO}
326-
command: ["collector", "import-tags", "/app/collector-data/tags.json"]
332+
# Uses default tags file: /data/collector/tags.json
333+
command: ["collector", "import-tags"]
327334

328335
# ==========================================================================
329336
# Volumes
@@ -333,5 +340,3 @@ volumes:
333340
name: meshcore_mosquitto_data
334341
mosquitto_log:
335342
name: meshcore_mosquitto_log
336-
meshcore_data:
337-
name: meshcore_data

src/meshcore_hub/api/cli.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,19 @@
1818
envvar="API_PORT",
1919
help="API server port",
2020
)
21+
@click.option(
22+
"--data-home",
23+
type=str,
24+
default=None,
25+
envvar="DATA_HOME",
26+
help="Base data directory (default: ./data)",
27+
)
2128
@click.option(
2229
"--database-url",
2330
type=str,
24-
default="sqlite:///./meshcore.db",
31+
default=None,
2532
envvar="DATABASE_URL",
26-
help="Database connection URL",
33+
help="Database connection URL (default: sqlite:///{data_home}/collector/meshcore.db)",
2734
)
2835
@click.option(
2936
"--read-key",
@@ -78,7 +85,8 @@ def api(
7885
ctx: click.Context,
7986
host: str,
8087
port: int,
81-
database_url: str,
88+
data_home: str | None,
89+
database_url: str | None,
8290
read_key: str | None,
8391
admin_key: str | None,
8492
mqtt_host: str,
@@ -108,14 +116,27 @@ def api(
108116
"""
109117
import uvicorn
110118

119+
from meshcore_hub.common.config import get_api_settings
111120
from meshcore_hub.api.app import create_app
112121

122+
# Get settings to compute effective values
123+
settings = get_api_settings()
124+
125+
# Override data_home if provided
126+
if data_home:
127+
settings = settings.model_copy(update={"data_home": data_home})
128+
129+
# Use effective database URL if not explicitly provided
130+
effective_db_url = database_url if database_url else settings.effective_database_url
131+
effective_data_home = data_home or settings.data_home
132+
113133
click.echo("=" * 50)
114134
click.echo("MeshCore Hub API Server")
115135
click.echo("=" * 50)
116136
click.echo(f"Host: {host}")
117137
click.echo(f"Port: {port}")
118-
click.echo(f"Database: {database_url}")
138+
click.echo(f"Data home: {effective_data_home}")
139+
click.echo(f"Database: {effective_db_url}")
119140
click.echo(f"MQTT: {mqtt_host}:{mqtt_port} (prefix: {mqtt_prefix})")
120141
click.echo(f"Read key configured: {read_key is not None}")
121142
click.echo(f"Admin key configured: {admin_key is not None}")
@@ -144,7 +165,7 @@ def api(
144165
else:
145166
# For production, create app directly
146167
app = create_app(
147-
database_url=database_url,
168+
database_url=effective_db_url,
148169
read_key=read_key,
149170
admin_key=admin_key,
150171
mqtt_host=mqtt_host,

0 commit comments

Comments
 (0)