How to Deploy SparkyFitness: Self-Hosted Fitness and Health Tracker

SparkyFitness is a privacy-first, self-hosted health and fitness platform that unifies nutrition, workout logging, and biometric tracking. Learn how to deploy SparkyFitness using Docker Compose and PostgreSQL to keep your health data on your own hardware.

How to Deploy SparkyFitness: Self-Hosted Fitness and Health Tracker

SparkyFitness is an all-in-one, privacy-first, self-hosted health, nutrition, workout, and biometric tracking platform. Distributed under a custom source-available license (free for personal and non-commercial homelab use), SparkyFitness replaces multiple fragmented, subscription-based commercial apps—such as MyFitnessPal (nutrition and calories), Hevy (strength training), Zero (intermittent fasting), Flo (cycle tracking), and Shotsy (medication adherence)—with a single unified suite that runs entirely on infrastructure you control.

SparkyFitness Diary Dashboard

Key Capabilities and Architecture

  1. Complete Biometric Data Sovereignty:

Commercial fitness apps frequently monetize personal health metrics, cardiovascular telemetry, and location routes. SparkyFitness stores all information in a private PostgreSQL database protected by native Row-Level Security (RLS), ensuring zero third-party telemetry leaks.

  1. Consolidated Health Metrics:

Instead of isolating diet from physical activity, SparkyFitness unifies calorie and macro intake, hydration, daily check-ins (weight, body fat percentage, steps), sleep stages, resting heart rate, and prescription medication schedules into synchronized analytical dashboards.

  1. Multi-User Household Support:

SparkyFitness supports multi-user family accounts with granular permission tiers. Family members can share recipes, meals, and workout routines while keeping personal biometric records completely isolated.

  1. Three-Tier Container Architecture:

The production deployment consists of three lightweight Docker services: - Frontend (sparkyfitness-frontend): A modern React 19 Single Page Application (SPA) bundled with Vite, Tailwind CSS, Radix UI primitives, and served via an optimized Nginx reverse proxy listening on port 80 (mapped to host port 3000). The proxy transparently routes /api requests to the backend. - Backend Server (sparkyfitness-server): A Node.js / Express v5 REST API handling authentication (via Better Auth with session tokens, passkeys, and MFA support), data validation, and database operations on internal port 3010. - Database (sparkyfitness-db): PostgreSQL 18 (postgres:18.3-alpine) managing persistent application records and user data.

  1. Open Integrations & Bring-Your-Own-LLM:

Features a built-in Model Context Protocol (MCP) server alongside support for external AI providers (OpenAI, Anthropic, Gemini) for natural language meal and health logging.

---

Prerequisites

Before deploying SparkyFitness, ensure your host environment meets the following specifications:

  • Host Operating System: Linux (Ubuntu, Debian, Fedora, Arch, Rocky, Alpine), macOS, or Windows via WSL2.
  • Container Runtime: Docker Engine 20.10+ with Docker Compose v2 (docker compose version v2.20+ recommended).
  • System Resources: Minimum 2 CPU cores and 2 GB available RAM to comfortably run PostgreSQL, the Express API, and the Nginx web frontend.
  • Network Ports: A free host TCP port (defaulting to 3000, or fallback ports such as 3004, 3001, 8080).
  • Host Utilities: Standard POSIX utilities including bash, curl, and openssl (for generating cryptographic secrets). No Node.js, Python, or database engines are required on the host system.

---

Exact Install Steps

The deployment uses the official multi-architecture container images (codewithcj/sparkyfitness:latest, codewithcj/sparkyfitness_server:latest, and postgres:18.3-alpine) configured with automated database healthcheck gating, cryptographically generated security keys, and persistent volume storage.

Step 1: Create the Project Directory and Storage Layout

Create a dedicated directory for your SparkyFitness stack and set up the persistent storage folders:

mkdir sparkyfitness && cd sparkyfitness
mkdir -p postgresql backup uploads
chmod -R 777 backup uploads

Permissions Note: The backup and uploads directories are given permissive write permissions so the backend container process can store user attachments and database dumps. Do not run chmod 777 on the postgresql/ directory; PostgreSQL 18 initializes and manages its own internal UID ownership (postgres:postgres) inside postgresql/18/.

Step 2: Generate Cryptographic Secrets and Configure .env

