name: sparkyfitness x-daemonless: class: "service" title: "SparkyFitness" icon: ":material-heart-pulse:" category: "Productivity" description: "Self-hosted privacy-first fitness tracker on FreeBSD." upstream_url: "https://github.com/CodeWithCJ/SparkyFitness" web_url: "https://github.com/CodeWithCJ/SparkyFitness" user: "bsd" mlock: false # sparkyfitness is the one people open; its database stays on the stack's # private network, where it is found by the name fjord puts in # SPARKY_FITNESS_DB_HOST (127.0.0.1 on host networking). networking: sparkyfitness: default "*": private hostnames: sparkyfitness-db: SPARKY_FITNESS_DB_HOST # Sidecar for the AppJail director bundle, named as the service is so fjord # finds its jail by the service's name. appjail: director: options: - alias: - ip4_inherit: depends_on: - name: sparkyfitness-db image: ghcr.io/daemonless/postgres:18 template: postgres-template.conf env: 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 docs: env: PUID: "User ID for the application process" PGID: "Group ID for the application process" TZ: "Timezone for the container" UPLOADS_LOCATION: "Path to store user uploads (profile pictures, exercise images)" BACKUPS_LOCATION: "Path to store server backups" DB_DATA_LOCATION: "Path to store PostgreSQL data files" SPARKY_FITNESS_DB_NAME: "PostgreSQL database name" SPARKY_FITNESS_DB_USER: "PostgreSQL superuser" SPARKY_FITNESS_DB_PASSWORD: "PostgreSQL password" SPARKY_FITNESS_API_ENCRYPTION_KEY: "64-char hex encryption key" BETTER_AUTH_SECRET: "Auth session signing secret" SPARKY_FITNESS_FRONTEND_URL: desc: "URL you will open this app at, e.g. http://myhost:3004 (sign-up fails if it does not match)" public_url: true # fjord fills in where the app lands NODE_ENV: "Node runtime mode; leave as 'production'" SPARKY_FITNESS_DB_HOST: desc: "Where the backend finds PostgreSQL (fjord sets this; 127.0.0.1 on host networking)" level: advanced optional: true SPARKY_FITNESS_DB_PORT: "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_APP_DB_USER: "Limited app DB role the backend creates during migrations" SPARKY_FITNESS_APP_DB_PASSWORD: "Password for the limited app DB role" SPARKY_FITNESS_SERVER_HOST: "Internal bind address for the node backend; leave as 127.0.0.1 (nginx proxies to it)" SPARKY_FITNESS_SERVER_PORT: "Internal node backend port; leave as default" SPARKY_FITNESS_LOG_LEVEL: "Backend log verbosity (e.g. ERROR, INFO, DEBUG)" volumes: /app/server/uploads: "User-uploaded files (profile pictures, exercise images)" /app/server/backup: "Server backup data" ports: 3004: "Web UI (nginx reverse proxy)" 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