Skip to content

Request to add Workout-Cool to the TrueNAS SCALE App Catalog #184

@Phyxsius7

Description

@Phyxsius7

@Snouzy , I’m very enthusiastic about this project; adding it to the TrueNAS SCALE App Catalog would let many more people benefit from it.

To support the project, I’ve put together a guide specifically for Workout-Cool.

Add Workout Cool to the TrueNAS SCALE Apps Catalog

Target platform: TrueNAS SCALE 25.04.x (Docker‑based Apps)

This guide gives the Workout Cool maintainer everything needed to contribute a catalog app for TrueNAS SCALE. It includes a ready‑to‑drop folder structure, prefilled files, testing steps, a draft PR body, and maintenance notes. The app definition intentionally does not map a fixed host port so TrueNAS can assign one dynamically during installation.

workout-cool-truenas-app.zip


1) What you’ll ship (high level)

Create a new app folder under the Community train:

https://github.com/truenas/apps/tree/master/trains/community/workout-cool/
  app.yaml          # compose-like definition: internal Postgres + web app (no host ports mapped)
  questions.yaml    # UI prompts (TZ, Node env, DB creds/URL, paths, auth, SMTP, billing, image tag)
  item.yaml         # catalog metadata (title, description, icon, categories)
  app-readme.md     # end‑user notes (how to access, DB host, URL/env guidance)
  icon.svg          # simple SVG/PNG icon

The TrueNAS Apps UI renders this folder into an installable tile under the Community train.


2) App files (prefilled)

Place the following files in trains/community/workout-cool/ of your fork of truenas/apps.

app.yaml

No host ports: mapping — TrueNAS assigns the host port automatically at install time.

services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - ${PGDATA_PATH}:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 5

  workout-cool:
    image: ghcr.io/snouzy/workout-cool:${IMAGE_TAG:-latest}
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      TZ: ${TZ}
      NODE_ENV: ${NODE_ENV}
      # Database (prefer DATABASE_URL; DB_* fallbacks for libs that expect them)
      DATABASE_URL: ${DATABASE_URL}
      DB_HOST: ${DB_HOST}
      DB_PORT: ${DB_PORT}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}

      # App URLs
      BETTER_AUTH_URL: ${BETTER_AUTH_URL}
      NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL}

      # Auth & Secrets
      BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}

      # Google OAuth (optional)
      GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
      GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}

      # OpenPanel (optional)
      OPENPANEL_SECRET_KEY: ${OPENPANEL_SECRET_KEY}
      NEXT_PUBLIC_OPENPANEL_CLIENT_ID: ${NEXT_PUBLIC_OPENPANEL_CLIENT_ID}

      # SMTP (optional)
      SMTP_HOST: ${SMTP_HOST}
      SMTP_PORT: ${SMTP_PORT}
      SMTP_USER: ${SMTP_USER}
      SMTP_PASS: ${SMTP_PASS}
      SMTP_FROM: ${SMTP_FROM}
      SMTP_SECURE: ${SMTP_SECURE}

      # Seed sample data
      SEED_SAMPLE_DATA: ${SEED_SAMPLE_DATA}

      # Stripe (optional)
      STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
      STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET}
      NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: ${NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY}
      NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_EU: ${NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_EU}
      NEXT_PUBLIC_STRIPE_PRICE_YEARLY_EU: ${NEXT_PUBLIC_STRIPE_PRICE_YEARLY_EU}
      NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_US: ${NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_US}
      NEXT_PUBLIC_STRIPE_PRICE_YEARLY_US: ${NEXT_PUBLIC_STRIPE_PRICE_YEARLY_US}
      NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_LATAM: ${NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_LATAM}
      NEXT_PUBLIC_STRIPE_PRICE_YEARLY_LATAM: ${NEXT_PUBLIC_STRIPE_PRICE_YEARLY_LATAM}
      NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_BR: ${NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_BR}
      NEXT_PUBLIC_STRIPE_PRICE_YEARLY_BR: ${NEXT_PUBLIC_STRIPE_PRICE_YEARLY_BR}
      NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_RU: ${NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_RU}
      NEXT_PUBLIC_STRIPE_PRICE_YEARLY_RU: ${NEXT_PUBLIC_STRIPE_PRICE_YEARLY_RU}
      NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_CN: ${NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_CN}
      NEXT_PUBLIC_STRIPE_PRICE_YEARLY_CN: ${NEXT_PUBLIC_STRIPE_PRICE_YEARLY_CN}

      # RevenueCat (optional)
      REVENUECAT_API_KEY: ${REVENUECAT_API_KEY}
      REVENUECAT_WEBHOOK_SECRET: ${REVENUECAT_WEBHOOK_SECRET}
      REVENUECAT_SECRET_KEY: ${REVENUECAT_SECRET_KEY}

      # Billing mode
      DEFAULT_BILLING_MODE: ${DEFAULT_BILLING_MODE}

    restart: unless-stopped