SparkyFitness enforces strict preflight validation during startup. Placeholder values like changeme or keys with incorrect byte lengths will trigger fatal server termination.

Specifically:

  • SPARKY_FITNESS_API_ENCRYPTION_KEY must be a 64-character hex string (32 random bytes).
  • BETTER_AUTH_SECRET must be a 44-character base64 string (32 random bytes).
  • SPARKY_FITNESS_FRONTEND_URL must match the URL where users access the web interface.

Generate cryptographic secrets and save them to .env:

DB_PASS=$(openssl rand -hex 16)
APP_DB_PASS=$(openssl rand -hex 16)
ENC_KEY=$(openssl rand -hex 32)
AUTH_SECRET=$(openssl rand -base64 32)

cat <<EOF > .env
PROJ=sparkyfitness
SPARKY_PORT=3000
SPARKY_FITNESS_DB_NAME=sparkyfitness_db
SPARKY_FITNESS_DB_USER=sparky
SPARKY_FITNESS_DB_PASSWORD=${DB_PASS}
SPARKY_FITNESS_APP_DB_USER=sparky_app
SPARKY_FITNESS_APP_DB_PASSWORD=${APP_DB_PASS}
SPARKY_FITNESS_API_ENCRYPTION_KEY=${ENC_KEY}
BETTER_AUTH_SECRET=${AUTH_SECRET}
SPARKY_FITNESS_FRONTEND_URL=http://localhost:3000
SPARKY_FITNESS_ADMIN_EMAIL=admin@example.com
SPARKY_FITNESS_DISABLE_SIGNUP=false
SPARKY_FITNESS_DEMO_MODE=false
NODE_ENV=production
TZ=UTC
EOF

chmod 600 .env

Step 3: Create the Docker Compose Specification

Create docker-compose.yml in the project root:

cat <<'EOF' > docker-compose.yml
services:
  sparkyfitness-db:
    image: postgres:18.3-alpine
    restart: always
    environment:
      POSTGRES_DB: ${SPARKY_FITNESS_DB_NAME:-sparkyfitness_db}
      POSTGRES_USER: ${SPARKY_FITNESS_DB_USER:-sparky}
      POSTGRES_PASSWORD: ${SPARKY_FITNESS_DB_PASSWORD}
    volumes:
      - ./postgresql:/var/lib/postgresql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 20
      start_period: 15s
    networks:
      - sparkyfitness-network

  sparkyfitness-server:
    image: codewithcj/sparkyfitness_server:latest
    restart: always
    stop_grace_period: 20s
    environment:
      SPARKY_FITNESS_DB_HOST: sparkyfitness-db
      SPARKY_FITNESS_DB_PORT: 5432
      SPARKY_FITNESS_DB_NAME: ${SPARKY_FITNESS_DB_NAME:-sparkyfitness_db}
      SPARKY_FITNESS_DB_USER: ${SPARKY_FITNESS_DB_USER:-sparky}
      SPARKY_FITNESS_DB_PASSWORD: ${SPARKY_FITNESS_DB_PASSWORD}
      SPARKY_FITNESS_APP_DB_USER: ${SPARKY_FITNESS_APP_DB_USER:-sparky_app}
      SPARKY_FITNESS_APP_DB_PASSWORD: ${SPARKY_FITNESS_APP_DB_PASSWORD}
      SPARKY_FITNESS_FRONTEND_URL: ${SPARKY_FITNESS_FRONTEND_URL}
      SPARKY_FITNESS_API_ENCRYPTION_KEY: ${SPARKY_FITNESS_API_ENCRYPTION_KEY}
      BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
      SPARKY_FITNESS_ADMIN_EMAIL: ${SPARKY_FITNESS_ADMIN_EMAIL:-admin@example.com}
      SPARKY_FITNESS_DISABLE_SIGNUP: ${SPARKY_FITNESS_DISABLE_SIGNUP:-false}
      SPARKY_FITNESS_DEMO_MODE: ${SPARKY_FITNESS_DEMO_MODE:-false}
      SPARKY_FITNESS_SERVER_PORT: 3010
      NODE_ENV: production
      TZ: UTC
    depends_on:
      sparkyfitness-db:
        condition: service_healthy
    volumes:
      - ./backup:/app/SparkyFitnessServer/backup
      - ./uploads:/app/SparkyFitnessServer/uploads
    networks:
      - sparkyfitness-network

  sparkyfitness-frontend:
    image: codewithcj/sparkyfitness:latest
    restart: always
    ports:
      - "${SPARKY_PORT:-3000}:80"
    environment:
      SPARKY_FITNESS_FRONTEND_URL: ${SPARKY_FITNESS_FRONTEND_URL}
      SPARKY_FITNESS_SERVER_HOST: sparkyfitness-server
      SPARKY_FITNESS_SERVER_PORT: 3010
      NGINX_RATE_LIMIT: 20r/s
    depends_on:
      - sparkyfitness-server
    networks:
      - sparkyfitness-network

