diff --git a/.github/prompts/repo-deploy.prompt.md b/.github/prompts/repo-deploy.prompt.md new file mode 100644 index 0000000..88423db --- /dev/null +++ b/.github/prompts/repo-deploy.prompt.md @@ -0,0 +1,294 @@ +--- +description: "Gated, end-to-end workflow for evaluating a public or private repo and deploying it into the homelab. Covers repo analysis, node placement, Compose authoring, documentation generation (README + SOP/KBA), smoke testing, and GitOps commit. One repo at a time." +--- + +# [ROLE] + +You are a **Senior Homelab DevOps Engineer** acting as a **mentor and deployment guide**. You help a homelab operator safely evaluate any repo and shepherd it through the full deployment lifecycle: analysis → planning → documentation → testing → production. + +You know this lab's conventions intimately: +- **Nodes**: `heimdall` (general apps, media, tools), `waldorf` (GPU/media workloads, Immich, Plex), `watchtower` (Docker auto-updates) +- **Orchestration**: Komodo (GitOps via Gitea), stacks live at `nodes///compose.yaml` +- **Reverse proxy**: Traefik with labels for routing +- **Secrets**: `.env` files managed with `git-crypt`; secrets never committed in plaintext +- **Appdata**: NFS at `10.0.0.250:/Volume1/appdata/` or local `/opt/appdata/` +- **User conventions**: `PUID=1000 PGID=1000` unless otherwise required +- **Documentation**: SOPs in `documentation/SOPs/`, KBAs in `documentation/KBAs/` + +# [NON-NEGOTIABLES] + +- **One repo at a time.** Do not attempt to deploy multiple services in a single run. +- **Explicit confirmation gates.** Do **not** advance past any gate without the exact confirmation phrase. +- **Secrets are sacred.** Never write secrets into tracked files. Always use `.env` references. +- **Minimal blast radius.** Changes must not affect unrelated stacks or infrastructure. +- **Documentation is not optional.** Every deployment produces a README and at minimum a KBA or SOP. +- **Testing is not optional.** Every deployment passes a smoke test checklist before being marked complete. + +# [INPUTS] + +The primary input variable: + +- Repo URL: `${input:repoUrl}` + +Additional inputs are collected progressively through the workflow gates. + +--- + +# [WORKFLOW] + +## Gate 0 — Repo Intake + +**Prompt the user:** + +> I have the repo URL. Before we dive in, confirm you are ready to start the deployment lifecycle for this repo. Type exactly: +> `DEPLOY: ` + +Do not proceed until the exact phrase is received. + +Once confirmed, **fetch and analyze the repo** at `${input:repoUrl}`. Produce a structured **Repo Snapshot** with these fields: + +| Field | Value | +|---|---| +| **Repo Name** | | +| **Maintainer / Org** | | +| **Description** | | +| **Primary Language** | | +| **License** | | +| **Last Commit** | | +| **Stars / Activity Signal** | | +| **Deployment Type** | `Docker`, `Docker Compose`, `Binary`, `Script`, `Ansible Role`, `Other` | +| **Official Docs URL** | | +| **Notable Tags / Releases** | | +| **Docker Hub / GHCR image** | (if applicable) | + +If the repo is inaccessible, ask the user to paste the relevant README or `docker-compose.yml` sections directly. + +--- + +## Step 1 — Classify & Risk-Assess + +Based on the Repo Snapshot, determine: + +1. **Deployment Category** + - `A` — Docker Compose app (has image, can integrate into node stacks) + - `B` — Script or CLI tool (runs on host or in CI) + - `C` — Ansible role or playbook + - `D` — Other / requires custom approach + +2. **Risk Level**: `Low` / `Medium` / `High` + - High: requires privileged mode, Docker socket, host network, root + - Medium: exposes ports, needs persistent storage, touches shared volumes + - Low: stateless, no special privileges + +3. **Recommended Node**: Suggest `heimdall`, `waldorf`, or `watchtower` with reasoning. + +4. **Dependency Flags**: Call out any of the following if present: + - External databases (Postgres, Redis, MySQL) + - GPU requirements + - Specific kernel modules or host dependencies + - OAuth / SSO integration (Authentik) + +Present this as a brief **Classification Report** and ask for feedback before continuing. + +Required confirmation phrase: +> `CLASSIFY CONFIRMED: ` + +--- + +## Step 2 — Deployment Planning + +Collect the following from the user (prompt for each): + +- **Target node**: `${input:targetNode}` (heimdall / waldorf / watchtower) +- **Compose folder path**: `nodes/${input:targetNode}/${input:serviceName}/compose.yaml` +- **Appdata path**: `${input:appdataPath}` (NFS or local) +- **Desired subdomain / URL**: `${input:subdomain}.castaldifamily.com` (or none) +- **Authentik SSO**: Required? `${input:ssoRequired}` (yes / no / later) +- **Komodo stack name**: `${input:stackName}` + +Then produce a **Deployment Plan** covering: + +1. **Compose file location** — path in Git repo +2. **Appdata layout** — directories to pre-create +3. **Network plan** — which Docker networks, port allocations; flag conflicts with existing services +4. **Secrets plan** — list of env vars that must go in `.env`, never in `compose.yaml` +5. **Traefik routing** — labels needed for HTTP/HTTPS routing (if applicable) +6. **SSO integration** — Authentik middleware labels (if applicable) +7. **Komodo configuration** — stack name, repo path, branch, deploy triggers +8. **Backup scope** — which appdata paths need backup coverage + +Required confirmation phrase: +> `PLAN CONFIRMED: ` + +--- + +## Step 3 — Author Compose File + +Produce a **production-ready `compose.yaml`** for `nodes///compose.yaml` following lab conventions: + +```yaml +# nodes///compose.yaml +# Deployed via Komodo | Managed by GitOps +# Service: +# Source: +``` + +Requirements: +- Use a **pinned image tag** (not `latest`) +- Set `restart: unless-stopped` +- Define a `healthcheck` if the upstream image supports it +- Mount appdata via bind mounts (no anonymous volumes) +- Reference all secrets as `${VAR_NAME}` — never hardcoded +- Set `PUID` / `PGID` if the image respects them +- Include Traefik labels if subdomain was provided +- Include Authentik middleware labels if SSO was requested +- Add `deploy.resources.limits` (cpu and memory) with conservative defaults + +Also produce a **`.env.example`** listing every variable with a description comment and placeholder value. This file IS committed to Git. The actual `.env` is not. + +Required confirmation phrase: +> `COMPOSE APPROVED: ` + +--- + +## Step 4 — Generate Documentation + +Produce **two documents**: + +### 4a — Service README + +File: `nodes///README.md` + +Sections: +- **Overview**: What the service does, link to upstream repo and docs +- **Node**: Which node runs it and why +- **Access**: URL, auth method +- **Appdata**: Paths and what lives there +- **Environment Variables**: Table of all vars (name, purpose, example value) +- **Networking**: Ports, Docker networks, Traefik routing +- **Dependencies**: External services or databases required +- **Backup & Recovery**: What to back up, restore steps +- **Known Issues / Notes**: Anything quirky about this deployment + +### 4b — Knowledge Base Article (KBA) or SOP + +**Choose based on complexity:** +- **KBA** — if this is a standalone service with no multi-step operational process +- **SOP** — if deployment involves multi-node coordination, migrations, or recurring procedures + +File: `documentation/KBAs/KBA-XXX--Deployment.md` OR `documentation/SOPs/SOP-XXX-Deploy-.md` + +Sections for KBA: +- **Symptom / Use Case** +- **Resolution / Deployment Steps** +- **Verification** +- **Related Resources** + +Sections for SOP: +- **Purpose** +- **Prerequisites** (access, infrastructure checklist) +- **Procedures** (numbered steps) +- **Rollback** +- **Verification** +- **Revision History** + +Required confirmation phrase: +> `DOCS APPROVED: ` + +--- + +## Step 5 — Testing & Validation Checklist + +Before marking the deployment ready, run through this checklist and mark each item as `PASS`, `FAIL`, or `SKIP` with a note: + +### Pre-Deploy Checks +- [ ] Image tag is pinned (not `latest`) +- [ ] All required env vars are documented in `.env.example` +- [ ] No secrets present in `compose.yaml` or README +- [ ] Port conflicts checked against existing node services +- [ ] Appdata directories noted for pre-creation +- [ ] `git-crypt` `.env` file excluded from plaintext tracking (`.gitattributes`) + +### Deploy Checks +- [ ] Komodo stack pulls from correct repo path and branch +- [ ] Container starts without error (`docker compose up -d` clean exit) +- [ ] Healthcheck reaches `healthy` state within expected time +- [ ] Service is reachable at the expected URL or port +- [ ] Traefik routes correctly (HTTPS, no certificate errors) +- [ ] SSO / Authentik login works (if configured) + +### Post-Deploy Checks +- [ ] Logs show no persistent errors (`docker compose logs --tail=50`) +- [ ] Appdata directories created with correct ownership +- [ ] Service survives a container restart (`docker compose restart`) +- [ ] Komodo webhook triggers a re-deploy successfully (GitOps round-trip) + +Present the completed checklist to the user. If any items are `FAIL`, do not proceed — diagnose and resolve first. + +Required confirmation phrase: +> `TESTS PASSED: ` + +--- + +## Gate 5 — GitOps Commit + +Present a **commit plan** summarizing all files that will be added or modified: + +| Action | File | +|---|---| +| `ADD` | `nodes///compose.yaml` | +| `ADD` | `nodes///README.md` | +| `ADD` | `nodes///.env.example` | +| `ADD` | `documentation/KBAs/KBA-XXX-...md` OR `documentation/SOPs/SOP-XXX-...md` | + +Provide the suggested commit message: + +``` +feat(): add stack to + +- Compose file with pinned image, healthcheck, Traefik routing +- .env.example with all required variables documented +- README covering access, appdata, backup scope +- KBA/SOP for deployment reference + +Source: +``` + +Required confirmation phrase: +> `COMMIT READY: ` + +Only after this confirmation: provide final file contents ready to copy/paste or apply. + +--- + +## Gate 6 — Deployment Complete + +Once files are committed and the Komodo stack is live, prompt the user to confirm: + +> "Deployment is live. Run through the Post-Deploy Checks one final time and confirm everything is green." + +Required confirmation phrase: +> `DEPLOYED: ` + +Output a brief **Deployment Summary**: +- Service name and URL +- Node +- Image and tag +- Appdata path +- Documentation files created +- Date deployed +- Any open follow-up items (backup integration, SSO, monitoring) + +--- + +# [FORMAT] + +- All output is Markdown +- Use tables for structured data (env vars, checklist, file lists) +- Use fenced code blocks with syntax highlighting for all YAML, shell, and config output +- Gate confirmations must be quoted exactly — highlight them in a `> blockquote` +- Never output partial steps; always complete a step fully before presenting the gate + +# [TONE] + +Mentor-first. Explain *why* each decision matters. Flag risks clearly without being alarmist. Keep momentum — the goal is a clean, documented, tested deployment in the homelab. diff --git a/documentation/KBAs/KBA-002-SparkyFitness-Deployment.md b/documentation/KBAs/KBA-002-SparkyFitness-Deployment.md new file mode 100644 index 0000000..9caebc3 --- /dev/null +++ b/documentation/KBAs/KBA-002-SparkyFitness-Deployment.md @@ -0,0 +1,220 @@ +# KBA-002: SparkyFitness — Initial Deployment on Heimdall + +**Status:** Active +**Created:** May 10, 2026 +**Last Updated:** May 10, 2026 +**Affected Systems:** heimdall, Komodo, Traefik +**Severity:** Informational + +--- + +## Summary + +Reference article for deploying SparkyFitness (self-hosted fitness tracker) on the heimdall node. Covers pre-deployment checklist, secret generation, first-boot sequence, and verification steps. + +--- + +## Use Case + +You want to deploy SparkyFitness for the first time, or you are recovering from a failed deployment or data loss event and need to redeploy from scratch. + +--- + +## Prerequisites + +### Access Required + +- [ ] SSH access to `heimdall` +- [ ] Gitea access: `git.castaldifamily.com` +- [ ] Komodo UI access: `komodo.castaldifamily.com` +- [ ] NFS share mounted and writable at `/mnt/appdata/` on heimdall + +### Files in Repo + +Confirm these files exist in `nodes/heimdall/sparkyfitness/` on `main`: + +- [ ] `compose.yaml` +- [ ] `.env.example` +- [ ] `README.md` + +--- + +## Resolution / Deployment Steps + +### Step 1 — Generate Secrets + +On any machine with `openssl` available, generate the four required secrets: + +```bash +# API encryption key — SAVE THIS. Do not rotate after data is stored. +openssl rand -hex 32 + +# Better Auth secret — SAVE THIS. Do not rotate after users enable 2FA. +openssl rand -hex 32 + +# Database superuser password +openssl rand -base64 24 + +# Application user password +openssl rand -base64 24 +``` + +Store these immediately in the git-crypt-encrypted `.env` file at: + +``` +nodes/heimdall/sparkyfitness/.env +``` + +Using `.env.example` as the template: + +```bash +cp nodes/heimdall/sparkyfitness/.env.example nodes/heimdall/sparkyfitness/.env +# Edit .env and fill in all four required secret values +``` + +> ⚠️ Confirm `.gitattributes` marks `nodes/heimdall/sparkyfitness/.env` as a git-crypt file before committing. + +### Step 2 — Create Appdata Directories + +SSH into heimdall and pre-create the required directories: + +```bash +ssh heimdall +mkdir -p /mnt/appdata/sparkyfitness/data/postgresql +mkdir -p /mnt/appdata/sparkyfitness/data/backup +mkdir -p /mnt/appdata/sparkyfitness/data/uploads +``` + +> PostgreSQL will fail to initialize if `postgresql/` does not exist or has incorrect ownership. Docker will create the directory automatically, but pre-creating it ensures the NFS mount is active and writable before the container starts. + +### Step 3 — Commit and Push + +```bash +git add nodes/heimdall/sparkyfitness/ +git commit -m "feat(sparkyfitness): add sparkyfitness stack to heimdall + +- Compose file with pinned images (v0.16.6.1), healthchecks, Traefik routing +- .env.example with all required variables documented +- README covering access, appdata, backup scope + +Source: https://github.com/CodeWithCJ/SparkyFitness" +git push origin main +``` + +### Step 4 — Create Komodo Stack + +1. Open Komodo UI → **Stacks** → **New Stack** +2. Configure: + +| Field | Value | +|---|---| +| **Name** | `sparkyfitness` | +| **Server** | `heimdall` | +| **Source** | Git Repo | +| **Repo** | `homelab` | +| **Branch** | `main` | +| **Run Directory** | `nodes/heimdall/sparkyfitness` | +| **File Paths** | *(leave blank — uses `compose.yaml` by default)* | +| **Webhook** | Enable — trigger on push to `main` | +| **Auto Update** | ❌ Disabled | + +3. Click **Deploy** + +### Step 5 — First Boot Sequence + +The startup order is enforced by `depends_on` + `healthcheck` conditions: + +``` +sparkyfitness-db (healthy) → sparkyfitness-server (healthy) → sparkyfitness-frontend +``` + +Expected timeline: +- `sparkyfitness-db`: healthy within ~30 seconds +- `sparkyfitness-server`: ~60–90 seconds (runs DB migrations on first boot) +- `sparkyfitness-frontend`: starts once server is healthy + +Monitor startup: + +```bash +ssh heimdall +docker compose -f /etc/komodo/stacks/sparkyfitness/compose.yaml logs -f +``` + +### Step 6 — Set Admin Account + +If `SPARKY_FITNESS_ADMIN_EMAIL` is set in `.env`, the first user with that email will be automatically granted admin on server startup. + +Alternatively, register via the UI at https://fitness.castaldifamily.com, then promote from the Admin panel. + +> After creating your account, set `SPARKY_FITNESS_DISABLE_SIGNUP=true` in `.env` to close open registration. Redeploy to apply. + +--- + +## Verification + +Run through the following checks after deployment: + +### Container Health + +```bash +docker ps --filter "name=sparkyfitness" +# All three containers should show: Up X minutes (healthy) +``` + +### Log Check + +```bash +docker compose logs --tail=50 sparkyfitness-server +# Should show: server started, DB migrations complete, no ERRORs +``` + +### UI Access + +- Open https://fitness.castaldifamily.com in a browser +- Confirm: HTTPS with valid certificate, no browser errors +- Confirm: Login page loads + +### Traefik Routing + +- Open Traefik dashboard → confirm `sparkyfitness` router is active and green +- Confirm TLS cert is issued via Cloudflare resolver + +### Restart Resilience + +```bash +docker restart sparkyfitness-frontend +# Wait 30s, then confirm the UI is still accessible +``` + +--- + +## Rollback + +If deployment fails and you need to revert: + +1. Stop the stack in Komodo: **Stack → Stop** +2. Remove containers: `docker compose down` on heimdall +3. The `postgresql/` data directory is untouched by `docker compose down` (bind mount, not a volume) +4. Revert the compose file changes in Git and push — Komodo will redeploy the previous version + +> ⚠️ `docker compose down -v` would only affect named volumes. Since we use bind mounts, data in `/mnt/appdata/sparkyfitness/data/` is always preserved unless you manually delete it. + +--- + +## Related Resources + +- [SparkyFitness README](../../nodes/heimdall/sparkyfitness/README.md) +- [Upstream GitHub](https://github.com/CodeWithCJ/SparkyFitness) +- [Official Docs](https://codewithcj.github.io/SparkyFitness/) +- [Docker Compose Install Guide](https://codewithcj.github.io/SparkyFitness/install/docker-compose) +- [Environment Variables Reference](https://codewithcj.github.io/SparkyFitness/install/environment-variables) +- [Postgres Upgrade Guide](https://codewithcj.github.io/SparkyFitness/install/postgres-upgrade) +- [KBA-001: Komodo GitOps Stack Deployment Failures](KBA-001-Komodo-GitOps-Stack-Deployment-Failures.md) + +--- + +## Revision History + +| Date | Author | Change | +|---|---|---| +| 2026-05-10 | Nathan Castaldi | Initial deployment — SparkyFitness v0.16.6.1 on heimdall | diff --git a/documentation/project-history/SESSION_SNAPSHOT_2026-05-10.md b/documentation/project-history/SESSION_SNAPSHOT_2026-05-10.md new file mode 100644 index 0000000..533a3ff --- /dev/null +++ b/documentation/project-history/SESSION_SNAPSHOT_2026-05-10.md @@ -0,0 +1,82 @@ +# Session Snapshot - 2026-05-10 + +## Summary + +Built a reusable end-to-end repo deployment prompt (`repo-deploy.prompt.md`) and ran it live against [SparkyFitness](https://github.com/CodeWithCJ/SparkyFitness) — a self-hosted fitness tracker. Completed all pre-deploy phases: repo analysis, classification, planning, compose authoring, and documentation. Stack is ready to deploy on heimdall pending live smoke tests on the node. + +--- + +## Work Completed + +### New Prompt +- Created `.github/prompts/repo-deploy.prompt.md` — a gated, six-step workflow prompt that takes a repo URL and shepherds it through the full deployment lifecycle: analysis → classification → planning → compose authoring → documentation → testing → GitOps commit. Designed to be reusable for any future service. + +### SparkyFitness Deployment (heimdall) +- Fetched and analyzed upstream repo (`v0.16.6.1`, 3.4k stars, actively maintained) +- Classified as Category A (Docker Compose), Risk: Medium, Node: heimdall +- Confirmed no port conflicts with existing heimdall services +- Produced deployment plan: `proxy-net` + isolated `sparkyfitness-network`, no host ports exposed, Traefik routing to `fitness.castaldifamily.com` +- Authored `nodes/heimdall/sparkyfitness/compose.yaml` with pinned images, healthchecks on all three services, resource limits, and Traefik labels +- Authored `nodes/heimdall/sparkyfitness/.env.example` with all four required secrets, generation commands, and optional OIDC/SMTP/Garmin blocks +- Created `nodes/heimdall/sparkyfitness/README.md` — full service reference (access, appdata, env vars, networking, backup/recovery, upgrade procedure, known issues) +- Created `documentation/KBAs/KBA-002-SparkyFitness-Deployment.md` — step-by-step deployment KBA covering secret generation, appdata setup, Komodo stack creation, first-boot sequence, verification, and rollback + +--- + +## Files Changed + +| Action | File | +|---|---| +| `ADD` | `.github/prompts/repo-deploy.prompt.md` | +| `ADD` | `nodes/heimdall/sparkyfitness/compose.yaml` | +| `ADD` | `nodes/heimdall/sparkyfitness/.env.example` | +| `ADD` | `nodes/heimdall/sparkyfitness/README.md` | +| `ADD` | `documentation/KBAs/KBA-002-SparkyFitness-Deployment.md` | + +--- + +## Validation and Test Results + +### Pre-Deploy Checks (static — all PASS) +- ✅ All image tags pinned (`postgres:18.3-alpine`, `sparkyfitness_server:v0.16.6.1`, `sparkyfitness:v0.16.6.1`) +- ✅ No secrets in tracked files — all four secrets use `${}` interpolation; `.env` covered by `.gitignore` +- ✅ No port conflicts on heimdall +- ✅ Appdata pre-creation steps documented in KBA-002 +- ✅ `.env.example` documents all required variables with generation commands + +### Deploy / Post-Deploy Checks +- ⬜ **PENDING** — require live execution on heimdall (Komodo stack creation, container health, Traefik routing, log check, restart resilience, webhook round-trip) + +--- + +## New Technical Debt + +- None. No `@TODO` or `FIXME` comments introduced this session. + +--- + +## Open Issues + +- SparkyFitness deploy/post-deploy smoke tests not yet run — stack has not been pushed to Komodo and brought live. Must complete before marking `TESTS PASSED: sparkyfitness`. +- No git-crypt configured in this repo — `.env` files are excluded via `.gitignore` rather than encrypted and tracked. Secrets are not version-controlled. Recommend storing secrets in Vaultwarden and/or completing the git-crypt migration (see `documentation/plans/plan-gitcryptMigration.md`). +- Garmin microservice (`sparkyfitness-garmin`) is commented out — upstream marks it as still in development. Revisit when maintainer marks stable. +- Upcoming mandatory Postgres upgrade flagged by upstream in release notes — monitor and apply before it becomes breaking. + +--- + +## Next Steps + +1. SSH into heimdall, pre-create appdata directories (`mkdir -p /mnt/appdata/sparkyfitness/data/{postgresql,backup,uploads}`) +2. Generate secrets (`openssl rand -hex 32` ×2, `openssl rand -base64 24` ×2) and populate `nodes/heimdall/sparkyfitness/.env` +3. Commit and push all new files to `main` +4. Create Komodo stack `sparkyfitness` per KBA-002 Step 4 +5. Run live smoke test checklist from Step 5 and confirm `TESTS PASSED: sparkyfitness` +6. Complete Gates 5 & 6 of `repo-deploy.prompt.md` (commit plan → deployment summary) +7. After first use is stable, consider registering initial admin account and disabling open signup (`SPARKY_FITNESS_DISABLE_SIGNUP=true`) + +--- + +## Session Notes + +- `repo-deploy.prompt.md` was validated end-to-end in this session and is ready for reuse on future services. The gated confirmation model (exact phrases per step) worked well for keeping the workflow deliberate. +- SparkyFitness chosen as the first service run through the new prompt. All static gates passed; live deploy is the remaining work. diff --git a/nodes/heimdall/sparkyfitness/README.md b/nodes/heimdall/sparkyfitness/README.md new file mode 100644 index 0000000..c9de6dd --- /dev/null +++ b/nodes/heimdall/sparkyfitness/README.md @@ -0,0 +1,181 @@ +# SparkyFitness + +**Node:** heimdall +**Stack:** `sparkyfitness` +**URL:** https://fitness.castaldifamily.com +**Version:** v0.16.6.1 +**Source:** https://github.com/CodeWithCJ/SparkyFitness +**Docs:** https://codewithcj.github.io/SparkyFitness/ + +--- + +## Overview + +SparkyFitness is a self-hosted, privacy-first fitness and health tracking platform — a full alternative to MyFitnessPal. It stores all data on infrastructure you control with no reliance on third-party services. + +**Core capabilities:** +- Nutrition, exercise, hydration, sleep, fasting, mood, and body measurement tracking +- Goal setting and daily check-ins +- Interactive charts and long-term reports +- Multiple user profiles and family access +- Native OIDC, TOTP, Passkey, and MFA support +- Health platform integrations: Apple Health, Google Health Connect, Fitbit, Withings, Garmin, Polar, Strava +- Food database integrations: OpenFoodFacts, USDA, FatSecret, Nutritionix +- Recipe app integrations: Mealie, Tandoor +- Optional AI chatbot for conversational food/exercise logging (beta) + +**Stack architecture:** + +| Container | Image | Role | +|---|---|---| +| `sparkyfitness-db` | `postgres:18.3-alpine` | PostgreSQL database | +| `sparkyfitness-server` | `codewithcj/sparkyfitness_server:v0.16.6.1` | Node.js backend API | +| `sparkyfitness-frontend` | `codewithcj/sparkyfitness:v0.16.6.1` | React frontend (Nginx) | + +--- + +## Node + +Runs on **heimdall** — the general-purpose applications node. No GPU is required. Workload is typical of a personal web application. + +--- + +## Access + +| Method | Detail | +|---|---| +| **URL** | https://fitness.castaldifamily.com | +| **Auth** | SparkyFitness native auth (TOTP, passkeys, MFA supported) | +| **SSO** | Not configured — OIDC with Authentik can be added via env vars (see `.env.example`) | +| **Admin panel** | Set `SPARKY_FITNESS_ADMIN_EMAIL` in `.env` to auto-grant admin on first start | + +> ⚠️ Set `SPARKY_FITNESS_DISABLE_SIGNUP=true` in `.env` after creating your initial account to prevent open registration. + +--- + +## Appdata + +All persistent data lives under `/mnt/appdata/sparkyfitness/data/` on the NFS share. + +| Path | Contents | Backup Priority | +|---|---|---| +| `postgresql/` | All user data — nutrition logs, workouts, health metrics, accounts | 🔴 Critical | +| `uploads/` | Profile pictures and exercise images | 🟡 Important | +| `backup/` | App-generated PostgreSQL exports | 🟢 Nice-to-have | + +Pre-create these directories before first deploy: + +```bash +mkdir -p /mnt/appdata/sparkyfitness/data/postgresql +mkdir -p /mnt/appdata/sparkyfitness/data/backup +mkdir -p /mnt/appdata/sparkyfitness/data/uploads +``` + +--- + +## Environment Variables + +Managed via `.env` (git-crypt encrypted). See `.env.example` for the full reference. + +### Required Secrets + +| Variable | Purpose | How to Generate | +|---|---|---| +| `SPARKY_FITNESS_DB_PASSWORD` | PostgreSQL superuser password | `openssl rand -base64 24` | +| `SPARKY_FITNESS_APP_DB_PASSWORD` | App-role DB password (limited privileges) | `openssl rand -base64 24` | +| `SPARKY_FITNESS_API_ENCRYPTION_KEY` | 64-char hex key for external integration encryption | `openssl rand -hex 32` | +| `BETTER_AUTH_SECRET` | Signs sessions and encrypts TOTP/2FA data | `openssl rand -hex 32` | + +> ⚠️ `SPARKY_FITNESS_API_ENCRYPTION_KEY` — changing this after data is stored invalidates all encrypted external integration credentials. +> ⚠️ `BETTER_AUTH_SECRET` — changing this after users have enabled TOTP/2FA locks them out. Do not rotate without disabling 2FA first. + +### Non-Secret Configuration (hardcoded in compose.yaml) + +| Variable | Value | Notes | +|---|---|---| +| `SPARKY_FITNESS_FRONTEND_URL` | `https://fitness.castaldifamily.com` | Used for CORS and session binding | +| `SPARKY_FITNESS_DB_HOST` | `sparkyfitness-db` | Internal Docker service name | +| `SPARKY_FITNESS_DB_NAME` | `sparkyfitness_db` | Database name | +| `SPARKY_FITNESS_DB_USER` | `sparky` | Superuser for migrations | +| `SPARKY_FITNESS_APP_DB_USER` | `sparky_app` | Runtime app user (limited privileges) | +| `SPARKY_FITNESS_LOG_LEVEL` | `ERROR` | Use `DEBUG` temporarily for troubleshooting | +| `NODE_ENV` | `production` | | +| `TZ` | `Etc/UTC` | Adjust if local time is needed for date handling | +| `SPARKY_FITNESS_FORCE_EMAIL_LOGIN` | `true` | Failsafe — prevents OIDC misconfiguration lockout | + +--- + +## Networking + +| Network | Type | Used By | +|---|---|---| +| `proxy-net` | External (Traefik) | `sparkyfitness-frontend` only | +| `sparkyfitness-network` | Internal bridge | All three containers | + +No host ports are exposed. All external traffic enters via Traefik. + +### Traefik Routing + +| Field | Value | +|---|---| +| **Router** | `sparkyfitness` | +| **Rule** | `Host(\`fitness.castaldifamily.com\`)` | +| **Entrypoint** | `websecure` (HTTPS) | +| **TLS** | Cloudflare cert resolver | +| **Middleware** | `security-headers@file` | +| **Backend port** | `80` (Nginx inside `sparkyfitness-frontend`) | + +The frontend Nginx container internally proxies API requests to `sparkyfitness-server:3010` over `sparkyfitness-network`. No separate Traefik route is needed for the API. + +--- + +## Dependencies + +| Dependency | Type | Notes | +|---|---|---| +| PostgreSQL 18.3-alpine | Bundled | Self-contained — no shared DB instance | +| Traefik | External (core stack) | Must be running on `proxy-net` | +| Cloudflare DNS | External | Required for TLS cert resolver | + +No Redis, GPU, or kernel module dependencies. + +--- + +## Backup & Recovery + +### What to Back Up + +The `postgresql/` directory is the **single source of truth** for all user data. This must be in the backup rotation before going live. + +``` +/mnt/appdata/sparkyfitness/data/postgresql/ ← Critical +/mnt/appdata/sparkyfitness/data/uploads/ ← Important +``` + +### Restore Steps + +1. Stop the stack: `docker compose down` +2. Restore the PostgreSQL data directory to `/mnt/appdata/sparkyfitness/data/postgresql/` +3. Restore uploads to `/mnt/appdata/sparkyfitness/data/uploads/` +4. Redeploy: `docker compose up -d` +5. Verify the DB is healthy: `docker compose logs sparkyfitness-db` + +### Upgrading + +> ⚠️ Auto-updating containers is **not recommended** by the maintainer. Watchtower should be excluded from this stack. + +1. Review release notes at https://github.com/CodeWithCJ/SparkyFitness/releases +2. Take a backup of `postgresql/` +3. Update image tags in `compose.yaml` +4. Commit and push — Komodo will redeploy via webhook +5. Check logs: `docker compose logs --tail=50` + +--- + +## Known Issues / Notes + +- **Garmin microservice** (`sparkyfitness-garmin`) is still in development upstream. It is commented out of the compose file intentionally. Do not enable until the maintainer marks it stable. +- **Auto-update warning**: The maintainer explicitly warns against automated container updates between releases. Ensure Watchtower is not targeting this stack. +- **Postgres upgrade notice**: The upstream release notes flag a forthcoming mandatory Postgres upgrade. Monitor release notes and follow the [official Postgres upgrade guide](https://codewithcj.github.io/SparkyFitness/install/postgres-upgrade) when the time comes. +- **AI chatbot** is in beta — expect potential bugs if enabled. +- **Family & Friends access** is in beta — expect potential bugs if enabled. diff --git a/nodes/heimdall/sparkyfitness/compose.yaml b/nodes/heimdall/sparkyfitness/compose.yaml new file mode 100644 index 0000000..71ece33 --- /dev/null +++ b/nodes/heimdall/sparkyfitness/compose.yaml @@ -0,0 +1,149 @@ +x-info: + repo: https://github.com/CodeWithCJ/SparkyFitness + releases: https://github.com/CodeWithCJ/SparkyFitness/releases + documentation: https://codewithcj.github.io/SparkyFitness/ + +# nodes/heimdall/sparkyfitness/compose.yaml +# Deployed via Komodo | Managed by GitOps +# Service: sparkyfitness +# Source: https://github.com/CodeWithCJ/SparkyFitness + +networks: + proxy-net: + external: true + sparkyfitness-network: + driver: bridge + +services: + + # ── Database ──────────────────────────────────────────────────────────────── + sparkyfitness-db: + image: postgres:18.3-alpine + container_name: sparkyfitness-db + restart: unless-stopped + networks: + - sparkyfitness-network + environment: + POSTGRES_DB: sparkyfitness_db + POSTGRES_USER: sparky + POSTGRES_PASSWORD: "${SPARKY_FITNESS_DB_PASSWORD}" + PUID: 1000 + GUID: 1000 + volumes: + - /mnt/appdata/sparkyfitness/data/postgresql:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U sparky -d sparkyfitness_db"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + deploy: + resources: + limits: + cpus: "1.0" + memory: 512M + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # ── Backend Server ─────────────────────────────────────────────────────────── + sparkyfitness-server: + image: codewithcj/sparkyfitness_server:v0.16.6.1 + container_name: sparkyfitness-server + restart: unless-stopped + networks: + - sparkyfitness-network + environment: + # Database connection + SPARKY_FITNESS_DB_HOST: sparkyfitness-db + SPARKY_FITNESS_DB_PORT: 5432 + SPARKY_FITNESS_DB_NAME: sparkyfitness_db + SPARKY_FITNESS_DB_USER: sparky + SPARKY_FITNESS_DB_PASSWORD: "${SPARKY_FITNESS_DB_PASSWORD}" + SPARKY_FITNESS_APP_DB_USER: sparky_app + SPARKY_FITNESS_APP_DB_PASSWORD: "${SPARKY_FITNESS_APP_DB_PASSWORD}" + # Security — values sourced from .env (git-crypt) + SPARKY_FITNESS_API_ENCRYPTION_KEY: "${SPARKY_FITNESS_API_ENCRYPTION_KEY}" + BETTER_AUTH_SECRET: "${BETTER_AUTH_SECRET}" + # Application + SPARKY_FITNESS_FRONTEND_URL: "https://fitness.castaldifamily.com" + SPARKY_FITNESS_LOG_LEVEL: ERROR + NODE_ENV: production + TZ: Etc/UTC + SPARKY_FITNESS_DISABLE_SIGNUP: "false" + SPARKY_FITNESS_FORCE_EMAIL_LOGIN: "true" + ALLOW_PRIVATE_NETWORK_CORS: "false" + PUID: 1000 + GUID: 1000 + # Garmin microservice (disabled — service not yet stable) + # GARMIN_MICROSERVICE_URL: http://sparkyfitness-garmin:8000 + volumes: + - /mnt/appdata/sparkyfitness/data/backup:/app/SparkyFitnessServer/backup + - /mnt/appdata/sparkyfitness/data/uploads:/app/SparkyFitnessServer/uploads + depends_on: + sparkyfitness-db: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:3010/ 2>&1 | grep -q '.' || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + deploy: + resources: + limits: + cpus: "1.5" + memory: 512M + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # ── Frontend ───────────────────────────────────────────────────────────────── + sparkyfitness-frontend: + image: codewithcj/sparkyfitness:v0.16.6.1 + container_name: sparkyfitness-frontend + restart: unless-stopped + networks: + - proxy-net + - sparkyfitness-network + environment: + SPARKY_FITNESS_FRONTEND_URL: "https://fitness.castaldifamily.com" + SPARKY_FITNESS_SERVER_HOST: sparkyfitness-server + SPARKY_FITNESS_SERVER_PORT: 3010 + PUID: 1000 + GUID: 1000 + depends_on: + sparkyfitness-server: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:80/ || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + labels: + # Enable Traefik + - "traefik.enable=true" + # HTTPS Router + - "traefik.http.routers.sparkyfitness.rule=Host(`fitness.castaldifamily.com`)" + - "traefik.http.routers.sparkyfitness.entrypoints=websecure" + - "traefik.http.routers.sparkyfitness.tls=true" + - "traefik.http.routers.sparkyfitness.tls.certresolver=cloudflare" + - "traefik.http.routers.sparkyfitness.service=sparkyfitness" + - "traefik.http.routers.sparkyfitness.middlewares=security-headers@file" + # Service definition + - "traefik.http.services.sparkyfitness.loadbalancer.server.port=80" + deploy: + resources: + limits: + cpus: "0.5" + memory: 128M + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3"