questions.yaml

These drive the TrueNAS install wizard so users don’t edit YAML.

questions:
  # Basics
  - variable: TZ
    label: Time Zone
    description: IANA TZ string (e.g., Europe/Amsterdam)
    schema:
      type: string
      default: Europe/Amsterdam

  - variable: NODE_ENV
    label: Node Environment
    description: Runtime environment for the app
    schema:
      type: string
      enum:
        - production
        - development
        - test
      default: production

  # Postgres (internal service)
  - variable: POSTGRES_USER
    label: PostgreSQL User
    schema:
      type: string
      default: workout
  - variable: POSTGRES_PASSWORD
    label: PostgreSQL Password
    schema:
      type: string
      private: true
      default: changeme
  - variable: POSTGRES_DB
    label: PostgreSQL Database
    schema:
      type: string
      default: workout_cool
  - variable: DB_PORT
    label: PostgreSQL Port
    description: Internal port used by the Postgres service
    schema:
      type: int
      default: 5432
      min: 1
  - variable: DB_HOST
    label: Database Host
    description: Hostname the app should use to reach Postgres (should be 'postgres')
    schema:
      type: string
      default: postgres
  - variable: DATABASE_URL
    label: DATABASE_URL
    description: Full connection string (e.g., postgresql://user:pass@postgres:5432/workout_cool). Must match values above.
    schema:
      type: string
      default: postgresql://workout:changeme@postgres:5432/workout_cool

  - variable: PGDATA_PATH
    label: PostgreSQL data path
    description: ZFS dataset path for Postgres data volume
    schema:
      type: path
      default: /mnt/tank/apps/workout-cool/pgdata

  # App URLs
  - variable: BETTER_AUTH_URL
    label: BETTER_AUTH_URL
    description: Public base URL where the app is reachable (update after install if using the assigned port or a domain)
    schema:
      type: string
      default: http://localhost:3000
  - variable: NEXT_PUBLIC_APP_URL
    label: NEXT_PUBLIC_APP_URL
    description: Public URL exposed to the browser (must match BETTER_AUTH_URL)
    schema:
      type: string
      default: http://localhost:3000

  # Secrets
  - variable: BETTER_AUTH_SECRET
    label: BETTER_AUTH_SECRET
    description: Generate a secure random string (e.g., openssl rand -base64 32)
    schema:
      type: string
      private: true
      default: ""

  # Google OAuth (optional)
  - variable: GOOGLE_CLIENT_ID
    label: GOOGLE_CLIENT_ID
    schema:
      type: string
      required: false
  - variable: GOOGLE_CLIENT_SECRET
    label: GOOGLE_CLIENT_SECRET
    schema:
      type: string
      private: true
      required: false

  # OpenPanel (optional)
  - variable: OPENPANEL_SECRET_KEY
    label: OPENPANEL_SECRET_KEY
    schema:
      type: string
      private: true
      required: false
  - variable: NEXT_PUBLIC_OPENPANEL_CLIENT_ID
    label: NEXT_PUBLIC_OPENPANEL_CLIENT_ID
    schema:
      type: string
      required: false

  # SMTP (optional)
  - variable: SMTP_HOST
    label: SMTP_HOST
    schema:
      type: string
      default: localhost
  - variable: SMTP_PORT
    label: SMTP_PORT
    schema:
      type: int
      default: 1025
  - variable: SMTP_USER
    label: SMTP_USER
    schema:
      type: string
      required: false
  - variable: SMTP_PASS
    label: SMTP_PASS
    schema:
      type: string
      private: true
      required: false
  - variable: SMTP_FROM
    label: SMTP_FROM
    schema:
      type: string
      default: "Workout Cool <noreply@workout.cool>"
  - variable: SMTP_SECURE
    label: SMTP_SECURE
    description: Use TLS for SMTP (true/false)
    schema:
      type: string
      enum: ["true", "false"]
      default: "false"

  # Seed
  - variable: SEED_SAMPLE_DATA
    label: SEED_SAMPLE_DATA
    description: Seed sample data on startup
    schema:
      type: string
      enum: ["true", "false"]
      default: "true"

  # Stripe (optional)
  - variable: STRIPE_SECRET_KEY
    label: STRIPE_SECRET_KEY
    schema:
      type: string
      private: true
      required: false
  - variable: STRIPE_WEBHOOK_SECRET
    label: STRIPE_WEBHOOK_SECRET
    schema:
      type: string
      private: true
      required: false
  - variable: NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
    label: NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
    schema:
      type: string
      required: false
  - variable: NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_EU
    label: NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_EU
    schema:
      type: string
      required: false
  - variable: NEXT_PUBLIC_STRIPE_PRICE_YEARLY_EU
    label: NEXT_PUBLIC_STRIPE_PRICE_YEARLY_EU
    schema:
      type: string
      required: false
  - variable: NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_US
    label: NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_US
    schema:
      type: string
      required: false
  - variable: NEXT_PUBLIC_STRIPE_PRICE_YEARLY_US
    label: NEXT_PUBLIC_STRIPE_PRICE_YEARLY_US
    schema:
      type: string
      required: false
  - variable: NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_LATAM
    label: NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_LATAM
    schema:
      type: string
      required: false
  - variable: NEXT_PUBLIC_STRIPE_PRICE_YEARLY_LATAM
    label: NEXT_PUBLIC_STRIPE_PRICE_YEARLY_LATAM
    schema:
      type: string
      required: false
  - variable: NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_BR
    label: NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_BR
    schema:
      type: string
      required: false
  - variable: NEXT_PUBLIC_STRIPE_PRICE_YEARLY_BR
    label: NEXT_PUBLIC_STRIPE_PRICE_YEARLY_BR
    schema:
      type: string
      required: false
  - variable: NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_RU
    label: NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_RU
    schema:
      type: string
      required: false
  - variable: NEXT_PUBLIC_STRIPE_PRICE_YEARLY_RU
    label: NEXT_PUBLIC_STRIPE_PRICE_YEARLY_RU
    schema:
      type: string
      required: false
  - variable: NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_CN
    label: NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_CN
    schema:
      type: string
      required: false
  - variable: NEXT_PUBLIC_STRIPE_PRICE_YEARLY_CN
    label: NEXT_PUBLIC_STRIPE_PRICE_YEARLY_CN
    schema:
      type: string
      required: false

  # RevenueCat (optional)
  - variable: REVENUECAT_API_KEY
    label: REVENUECAT_API_KEY
    schema:
      type: string
      private: true
      required: false
  - variable: REVENUECAT_WEBHOOK_SECRET
    label: REVENUECAT_WEBHOOK_SECRET
    schema:
      type: string
      private: true
      required: false
  - variable: REVENUECAT_SECRET_KEY
    label: REVENUECAT_SECRET_KEY
    schema:
      type: string
      private: true
      required: false

  # Billing mode
  - variable: DEFAULT_BILLING_MODE
    label: DEFAULT_BILLING_MODE
    description: Billing mode for self-hosted\nOptions: DISABLED, LICENSE_KEY, SUBSCRIPTION, FREEMIUM
    schema:
      type: string
      enum: ["DISABLED", "LICENSE_KEY", "SUBSCRIPTION", "FREEMIUM"]
      default: "DISABLED"

  # Image
  - variable: IMAGE_TAG
    label: Image tag
    description: Docker image tag to deploy (e.g., latest or a specific version)
    schema:
      type: string
      default: latest

item.yaml

name: Workout Cool
slug: workout-cool
train: community
title: Workout Cool
description: "A modern workout and coaching platform with auth, email, and optional billing integrations."
categories:
  - Productivity
  - Media
homepage: https://github.com/Snouzy/workout-cool
maintainers:
  - name: Project Maintainers
icon: ./icon.svg

app-readme.md

# Workout Cool (Community)

**What it is**
Workout Cool is a modern fitness/workout platform. This catalog entry deploys the app with an internal PostgreSQL service for persistence.

**How to access**
After install, TrueNAS assigns a host port automatically. Use the app link in **Apps → Installed** (or visit `http://<TrueNAS-IP>:<assigned-port>`).
*(The app listens on port 3000 in-container; TrueNAS chooses the host port.)*

**PostgreSQL**
- The app uses an internal Postgres service reachable at host `postgres` port `5432`.
- A persistent dataset is mounted at `/var/lib/postgresql/data` inside the Postgres container.

**Important URLs & OAuth**
- Set **BETTER_AUTH_URL** and **NEXT_PUBLIC_APP_URL** to the public URL of the app (after install, update to use the assigned port or your domain/reverse proxy).
- If using Google OAuth, configure authorized redirect URIs to your public URL in the Google Cloud Console.

**Default settings to review**
- `NODE_ENV` (default: `production`)
- SMTP settings (for email), Stripe/RevenueCat (optional), and `DEFAULT_BILLING_MODE` (default: `DISABLED`)
- `SEED_SAMPLE_DATA` (default: `true`)

**Storage**
- Postgres data: map a ZFS dataset path to `/var/lib/postgresql/data` (configured via `PGDATA_PATH`).

**Notes**
- The `DATABASE_URL` must point to `postgres` as the host (not `localhost`) when the database runs as a separate container.
- For production, replace secrets (e.g., `BETTER_AUTH_SECRET`) with strong random values.

icon.svg

Use any simple SVG or PNG icon (48–256px). Placeholder SVG works fine during review.


3) Local testing (recommended)

