# SparkyFitness [![Build Status](https://img.shields.io/github/actions/workflow/status/daemonless/sparkyfitness/build.yaml?style=flat-square&label=Build&color=green)](https://github.com/daemonless/sparkyfitness/actions) [![Last Commit](https://img.shields.io/github/last-commit/daemonless/sparkyfitness?style=flat-square&label=Last+Commit&color=blue)](https://github.com/daemonless/sparkyfitness/commits) [![OCI Pulls](https://img.shields.io/docker/pulls/daemonless/sparkyfitness?style=flat-square&label=OCI+Pulls&color=blue)](https://hub.docker.com/r/daemonless/sparkyfitness) Self-hosted privacy-first fitness tracker on FreeBSD. | | | |---|---| | **Registry** | `ghcr.io/daemonless/sparkyfitness` | | **Source** | [https://github.com/CodeWithCJ/SparkyFitness](https://github.com/CodeWithCJ/SparkyFitness) | | **Website** | [https://github.com/CodeWithCJ/SparkyFitness](https://github.com/CodeWithCJ/SparkyFitness) | ## Version Tags | Tag | Description | Best For | | :--- | :--- | :--- | | `latest` | **Upstream Binary**. Built from official release. | Most users — recommended. | ## Parts | Service | Image | Role | | |---|---|---|---| | **sparkyfitness** | `ghcr.io/daemonless/sparkyfitness:${SPARKYFITNESS_TAG:-latest}` | | [docs](https://github.com/daemonless/sparkyfitness) | | **sparkyfitness-db** | `ghcr.io/daemonless/postgres:18` | | [docs](https://github.com/daemonless/postgres) | ## Prerequisites Before deploying, ensure your host environment is ready. See the [Quick Start Guide](https://daemonless.io/guides/quick-start) for host setup instructions. ## Deployment ### Podman Compose **1.** Save as `.env` and fill in what is empty: ```env { data-zip-bundle="sparkyfitness-podman" data-zip-filename=".env" } # SparkyFitness for FreeBSD (Daemonless) # Copy to .env and edit. # Timezone (TZ identifier) # TZ=UTC # Where data lives on the host UPLOADS_LOCATION=/containers/sparkyfitness/uploads BACKUPS_LOCATION=/containers/sparkyfitness/backups DB_DATA_LOCATION=/containers/sparkyfitness/postgres # PostgreSQL superuser + database (provisioned by the sidecar) SPARKY_FITNESS_DB_NAME=sparkyfitness SPARKY_FITNESS_DB_USER=sparky # Change this to a random password! A-Za-z0-9 only. SPARKY_FITNESS_DB_PASSWORD=sparky # Limited app role the backend creates during migrations SPARKY_FITNESS_APP_DB_USER=sparky_app SPARKY_FITNESS_APP_DB_PASSWORD=sparky_app # Secrets: no defaults on purpose -- these become the catalog's install # defaults, and a shipped value would be shared by every install. Generate # your own, e.g. `openssl rand -hex 32` for each. SPARKY_FITNESS_API_ENCRYPTION_KEY= BETTER_AUTH_SECRET= # Public URL the browser uses to reach the app, e.g. http://myhost:3004. # No default on purpose: Better Auth compares this against the request origin # and rejects the sign-up POST when it does not match, so a shipped default is # wrong for everyone who does not browse from the server itself. SPARKY_FITNESS_FRONTEND_URL= ``` **2.** Save as `compose.yaml`: ```yaml { data-zip-bundle="sparkyfitness-podman" data-zip-filename="compose.yaml" } name: sparkyfitness services: sparkyfitness: # Tag in a ${VAR} so the catalog exposes it as an image_tag variable and the # install wizard can offer this repo's published versions. The database is # pinned deliberately: retagging it would be a destructive migration. image: ghcr.io/daemonless/sparkyfitness:${SPARKYFITNESS_TAG:-latest} container_name: sparkyfitness restart: unless-stopped network_mode: host depends_on: - sparkyfitness-db environment: - PUID=1000 - PGID=1000 - TZ=${TZ:-UTC} - NODE_ENV=production - SPARKY_FITNESS_DB_HOST=${SPARKY_FITNESS_DB_HOST:-127.0.0.1} - SPARKY_FITNESS_DB_PORT=5433 - SPARKY_FITNESS_DB_NAME=${SPARKY_FITNESS_DB_NAME} - SPARKY_FITNESS_DB_USER=${SPARKY_FITNESS_DB_USER} - SPARKY_FITNESS_DB_PASSWORD=${SPARKY_FITNESS_DB_PASSWORD} - SPARKY_FITNESS_APP_DB_USER=${SPARKY_FITNESS_APP_DB_USER} - SPARKY_FITNESS_APP_DB_PASSWORD=${SPARKY_FITNESS_APP_DB_PASSWORD} - SPARKY_FITNESS_API_ENCRYPTION_KEY=${SPARKY_FITNESS_API_ENCRYPTION_KEY} - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} - SPARKY_FITNESS_FRONTEND_URL=${SPARKY_FITNESS_FRONTEND_URL} - SPARKY_FITNESS_SERVER_HOST=127.0.0.1 - SPARKY_FITNESS_SERVER_PORT=3010 - SPARKY_FITNESS_LOG_LEVEL=ERROR volumes: - ${UPLOADS_LOCATION}:/app/server/uploads - ${BACKUPS_LOCATION}:/app/server/backup sparkyfitness-db: image: ghcr.io/daemonless/postgres:18 container_name: sparkyfitness-db restart: unless-stopped network_mode: host annotations: org.freebsd.jail.allow.sysvipc: "true" # Generic daemonless PostgreSQL 18 image. Only the superuser role + database # are provisioned here; the SparkyFitness backend creates its own limited # app role (SPARKY_FITNESS_APP_DB_USER) during migrations. environment: - POSTGRES_DB=${SPARKY_FITNESS_DB_NAME} - POSTGRES_USER=${SPARKY_FITNESS_DB_USER} - POSTGRES_PASSWORD=${SPARKY_FITNESS_DB_PASSWORD} # Distinct port (NOT 5432) so this can run alongside another host-networked Postgres # (e.g. Immich on 5432) without colliding. ⚠ Requires the daemonless/postgres image to # honor POSTGRES_PORT — if it doesn't, it falls back to 5432 and WILL collide. Two # host-net Postgres must never share a port. - POSTGRES_PORT=5433 volumes: - ${DB_DATA_LOCATION}:/var/lib/postgresql/data ``` Then run `podman-compose up -d`. ### AppJail Director **.env**: ``` # .env DIRECTOR_PROJECT=sparkyfitness PUID=1000 PGID=1000 TZ=UTC NODE_ENV=production SPARKY_FITNESS_DB_HOST=127.0.0.1 SPARKY_FITNESS_DB_PORT=5433 SPARKY_FITNESS_DB_NAME=sparkyfitness SPARKY_FITNESS_DB_USER=sparky SPARKY_FITNESS_DB_PASSWORD=sparky SPARKY_FITNESS_APP_DB_USER=sparky_app SPARKY_FITNESS_APP_DB_PASSWORD=sparky_app SPARKY_FITNESS_API_ENCRYPTION_KEY= BETTER_AUTH_SECRET= SPARKY_FITNESS_FRONTEND_URL= SPARKY_FITNESS_SERVER_HOST=127.0.0.1 SPARKY_FITNESS_SERVER_PORT=3010 SPARKY_FITNESS_LOG_LEVEL=ERROR UPLOADS_LOCATION=/containers/sparkyfitness/uploads BACKUPS_LOCATION=/containers/sparkyfitness/backups DB_DATA_LOCATION=/containers/sparkyfitness/postgres ``` **appjail-director.yml**: ```yaml # appjail-director.yml options: - alias: - ip4_inherit: services: sparkyfitness: name: sparkyfitness options: - container: 'args:--pull' oci: user: root environment: - PUID: !ENV '${PUID}' - PGID: !ENV '${PGID}' - TZ: !ENV '${TZ}' - NODE_ENV: !ENV '${NODE_ENV}' - SPARKY_FITNESS_DB_HOST: !ENV '${SPARKY_FITNESS_DB_HOST}' - SPARKY_FITNESS_DB_PORT: !ENV '${SPARKY_FITNESS_DB_PORT}' - SPARKY_FITNESS_DB_NAME: !ENV '${SPARKY_FITNESS_DB_NAME}' - SPARKY_FITNESS_DB_USER: !ENV '${SPARKY_FITNESS_DB_USER}' - SPARKY_FITNESS_DB_PASSWORD: !ENV '${SPARKY_FITNESS_DB_PASSWORD}' - SPARKY_FITNESS_APP_DB_USER: !ENV '${SPARKY_FITNESS_APP_DB_USER}' - SPARKY_FITNESS_APP_DB_PASSWORD: !ENV '${SPARKY_FITNESS_APP_DB_PASSWORD}' - SPARKY_FITNESS_API_ENCRYPTION_KEY: !ENV '${SPARKY_FITNESS_API_ENCRYPTION_KEY}' - BETTER_AUTH_SECRET: !ENV '${BETTER_AUTH_SECRET}' - SPARKY_FITNESS_FRONTEND_URL: !ENV '${SPARKY_FITNESS_FRONTEND_URL}' - SPARKY_FITNESS_SERVER_HOST: !ENV '${SPARKY_FITNESS_SERVER_HOST}' - SPARKY_FITNESS_SERVER_PORT: !ENV '${SPARKY_FITNESS_SERVER_PORT}' - SPARKY_FITNESS_LOG_LEVEL: !ENV '${SPARKY_FITNESS_LOG_LEVEL}' - UPLOADS_LOCATION: !ENV '${UPLOADS_LOCATION}' - BACKUPS_LOCATION: !ENV '${BACKUPS_LOCATION}' - DB_DATA_LOCATION: !ENV '${DB_DATA_LOCATION}' volumes: - sparkyfitness_uploads: /app/server/uploads - sparkyfitness_backup: /app/server/backup sparkyfitness-db: name: sparkyfitness_db options: - from: ghcr.io/daemonless/postgres:18 - template: !ENV '${PWD}/postgres-template.conf' oci: environment: - POSTGRES_DB: !ENV '${SPARKY_FITNESS_DB_NAME}' - POSTGRES_USER: !ENV '${SPARKY_FITNESS_DB_USER}' - POSTGRES_PASSWORD: !ENV '${SPARKY_FITNESS_DB_PASSWORD}' - POSTGRES_PORT: "5433" volumes: - db_data: /var/lib/postgresql/data volumes: sparkyfitness_uploads: device: '/containers/sparkyfitness/uploads' sparkyfitness_backup: device: '/containers/sparkyfitness/backup' db_data: device: !ENV '${DB_DATA_LOCATION}' ``` **Makejail**: ``` # Makejail ARG tag=latest OPTION container=boot OPTION overwrite=force OPTION from=ghcr.io/daemonless/sparkyfitness:${tag} ``` Save the files above, then run `appjail-director up`. ## Parameters ### Environment Variables | Variable | Default | Description | |----------|---------|-------------| | `PUID` | `1000` | User ID for the application process | | `PGID` | `1000` | Group ID for the application process | | `TZ` | `UTC` | Timezone for the container | | `NODE_ENV` | `production` | Node runtime mode; leave as 'production' | | `SPARKY_FITNESS_DB_HOST` | `127.0.0.1` | Where the backend finds PostgreSQL (fjord sets this; 127.0.0.1 on host networking) | | `SPARKY_FITNESS_DB_PORT` | `5433` | PostgreSQL port; MUST match the sidecar's POSTGRES_PORT. Default 5433 (NOT 5432) so this can coexist with another host-networked Postgres (e.g. Immich on 5432) on the same host. ⚠ With network_mode: host, two Postgres on the same port silently collide — keep each service on a distinct port. NOTE: this only takes effect if the daemonless/postgres image honors POSTGRES_PORT (see project README / upstream fix); otherwise the sidecar falls back to 5432. | | `SPARKY_FITNESS_DB_NAME` | `sparkyfitness` | PostgreSQL database name | | `SPARKY_FITNESS_DB_USER` | `sparky` | PostgreSQL superuser | | `SPARKY_FITNESS_DB_PASSWORD` | `sparky` | PostgreSQL password | | `SPARKY_FITNESS_APP_DB_USER` | `sparky_app` | Limited app DB role the backend creates during migrations | | `SPARKY_FITNESS_APP_DB_PASSWORD` | `sparky_app` | Password for the limited app DB role | | `SPARKY_FITNESS_API_ENCRYPTION_KEY` | `` | 64-char hex encryption key | | `BETTER_AUTH_SECRET` | `` | Auth session signing secret | | `SPARKY_FITNESS_FRONTEND_URL` | `` | URL you will open this app at, e.g. http://myhost:3004 (sign-up fails if it does not match) | | `SPARKY_FITNESS_SERVER_HOST` | `127.0.0.1` | Internal bind address for the node backend; leave as 127.0.0.1 (nginx proxies to it) | | `SPARKY_FITNESS_SERVER_PORT` | `3010` | Internal node backend port; leave as default | | `SPARKY_FITNESS_LOG_LEVEL` | `ERROR` | Backend log verbosity (e.g. ERROR, INFO, DEBUG) | | `UPLOADS_LOCATION` | `/containers/sparkyfitness/uploads` | Path to store user uploads (profile pictures, exercise images) | | `BACKUPS_LOCATION` | `/containers/sparkyfitness/backups` | Path to store server backups | | `DB_DATA_LOCATION` | `/containers/sparkyfitness/postgres` | Path to store PostgreSQL data files | ### Volumes | Path | Description | |------|-------------| | `/app/server/uploads` | User-uploaded files (profile pictures, exercise images) | | `/app/server/backup` | Server backup data | **Architectures:** amd64 **User:** `bsd` (UID/GID via PUID/PGID, defaults to 1000:1000) **Base:** FreeBSD 15 --- Need help? Join our [Discord](https://discord.gg/Kb9tkhecZT) community.