Skip to content

SparkyFitness SparkyFitness

Description / nameInput element
Container Registry
Container Configuration Root Path
Timezone
User ID
Group ID
SparkyFitness /uploads Path
SparkyFitness /backups Path

Build Status Last Commit

Self-hosted privacy-first fitness tracker on FreeBSD.

Registry ghcr.io/daemonless/sparkyfitness
Daemonless daemonless/sparkyfitness
Source CodeWithCJ/SparkyFitness
Website github.com/CodeWithCJ/SparkyFitness

Version Tags

Tag Description Best For
latest Upstream Binary. Built from official release. Most users — recommended.

Before deploying, ensure your host environment is ready. See the Quick Start Guide for host setup instructions, including the security model for host vs. container privileges.

Deployment

services:
  sparkyfitness:
    image: "ghcr.io/daemonless/sparkyfitness:latest"
    container_name: sparkyfitness
    environment:
      - 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  # PostgreSQL host the backend connects to; leave as 127.0.0.1 (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=${SPARKY_FITNESS_DB_NAME}  # PostgreSQL database name
      - SPARKY_FITNESS_DB_USER=${SPARKY_FITNESS_DB_USER}  # PostgreSQL superuser
      - SPARKY_FITNESS_DB_PASSWORD=${SPARKY_FITNESS_DB_PASSWORD}  # PostgreSQL password (from secrets.env)
      - SPARKY_FITNESS_APP_DB_USER=${SPARKY_FITNESS_APP_DB_USER}  # Limited app DB role the backend creates during migrations (from secrets)
      - SPARKY_FITNESS_APP_DB_PASSWORD=${SPARKY_FITNESS_APP_DB_PASSWORD}  # Password for the limited app DB role (from secrets)
      - SPARKY_FITNESS_API_ENCRYPTION_KEY=${SPARKY_FITNESS_API_ENCRYPTION_KEY}  # 64-char hex encryption key (from secrets.env)
      - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}  # Auth session signing secret (from secrets.env)
      - SPARKY_FITNESS_FRONTEND_URL=${SPARKY_FITNESS_FRONTEND_URL}  # Public URL of the frontend for CORS
      - 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)
    volumes:
      - "/path/to/containers/sparkyfitness/uploads:/uploads"
      - "/path/to/containers/sparkyfitness/backups:/backups"
    # always (not unless-stopped) so FreeBSD's podman rc.d auto-starts it at boot
    restart: always

Save as compose.yaml, then run podman-compose up -d.

podman run -d --name sparkyfitness \
  -e PUID=1000 \
  -e PGID=1000 \
  -e TZ=UTC \
  -e NODE_ENV=production \
  -e SPARKY_FITNESS_DB_HOST=127.0.0.1 \
  -e SPARKY_FITNESS_DB_PORT=5433 \
  -e SPARKY_FITNESS_DB_NAME=${SPARKY_FITNESS_DB_NAME} \
  -e SPARKY_FITNESS_DB_USER=${SPARKY_FITNESS_DB_USER} \
  -e SPARKY_FITNESS_DB_PASSWORD=${SPARKY_FITNESS_DB_PASSWORD} \
  -e SPARKY_FITNESS_APP_DB_USER=${SPARKY_FITNESS_APP_DB_USER} \
  -e SPARKY_FITNESS_APP_DB_PASSWORD=${SPARKY_FITNESS_APP_DB_PASSWORD} \
  -e SPARKY_FITNESS_API_ENCRYPTION_KEY=${SPARKY_FITNESS_API_ENCRYPTION_KEY} \
  -e BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} \
  -e SPARKY_FITNESS_FRONTEND_URL=${SPARKY_FITNESS_FRONTEND_URL} \
  -e SPARKY_FITNESS_SERVER_HOST=127.0.0.1 \
  -e SPARKY_FITNESS_SERVER_PORT=3010 \
  -e SPARKY_FITNESS_LOG_LEVEL=ERROR \
  -v /path/to/containers/sparkyfitness/uploads:/uploads \
  -v /path/to/containers/sparkyfitness/backups:/backups \
  ghcr.io/daemonless/sparkyfitness:latest

Save as run.sh, then run sh run.sh.

- name: Deploy sparkyfitness
  containers.podman.podman_container:
    name: sparkyfitness
    image: "ghcr.io/daemonless/sparkyfitness:latest"
    state: started
    restart_policy: always
    env:
      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: "${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:
      - "/path/to/containers/sparkyfitness/uploads:/uploads"
      - "/path/to/containers/sparkyfitness/backups:/backups"

Save as sparkyfitness-deploy.yaml, then run ansible-playbook sparkyfitness-deploy.yaml.

# .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=${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
# appjail-director.yml

options:
  - virtualnet: ':<random> default'
  - nat:
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}'
    volumes:
      - SPARKYFITNESS_UPLOADS_PATH: /uploads
      - SPARKYFITNESS_BACKUPS_PATH: /backups
volumes:
  SPARKYFITNESS_UPLOADS_PATH:
    device: '/path/to/containers/sparkyfitness/uploads'
  SPARKYFITNESS_BACKUPS_PATH:
    device: '/path/to/containers/sparkyfitness/backups'
1
2
3
4
5
6
7
# 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.

