-
-
Notifications
You must be signed in to change notification settings - Fork 454
Description
@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.
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 oftruenas/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 theapps
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
- Fork
https://github.com/truenas/apps
. - Create a branch:
feature/workout-cool
. - Add the folder
trains/community/workout-cool/
with the five files above. - Commit and push.
- Open a PR with title: Add Workout Cool to Community train.
- In the PR body, include the details from the template in the next section (image, services, volumes, envs, dynamic port, etc.).
- 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 viaIMAGE_TAG
, defaultlatest
) - Services: Internal
postgres:15
plus theworkout-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 viaPGDATA_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
undertrains/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 hostpostgres
(notlocalhost
) when using the internal DB service. - Replace all secrets and consider setting a public domain and reverse proxy in production.
Maintainers
- Upstream: https://github.com/Snouzy/workout-cool
- Catalog entry: Community train; maintainers TBD (upstream maintainers welcome).
6) Maintenance (lightweight)
- Image tags: Keep
IMAGE_TAG
default tolatest
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
andapp-readme.md
accordingly and note it in the PR body.
7) FAQ (for the catalog entry & users)
-
What URL should the app use?
SetBETTER_AUTH_URL
andNEXT_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 viaPGDATA_PATH
to persist/var/lib/postgresql/data
. -
How do I pick the right PUID/PGID?
Use the numeric UID/GID of your TrueNASapps
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.