Quick compose sanity test (any Docker host)

This mirrors the catalog app and uses a fixed port only for local testing.

services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_USER: workout
      POSTGRES_PASSWORD: changeme
      POSTGRES_DB: workout_cool
    volumes:
      - ./pgdata:/var/lib/postgresql/data

  workout-cool:
    image: ghcr.io/snouzy/workout-cool:latest
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      TZ: Europe/Amsterdam
      NODE_ENV: production
      DATABASE_URL: postgresql://workout:changeme@postgres:5432/workout_cool
      DB_HOST: postgres
      DB_PORT: 5432
      POSTGRES_USER: workout
      POSTGRES_PASSWORD: changeme
      POSTGRES_DB: workout_cool
      BETTER_AUTH_URL: http://localhost:3000
      NEXT_PUBLIC_APP_URL: http://localhost:3000
      BETTER_AUTH_SECRET: "REPLACE_ME"
      SEED_SAMPLE_DATA: "true"
    ports:
      - "3000:3000"

Browse to http://localhost:3000 and verify:

  • UI loads and connects to Postgres (host postgres).
  • Account creation/login flows (email OTP or OAuth) work when SMTP/OAuth is configured.

TrueNAS test

  • Create a dataset for Postgres (PGDATA_PATH) owned by the apps user/group.
  • Install via the catalog (once merged) or temporarily via Install via YAML for early validation.
  • Confirm the app starts and the UI link in Apps → Installed opens to the dynamically assigned port.