appjail oci run -Pd \
  -o overwrite=force \
  -o container="args:--pull" \
  -o virtualnet=":<random> default" \
  -o nat \
  -e PUID=1000 \
  -e PGID=1000 \
  -e TZ=UTC \
  -e NODE_ENV=production \
  -e SPARKY_FITNESS_DB_HOST=127.0.0.1 \
  -e SPARKY_FITNESS_DB_PORT=5433 \
  -e SPARKY_FITNESS_DB_NAME=${SPARKY_FITNESS_DB_NAME} \
  -e SPARKY_FITNESS_DB_USER=${SPARKY_FITNESS_DB_USER} \
  -e SPARKY_FITNESS_DB_PASSWORD=${SPARKY_FITNESS_DB_PASSWORD} \
  -e SPARKY_FITNESS_APP_DB_USER=${SPARKY_FITNESS_APP_DB_USER} \
  -e SPARKY_FITNESS_APP_DB_PASSWORD=${SPARKY_FITNESS_APP_DB_PASSWORD} \
  -e SPARKY_FITNESS_API_ENCRYPTION_KEY=${SPARKY_FITNESS_API_ENCRYPTION_KEY} \
  -e BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} \
  -e SPARKY_FITNESS_FRONTEND_URL=${SPARKY_FITNESS_FRONTEND_URL} \
  -e SPARKY_FITNESS_SERVER_HOST=127.0.0.1 \
  -e SPARKY_FITNESS_SERVER_PORT=3010 \
  -e SPARKY_FITNESS_LOG_LEVEL=ERROR \
  -o fstab="/path/to/containers/sparkyfitness/uploads /uploads <pseudofs>" \
  -o fstab="/path/to/containers/sparkyfitness/backups /backups <pseudofs>" \
  ghcr.io/daemonless/sparkyfitness:latest sparkyfitness

Save the files above, then run sh run.sh.

Experimental

Bastille's OCI support is experimental. It requires buildah and shares the host network stack (inherit). Mount volumes with --volume HOST JAIL; without it, image-declared volumes are stored under ${bastille_volumesdir}/${jail}.

services:
  sparkyfitness:
    name: sparkyfitness
    image: "ghcr.io/daemonless/sparkyfitness:latest"
    network:
      - mode: host
    environment:
      - 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=${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:
      - "/path/to/containers/sparkyfitness/uploads:/uploads"
      - "/path/to/containers/sparkyfitness/backups:/backups"

Save as bastille-compose.yml, then run bastille up.

bastille create -O \
  --env PUID=1000 \
  --env PGID=1000 \
  --env TZ=UTC \
  --env NODE_ENV=production \
  --env SPARKY_FITNESS_DB_HOST=127.0.0.1 \
  --env SPARKY_FITNESS_DB_PORT=5433 \
  --env SPARKY_FITNESS_DB_NAME=${SPARKY_FITNESS_DB_NAME} \
  --env SPARKY_FITNESS_DB_USER=${SPARKY_FITNESS_DB_USER} \
  --env SPARKY_FITNESS_DB_PASSWORD=${SPARKY_FITNESS_DB_PASSWORD} \
  --env SPARKY_FITNESS_APP_DB_USER=${SPARKY_FITNESS_APP_DB_USER} \
  --env SPARKY_FITNESS_APP_DB_PASSWORD=${SPARKY_FITNESS_APP_DB_PASSWORD} \
  --env SPARKY_FITNESS_API_ENCRYPTION_KEY=${SPARKY_FITNESS_API_ENCRYPTION_KEY} \
  --env BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} \
  --env SPARKY_FITNESS_FRONTEND_URL=${SPARKY_FITNESS_FRONTEND_URL} \
  --env SPARKY_FITNESS_SERVER_HOST=127.0.0.1 \
  --env SPARKY_FITNESS_SERVER_PORT=3010 \
  --env SPARKY_FITNESS_LOG_LEVEL=ERROR \
  --volume /path/to/containers/sparkyfitness/uploads /uploads \
  --volume /path/to/containers/sparkyfitness/backups /backups \
  sparkyfitness ghcr.io/daemonless/sparkyfitness:latest inherit

Interactive Configuration

Parameters

Environment Variables

Variable Default Description
PUID 1000 User ID for the application process
PGID 1000 Group ID for the application process
TZ ${TZ} Timezone for the container
NODE_ENV production Node runtime mode; leave as 'production'
SPARKY_FITNESS_DB_HOST 127.0.0.1 PostgreSQL host the backend connects to; leave as 127.0.0.1 (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 ${SPARKY_FITNESS_DB_NAME} PostgreSQL database name
SPARKY_FITNESS_DB_USER ${SPARKY_FITNESS_DB_USER} PostgreSQL superuser
SPARKY_FITNESS_DB_PASSWORD ${SPARKY_FITNESS_DB_PASSWORD} PostgreSQL password (from secrets.env)
SPARKY_FITNESS_APP_DB_USER ${SPARKY_FITNESS_APP_DB_USER} Limited app DB role the backend creates during migrations (from secrets)
SPARKY_FITNESS_APP_DB_PASSWORD ${SPARKY_FITNESS_APP_DB_PASSWORD} Password for the limited app DB role (from secrets)
SPARKY_FITNESS_API_ENCRYPTION_KEY ${SPARKY_FITNESS_API_ENCRYPTION_KEY} 64-char hex encryption key (from secrets.env)
BETTER_AUTH_SECRET ${BETTER_AUTH_SECRET} Auth session signing secret (from secrets.env)
SPARKY_FITNESS_FRONTEND_URL ${SPARKY_FITNESS_FRONTEND_URL} Public URL of the frontend for CORS
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)

Volumes

Path Description
/uploads User-uploaded files (profile pictures, exercise images)
/backups Server backup data

Implementation Details

  • Architectures: amd64
  • User: bsd (UID/GID set via PUID/PGID). Defaults to 1000:1000.
  • Base: Built on ghcr.io/daemonless/base (FreeBSD 15.1).

Need help? Join our Discord community.