diff --git a/.env.dev.example b/.env.dev.example new file mode 100644 index 0000000..940c122 --- /dev/null +++ b/.env.dev.example @@ -0,0 +1,20 @@ +# Copying this file is optional. scripts/dev-env-init.sh generates strong secrets. +APP_ENV=development +APP_TIMEZONE=America/Winnipeg +POSTGRES_DB=minerops_dev +POSTGRES_USER=minerops_dev +POSTGRES_PASSWORD=replace-with-a-random-local-only-secret +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 +DATABASE_URL=postgresql://minerops_dev:replace-with-a-random-local-only-secret@postgres:5432/minerops_dev +REDIS_URL=redis://redis:6379/0 +JWT_SECRET_KEY=replace-with-a-random-local-only-secret +MINEROPS_AGENT_SHARED_SECRET=replace-with-a-random-local-only-secret +MINEROPS_DEV_PORT=18000 + +# Safety interlocks: keep these values disabled for development. +SCHEDULED_SCAN_ENABLED=false +MINEROPS_HUB_MODE=true +MINEROPS_DISABLE_LOCAL_SCANNER=true +MINEROPS_ENABLE_REAL_REBOOT=false +MINEROPS_ENABLE_REAL_POWER_CONTROL=false diff --git a/.gitignore b/.gitignore index cffc07c..686bbf4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ .env .env.* *.env +!.env.example +!.env.dev.example # Backups / dumps / generated data backups/ diff --git a/README.md b/README.md index 60a50d6..35240a8 100644 --- a/README.md +++ b/README.md @@ -74,9 +74,39 @@ Git status: pct exec 306 -- bash -lc "cd /opt/minerops && git status --short" +## Safe local development + +Development uses a separate Compose project, database volume, Redis volume, and +localhost-only backend port. It does not reuse the production `.env` file. +Scheduled scanning and real miner power/reboot controls are disabled. + +First-time setup: + +```bash +./scripts/dev-env-init.sh +docker compose --env-file .env.dev -f compose.dev.yaml up -d --build +``` + +Verify the environment: + +```bash +docker compose --env-file .env.dev -f compose.dev.yaml ps +curl --fail --silent http://127.0.0.1:18000/api/health +``` + +Stop development services without deleting their data: + +```bash +docker compose --env-file .env.dev -f compose.dev.yaml down +``` + +The generated `.env.dev` file is mode `0600` and ignored by Git. To use a +different local port, change `MINEROPS_DEV_PORT` in `.env.dev`. + ## Safety notes - Do not commit .env. - Do not commit backups, logs, database dumps, imports, or runtime data. +- Do not enable scanning or real miner controls in the development environment. - Check /system-health after major changes. - Commit and push after each stable working change. diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..db484a1 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,6 @@ +__pycache__/ +*.py[cod] +*.bak +*.bak.* +.pytest_cache/ +.mypy_cache/ diff --git a/compose.dev.yaml b/compose.dev.yaml new file mode 100644 index 0000000..091762c --- /dev/null +++ b/compose.dev.yaml @@ -0,0 +1,105 @@ +name: minerops-dev + +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - postgres-dev:/var/lib/postgresql/data + networks: + - dev + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 12 + + redis: + image: redis:7-alpine + command: ["redis-server", "--appendonly", "yes"] + volumes: + - redis-dev:/data + networks: + - dev + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 12 + + backend: + build: + context: ./backend + env_file: + - .env.dev + command: + - /bin/sh + - -ec + - alembic upgrade head && exec uvicorn app.main:app --host 0.0.0.0 --port 8000 + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + networks: + - dev + read_only: true + tmpfs: + - /tmp + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + healthcheck: + test: + - CMD + - python + - -c + - "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/health', timeout=3)" + interval: 10s + timeout: 5s + retries: 12 + start_period: 20s + + gateway: + image: nginx:1.29-alpine + depends_on: + backend: + condition: service_healthy + ports: + - "127.0.0.1:${MINEROPS_DEV_PORT:-18000}:8080" + networks: + - dev + - edge + volumes: + - ./dev/nginx.conf:/etc/nginx/nginx.conf:ro + read_only: true + tmpfs: + - /tmp + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + user: "101:101" + healthcheck: + test: + - CMD + - wget + - --quiet + - --spider + - http://127.0.0.1:8080/api/health + interval: 10s + timeout: 5s + retries: 12 + +networks: + dev: + internal: true + edge: + +volumes: + postgres-dev: + redis-dev: diff --git a/dev/nginx.conf b/dev/nginx.conf new file mode 100644 index 0000000..7ca033b --- /dev/null +++ b/dev/nginx.conf @@ -0,0 +1,30 @@ +worker_processes 1; +pid /tmp/nginx.pid; + +events { + worker_connections 128; +} + +http { + access_log /dev/stdout; + error_log /dev/stderr notice; + + client_body_temp_path /tmp/client_body; + proxy_temp_path /tmp/proxy; + fastcgi_temp_path /tmp/fastcgi; + uwsgi_temp_path /tmp/uwsgi; + scgi_temp_path /tmp/scgi; + + server { + listen 8080; + server_name _; + + location / { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } +} diff --git a/scripts/dev-env-init.sh b/scripts/dev-env-init.sh new file mode 100755 index 0000000..a1e2cfc --- /dev/null +++ b/scripts/dev-env-init.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +env_file="${project_dir}/.env.dev" + +if [[ -e "${env_file}" ]]; then + echo "Keeping existing ${env_file}" + exit 0 +fi + +umask 077 + +generate_secret() { + if command -v openssl >/dev/null 2>&1; then + openssl rand -hex 32 + else + python3 -c 'import secrets; print(secrets.token_hex(32))' + fi +} + +postgres_password="$(generate_secret)" +jwt_secret="$(generate_secret)" +agent_secret="$(generate_secret)" + +{ + echo "APP_ENV=development" + echo "APP_TIMEZONE=America/Winnipeg" + echo "POSTGRES_DB=minerops_dev" + echo "POSTGRES_USER=minerops_dev" + echo "POSTGRES_PASSWORD=${postgres_password}" + echo "POSTGRES_HOST=postgres" + echo "POSTGRES_PORT=5432" + echo "DATABASE_URL=postgresql://minerops_dev:${postgres_password}@postgres:5432/minerops_dev" + echo "REDIS_URL=redis://redis:6379/0" + echo "JWT_SECRET_KEY=${jwt_secret}" + echo "MINEROPS_AGENT_SHARED_SECRET=${agent_secret}" + echo "MINEROPS_DEV_PORT=18000" + echo "SCHEDULED_SCAN_ENABLED=false" + echo "MINEROPS_HUB_MODE=true" + echo "MINEROPS_DISABLE_LOCAL_SCANNER=true" + echo "MINEROPS_ENABLE_REAL_REBOOT=false" + echo "MINEROPS_ENABLE_REAL_POWER_CONTROL=false" +} >"${env_file}" + +chmod 0600 "${env_file}" +echo "Created ${env_file} with development-only secrets and safety interlocks."