networks:
  sparkyfitness-network:
    driver: bridge
EOF

Step 4: Validate and Launch the Stack

Validate the Compose configuration and launch the containers:

docker compose --env-file .env config --quiet && echo "Compose configuration syntax is valid"
docker compose --env-file .env up -d

Expected output:

Compose configuration syntax is valid
[+] Running 4/4
 ✔ Network sparkyfitness_sparkyfitness-network  Created
 ✔ Container sparkyfitness-sparkyfitness-db-1        Healthy
 ✔ Container sparkyfitness-sparkyfitness-server-1    Started
 ✔ Container sparkyfitness-sparkyfitness-frontend-1  Started

Step 5: Monitor Startup and Health Status

Poll the unauthenticated health endpoint until the server reports operational status:

until [ "$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/api/health)" = "200" ]; do
  echo "Waiting for SparkyFitness backend startup..."
  sleep 2
done
echo "SparkyFitness is healthy and ready!"

Inspect the backend logs to confirm database migrations and services initialized cleanly:

docker compose logs --tail 20 sparkyfitness-server

Expected log markers:

SparkyFitnessServer listening on port 3010
Better Auth mounted
Default backup settings created

Check running container status:

docker compose ps

Verify that sparkyfitness-db reports healthy and that sparkyfitness-server and sparkyfitness-frontend report Up.

Step 6: Seed Initial Administrator Account via REST API

You can seed the initial administrative account directly via the Better Auth REST API. Note that the request includes the Origin header to satisfy Better Auth's trusted-origin check:

curl -s -X POST "http://127.0.0.1:3000/api/auth/sign-up/email" \
  -H "Content-Type: application/json" \
  -H "Origin: http://localhost:3000" \
  -d '{
    "name": "Sparky Admin",
    "email": "admin@example.com",
    "password": "Password123!"
  }'

Expected response confirms user creation with "role":"admin".

---

Browser Setup & First Run

Once the stack is healthy, open http://localhost:3000/ in your web browser.

1. Sign In to the Dashboard

Navigate to http://localhost:3000/login:

SparkyFitness Login Screen

Log in using the seeded administrator credentials:

  • Email: admin@example.com
  • Password: Password123!

Click Sign In. (If you did not seed an account via API, click the Sign Up tab to register your initial administrator account).

2. Dismiss Announcement Modal

Upon first sign-in, SparkyFitness displays a release announcement modal informing users of the Android APK release:

Announcement Modal

Click "Got it, don't show again" to dismiss the modal and proceed to your dashboard.

3. Onboarding Wizard

If the initial profile setup wizard appears, fill in your physical parameters (height, birthdate, activity level) or click Skip to jump straight into the application. (The choice is saved to the server for subsequent logins).

4. Explore the Daily Diary Dashboard

You arrive at your central daily dashboard (/):

SparkyFitness Diary Dashboard Full

The Diary dashboard summarizes your health metrics for the day:

  • Energy Ring: Shows your daily caloric target, consumed calories, and remaining budget (e.g. 2000 remaining kcal).
  • Nutrition Summary Grid: Visual progress bars for Protein, Carbohydrates, Fat, and Dietary Fiber.
  • Hydration & Micronutrient Cards: Quick tracking for Water, Caffeine, and Alcohol consumption.

5. Log Water Intake

Click the + button on the Water Intake card:

Water Intake Logged

The water bottle animation updates immediately, and a confirmation toast "Water intake updated" appears in the top-right corner.

6. Daily Check-In (/checkin)

Click Check-In in the navigation bar (or visit /checkin):

Daily Check-In View

The Check-In module provides comprehensive biometric tracking organized into dedicated tabs:

  • Measurements: Body weight, step counts, waist/hip circumferences, and body fat percentage calculator.
  • Fasting & Mood: Fasting timer logs and daily subjective mood tracking.
  • Mindfulness & Sleep: Sleep duration, sleep quality ratings, and meditation minutes.
  • Photos & Symptoms: Body composition progress photography and custom symptom logs.

7. Medications, Foods & Exercises Catalogs

Explore the dedicated tracking modules from the main navigation menu:

  • Medications (/medications): Set up prescription and supplement schedules with dosage reminders and inventory tracking:

Medications Screen

  • Foods (/foods): View your custom food database and nutritional items:

Foods Screen

  • Exercises (/exercises): Browse workout routines, strength exercises, and endurance templates:

Exercises Catalog

Note on Initial Catalogs: On a fresh standalone deployment, the Foods and Exercises libraries start empty (No results found). You can add custom items immediately or import public libraries (such as the Free Exercise DB or OpenFoodFacts) via the settings interface.

8. Goals & Target Configuration

Navigate to Goals (/goals):

Goals Configuration

Here you can customize daily energy budgets, macronutrient splits (grams or percentage distributions), minimum water targets, and weekly exercise frequency goals.

9. Biometric Reports & Long-Term Trends

Navigate to Reports (/reports):

Reports and Analytics

The Reports engine aggregates your logged activity across multiple dimensions:

  • Nutrients & Caloric Deficit: Macro adherence trends over 7-day, 30-day, or custom date ranges.
  • Hydration Chart: Visualizes daily water intake over time (the water logged in Step 5 appears immediately on the chart).
  • Heart Rate & Sleep Stages: Synchronized wearable metrics and long-term recovery curves.

10. System Settings & Customization

Navigate to Settings (/settings):

Settings Screen

Configure user preferences, metric versus imperial measurement units, UI themes (light and dark modes), multi-factor authentication (passkeys and TOTP), and database backup downloads.

---

Verification Checklist

Run these terminal commands to verify your deployment is healthy, authenticated, and storing data correctly:

  1. Verify Backend Health Endpoint:

``sh curl -s http://localhost:3000/api/health ` *Expected Output:* {"status":"UP"}`

  1. Verify Frontend Reverse Proxy Response:

``sh curl -sI http://localhost:3000/ | head -n 5 ` *Expected Output:* HTTP/1.1 200 OK with Content-Type: text/html`.

  1. Verify Authentication & Session Security:

```sh # Valid credentials return 200 with session details: curl -s -X POST http://localhost:3000/api/auth/sign-in/email \ -H "Content-Type: application/json" \ -H "Origin: http://localhost:3000" \ -d '{"email":"admin@example.com","password":"Password123!"}' | jq -r '.user.email' # Expected: admin@example.com

# Invalid credentials return 401 Unauthorized: curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:3000/api/auth/sign-in/email \ -H "Content-Type: application/json" \ -H "Origin: http://localhost:3000" \ -d '{"email":"admin@example.com","password":"WrongPassword"}' # Expected: 401 ```

  1. Verify Container Health & Running State:

``sh docker compose ps ` *Expected Output:* All three containers (sparkyfitness-db, sparkyfitness-server, sparkyfitness-frontend) report Up with sparkyfitness-db marked (healthy)`.

  1. Verify Persistent Storage Creation:

``sh ls -la postgresql/ backup/ uploads/ `` *Expected Output:* Host directories exist and contain initialized PostgreSQL database files and application asset folders.

---

Troubleshooting

The table below outlines common issues encountered when deploying SparkyFitness and their verified solutions:

Symptom / ErrorRoot CauseSolution
Bind for 0.0.0.0:3000 failed: port is already allocatedAnother host service (e.g. Grafana or Mealie) is already bound to port 3000.Update SPARKY_PORT in .env (e.g. SPARKY_PORT=3004), update SPARKY_FITNESS_FRONTEND_URL=http://localhost:3004, and run docker compose up -d.
Fatal backend crash: Preflight check failed: invalid encryption keySPARKY_FITNESS_API_ENCRYPTION_KEY is missing or not a 64-character hex string.Generate a valid 32-byte hex key: ENC_KEY=$(openssl rand -hex 32) and update .env. Ensure length is exactly 64 hex characters.
Sign-up or sign-in returns 403 Forbidden or CORS errorBetter Auth origin verification failed because SPARKY_FITNESS_FRONTEND_URL does not match the incoming request origin.Verify SPARKY_FITNESS_FRONTEND_URL in .env matches your browser URL (including protocol and port). When curling the API, pass -H "Origin: http://localhost:3000".
Backend logs show Waiting for database... and connection failssparkyfitness-server attempted to connect before PostgreSQL completed initialization.Ensure depends_on in docker-compose.yml specifies condition: service_healthy for sparkyfitness-db so the server waits for pg_isready.
Browser navigates to /check-in and returns a 404 pageThe frontend router registers the Check-In route as /checkin (without hyphen).Use /checkin in your browser address bar or click the Check-In button in the navigation header.
Exercises or Foods view shows No results foundA fresh installation contains an empty database without third-party seed libraries.Create items manually or use the external data import tools in Settings to import the Free Exercise DB or OpenFoodFacts catalog.
rm -rf postgresql fails with Permission denied during cleanupPostgreSQL writes database cluster files owned by the container root/postgres UID.Remove root-owned files using an ephemeral Docker container: docker run --rm -v $(pwd):/d alpine rm -rf /d/postgresql /d/backup /d/uploads.
SparkyChat AI assistant widget does not appear in the interfaceNo AI provider API keys (OpenAI, Anthropic, Gemini) are configured in the environment.SparkyChat renders null when AI service keys are absent. Add provider API keys to .env if conversational meal logging is required.
Nginx rate limit exceeded (429 Too Many Requests)Rapid API requests or automated browser testing exceeded the default Nginx rate limit.Set NGINX_RATE_LIMIT: 20r/s in docker-compose.yml under sparkyfitness-frontend and run docker compose up -d.

---

Teardown & Clean Up

To stop the SparkyFitness stack and safely clean up resources:

# 1. Stop and remove containers and network
docker compose down

# 2. To remove containers, networks, volumes, and persistent host files completely:
docker compose down -v --remove-orphans

# Clean up root-owned PostgreSQL files using an ephemeral container before removing local directory:
docker run --rm -v "$(pwd)":/d alpine rm -rf /d/postgresql /d/backup /d/uploads
rm -rf .env docker-compose.yml

---

Assumptions & decisions

  • License & Commercial Scope: SparkyFitness is distributed under a custom source-available license that is free for personal, non-commercial self-hosting, but requires explicit permission for commercial use. This distinction is clearly reflected in the post overview and metadata.
  • Port Selection: Tested and validated on host port 3000 mapped to Nginx port 80. An environment variable SPARKY_PORT is exposed in .env to facilitate easy re-mapping if port 3000 is already bound by another homelab service.
  • Security Preflight Enforcement: Upstream's preflightChecks.ts unconditionally crashes the backend server if default placeholder keys are detected or if the encryption key is not a 32-byte (64 hex character) string. The installation steps use openssl rand to generate verified cryptographically secure secrets.
  • Frontend Reverse Proxy & Route Prefix: The deployment uses the official three-container architecture where sparkyfitness-frontend serves both static React SPA assets and acts as an Nginx reverse proxy routing /api traffic to sparkyfitness-server:3010.
  • Route Validation: The plan listed /check-in, but runtime router verification confirmed the route is /checkin. The guide documents /checkin to prevent user 404 errors.
  • Origin Header Requirement: Better Auth validates request origins. When interacting with the API programmatically (e.g. during administrative account seeding), the Origin header must match SPARKY_FITNESS_FRONTEND_URL.
  • Initial Catalog Empty States: Disclosed that the Foods and Exercises libraries start empty on initial deployment, avoiding false claims about pre-populated databases while explaining how to import them.
  • SparkyChat Optional Dependency: Noted that SparkyChat is conditionally rendered only when external AI provider keys are supplied, confirming that its absence on initial startup is expected behavior rather than a defect.
  • PostgreSQL File Permissions: Avoided invalid chmod 777 on the postgresql/ directory, which can conflict with PostgreSQL internal security checks, while documenting the ephemeral container technique required to clean up root-owned database volumes during teardown.
  • Ghost HTML Card Compatibility: Formatted all troubleshooting and comparison matrices using GitHub-flavored Markdown table syntax, ensuring seamless automatic conversion into Ghost HTML cards (ghostHtmlCard) by ghost-draft.
  • Screenshot Asset Paths: Embedded authentic relative screenshot paths generated during the verified Playwright automation run (runs/sparkyfitness-20261101-055322/browser/*.png).