4) Open the PR to truenas/apps

  1. Fork https://github.com/truenas/apps.
  2. Create a branch: feature/workout-cool.
  3. Add the folder trains/community/workout-cool/ with the five files above.
  4. Commit and push.
  5. Open a PR with title: Add Workout Cool to Community train.
  6. In the PR body, include the details from the template in the next section (image, services, volumes, envs, dynamic port, etc.).
  7. Address any reviewer feedback (labels, field descriptions, defaults). Once merged, the app will appear to users under Apps → Discover (Community train) after catalog refresh.

5) Draft PR body (copy/paste)

Summary
Add Workout Cool to the Community train of the TrueNAS SCALE Apps catalog.

  • Image: ghcr.io/snouzy/workout-cool (tag configurable via IMAGE_TAG, default latest)
  • Services: Internal postgres:15 plus the workout-cool web service.
  • Ports: No host ports: mapping; TrueNAS assigns an external port dynamically at install. App listens on 3000 internally.
  • Volumes: Postgres data mounted at /var/lib/postgresql/data (configurable via PGDATA_PATH).
  • Env: App/DB URLs and credentials (DATABASE_URL, DB_HOST, DB_PORT, POSTGRES_*), auth/email (BETTER_AUTH_URL/SECRET, SMTP_*), Google OAuth, optional Stripe/RevenueCat, billing mode, seeding, and public app URL.
  • Files included: app.yaml, questions.yaml, item.yaml, app-readme.md, icon.svg under trains/community/workout-cool/.

Rationale
Provides a one‑click way to deploy Workout Cool on TrueNAS SCALE (25.04.x Docker Apps). Uses an internal Postgres service, exposes only necessary settings in the UI, and leaves host port assignment to TrueNAS to avoid conflicts.

Testing

  • Installed locally using an equivalent compose and on TrueNAS with dynamic port assignment.
  • Verified UI reachable, database connectivity working (host postgres), and Postgres persistence via the mapped dataset.

Notes

  • Ensure DATABASE_URL uses host postgres (not localhost) when using the internal DB service.
  • Replace all secrets and consider setting a public domain and reverse proxy in production.

Maintainers


6) Maintenance (lightweight)

  • Image tags: Keep IMAGE_TAG default to latest for zero‑touch, or pin to a version (recommended for reproducibility). You can switch to versioned tags at any time in a follow‑up PR.
  • Automated bumps: Optionally use Renovate or a small GitHub Action to open PRs that update IMAGE_TAG when new releases are cut.
  • Breaking changes: If mounts or envs change, update questions.yaml and app-readme.md accordingly and note it in the PR body.

7) FAQ (for the catalog entry & users)

  • What URL should the app use?
    Set BETTER_AUTH_URL and NEXT_PUBLIC_APP_URL to the external URL you’ll use (assigned port, hostname, or reverse‑proxied domain). Update OAuth callback URLs accordingly.

  • Why is the database host postgres?
    The app runs in a separate container. In Docker networking, the service name (postgres) is the hostname.

  • How do I keep data persistent?
    Map the Postgres dataset path via PGDATA_PATH to persist /var/lib/postgresql/data.

  • How do I pick the right PUID/PGID?
    Use the numeric UID/GID of your TrueNAS apps user/group (or another service account that owns the datasets you mapped).


8) Optional extras

  • Healthcheck: Add a simple HTTP healthcheck for the app service if desired.
  • Docs: After the app is merged, consider a short Apps Market page or README update with screenshots.

That’s it! With these files and the draft PR text, the Workout Cool maintainer can open a PR to the TrueNAS Apps catalog with minimal effort and iterate on any feedback from the Apps reviewers.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions