47 KiB
Contributing to TrueNAS Apps Catalog
This guide will walk you through everything you need to know about contributing a new application to the catalog, from understanding the project architecture to submitting your first pull request.
Table of Contents
- Welcome & Project Overview
- Getting Started
- Project Architecture
- Contributing a New Application
- Configuration System Deep Dive
- Templates and Rendering
- Local Testing
- Updates and Migrations
- Best Practices
- Submission Guidelines
- Getting Help
Welcome & Project Overview
What is TrueNAS Apps?
TrueNAS Apps is a Docker Compose-based application catalog that makes it easy to deploy and manage containerized applications on TrueNAS systems. Think of it as an app store for your TrueNAS server.
Users can browse, install, configure, and manage applications through a friendly web interface without needing to write Docker Compose files manually.
How It Works
The system uses a templating approach:
- App developers (that's you!) define applications using a combination of metadata files (
app.yaml), configuration schemas (questions.yaml), and Jinja2 templates (docker-compose.yaml) - TrueNAS users select an app from the catalog and fill out a form based on the questions you define
- The rendering system processes your templates with the user's values and generates a standard Docker Compose file
- Docker Compose deploys and manages the containers
Our Mission
We aim to:
- Make self-hosting applications accessible to everyone, regardless of technical expertise
- Provide a curated, well-tested catalog of applications
- Enable easy application management with sensible defaults
- Support the open-source community by promoting and simplifying deployment of great applications
Why Contribute?
By contributing an app to this catalog, you:
- Help TrueNAS users easily deploy applications they love
- Give back to the open-source community
- Gain experience with Docker, templating systems, and Python
- Connect with other app developers and the TrueNAS community
Getting Started
Prerequisites
Before you begin, make sure you have the following installed on your local machine:
Required Software
- Git - For cloning the repository
- Docker - To run containers locally
- Docker Compose - For orchestrating multi-container apps
- Python 3.x - The templating and testing system is written in Python
- jq - JSON processor used by the CI scripts
To avoid installing python (and its dependencies) on your system, you can install devbox
and use the provided devbox.json file to set up your environment:
devbox shell
Required Python Packages
You can skip this if using devbox as described above.
Install these Python packages:
pip install pyyaml psutil pytest pytest-cov bcrypt pydantic
Alternative using nix-shell (if you have Nix installed):
nix-shell -p 'python3.withPackages (ps: with ps; [ pyyaml psutil pytest pytest-cov bcrypt pydantic ])'
Cloning the Repository
git clone https://github.com/truenas/apps.git
cd apps
Avoid Duplicate Work
Before starting work on an app:
- Check existing issues: Someone might already be working on it
- Check existing PRs: The app might already be in review
- Open an issue or comment: Let others know you're working on it before starting work
- Open a draft PR: This allows maintainers to catch any wrong directions early
Repository Structure
Once cloned, you'll see this structure:
.
├── ix-dev/ # App definitions
│ ├── community/ # Community-contributed apps (this is where you work!)
│ ├── stable/ # TrueNAS curated apps
│ ├── enterprise/ # Apps for enterprise users
│ ├── dev/ # Development/testing
│ └── test/ # Test apps
├── library/ # Rendering library (Python modules)
│ └── 2.x.x/ # Library versions
├── trains/ # Auto-generated catalog files (DO NOT EDIT)
├── docs/ # Documentation
├── .github/ # CI/CD scripts and workflows
│ └── scripts/
│ └── ci.py # Local/CI testing script
└── README.md
Important: ⚠️ You should only modify files under /ix-dev/ or /library/ directories. All other files are auto-generated. ⚠️
Understanding Trains
Apps are organized into "trains" (categories):
- community: Community-contributed apps - all new contributions go here
- stable: TrueNAS applications that are curated and maintained by TrueNAS team
- enterprise: Enterprise-grade applications maintained by iXsystems
All new contributions should target the community train. Other trains are managed by iXsystems maintainers.
Project Architecture
High-Level Overview
The TrueNAS Apps system follows this flow:
┌─────────────────┐
│ App Developer │ ← You define the app structure
└────────┬────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ App Definition Files │
│ • app.yaml (metadata) │
│ • ix_values.yaml (static defaults) │
│ • questions.yaml (user configuration schema) │
│ • templates/docker-compose.yaml (Jinja2) │
└────────┬────────────────────────────────────────┘
│
▼
┌─────────────────┐
│ TrueNAS User │ ← User fills out configuration form
└────────┬────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Rendering System │
│ • Merges ix_values.yaml + user values │
│ • Processes Jinja2 template │
│ • Uses library functions (ports, storage, etc) │
│ • Validates configuration │
└────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Docker Compose File │
│ • Standard docker-compose.yaml │
│ • Contains all services, volumes, networks │
└────────┬────────────────────────────────────────┘
│
▼
┌─────────────────┐
│ Docker Engine │ ← Deploys and manages containers
└─────────────────┘
Component Architecture (Top-Down)
1. App Directory Structure
Each app lives in /ix-dev/{train}/{app}/ and has this structure:
/ix-dev/{train}/{app}/
├── app.yaml # App metadata (required)
├── item.yaml # Auto-generated catalog entry
├── ix_values.yaml # Static default values (required)
├── questions.yaml # User configuration schema (required)
├── README.md # Short app description (required)
├── app_migrations.yaml # Migration definitions (optional)
├── migrations/ # Migration scripts (optional)
│ └── migration_script # Python migration script
└── templates/
├── docker-compose.yaml # Jinja2 template (required)
├── library/ # Auto-copied library files (auto-generated)
│ └── base_v2_x_xx/ # Library version (auto-generated)
├── rendered/ # Temporary (gitignored)
│ └── docker-compose.yaml # Rendered compose file (auto-generated)
└── test_values/ # CI test configurations (required)
└── basic-values.yaml # Basic test scenario (required)
2. The Library System
The library (/library/{version}/) is a collection of Python modules that handle common tasks:
- render.py: Main orchestration—coordinates the entire rendering process
- container.py: Container configuration (image, command, environment, etc.)
- storage.py: Volume and mount management
- ports.py: Port mapping configuration
- environment.py: Environment variable handling
- healthcheck.py: Health check configuration
- deps_*.py: Pre-built common dependencies:
deps_postgres.py: PostgreSQL databasedeps_redis.py: Redis cachedeps_mariadb.py: MariaDB databasedeps_perms.py: Permission initialization container- And more...
The library provides a Python API that you use in your Jinja2 templates to generate Docker Compose configuration.
3. The Rendering Process
When a user deploys an app:
- Value Collection: User input from the UI form (based on
questions.yaml) is collected - Validation: Middleware validates user input against the schema
- Value Merging: User values are merged with
ix_values.yamldefaults - Template Processing:
- Jinja2 processes
templates/docker-compose.yaml - Your template calls library functions to build configuration
- Library returns structured data representing Docker Compose services
- Jinja2 processes
- Rendering: The final Docker Compose YAML is generated
- Validation: The compose file is validated against docker engine for spec correctness
- Deployment: Docker Compose creates and starts the containers
- Monitoring: Health checks ensure containers are running properly
- Portal Generation: Web UI links are made available to the user
4. The CI/CD Pipeline
The GitHub Actions CI/CD system:
- Validates all app definitions
- Runs tests against test values
- Publishes to the catalog (after PR is merged)
Contributing a New Application
Step-by-Step Process
Step 1: Find a Similar App
The easiest way to create a new app is to copy an existing one that's similar. Browse /ix-dev/community/ to find an app with similar requirements:
- Similar number of containers (single app vs. app + database)
- Similar storage needs
- Similar networking requirements
# Example: Creating a new app based on qbittorrent
cd ix-dev/community
cp -r qbittorrent myapp
cd myapp
The app you copy may carry files that a brand-new app must not have.
If the app you copied had them, delete app_migrations.yaml, migrations/ and
deprecations.yaml: a new app has no previous version to migrate from and nothing
to deprecate yet.
Step 2: Modify app.yaml
Update the app metadata in app.yaml:
app_version: 2.1.0 # Upstream application version (ie the container image tag)
capabilities: [] # List any Linux capabilities needed (autogenerated using the generate metadata script)
categories: # Single category that best fits the app
- productivity
date_added: "2025-01-01" # Date the app was added to TrueNAS
description: A brief description of what the app does
home: https://myapp.com # Upstream homepage
host_mounts: [] # List any host mounts needed (usually for devices like /var/run/docker.sock)
icon: https://media.sys.truenas.net/apps/myapp/icons/icon.png
keywords:
- productivity
- tools
lib_version: 2.1.60 # Use the latest library version (check /library/)
lib_version_hash: "" # Leave empty, will be auto-generated
maintainers: # TrueNAS is the only maintainer for now
- email: [email protected]
name: truenas
url: https://www.ixsystems.com
name: myapp # Must match directory name
run_as_context: # Metadata for user/group the app runs as (use 568 if it can run as any non-root user)
- description: MyApp runs as any non-root user
gid: 568
group_name: myapp
uid: 568
user_name: myapp
screenshots: [] # URLs to screenshots (hosted on TrueNAS CDN)
sources: [] # Upstream source code repositories
title: My Awesome Application # Human-friendly app title
train: community # Must match parent directory
version: 1.0.0 # App version (start at 1.0.0)
Key fields:
name: Must exactly match your directory nametrain: Must match the parent directory (community,stable, etc.)version: Increment this whenever you make changes to the appapp_version: The version of the upstream applicationlib_version: Use the latest non-v1 library version (check/library/directory)lib_version_hash: Leave empty, generated bycopy-lib(see below)run_as_context: Metadata for user/group the app runs as (security best practice)changelog_url: Must actually list the version you are shipping. Upstream release pages are sometimes stale or abandoned in favour of tags; check the page before using it, and fall back to a tags page or aCHANGELOG.mdwhen there is no usable release feed.
About the library:
templates/library/ is generated, never edit or copy it by hand.
After setting lib_version, vendor the matching library and fill in lib_version_hash with:
devbox run copy-lib
Re-run it whenever you change lib_version.
The generated library must be committed with the rest of the app, it is part of what ships.
About icons and screenshots:
Icons and screenshots will be hosted on the TrueNAS CDN, include the URLs or attach the images in your PR description. The PR reviewer will upload them to the CDN and provide you with the correct URLs.
Step 3: Define Static Values (ix_values.yaml)
This file contains values that are always used, not exposed to users:
Keys for the images must always end with image.
If the versioning of an image is not SemVer, a custom versioning regex must be added in the renovate-config.js file to ensure proper updates.
Prefer ghcr over docker.io for images.
images:
image:
repository: myorg/myapp
tag: 2.1.0
worker_image:
repository: myorg/myapp-worker
tag: 2.1.0
consts:
app_container_name: myapp
perms_container_name: myapp-perms
Common patterns:
images: Define all container images and their tagsconsts: Constants used throughout your templates
What belongs in consts:
- Container names always belong there, by convention, even in a single-container app that only uses one of them once.
- Paths and other values only when the template actually needs them in more than one place.
A mount path used solely by a single
add_storage()call reads better inline as a literal. A data path that is also used to build env vars (BLOB_DIR,DATABASE_URL, ...) belongs inconsts.
Step 4: Create User Configuration Schema (questions.yaml)
This defines the form users see when configuring your app.
Most groups should exist across all apps. For example:
- My App Configuration (App-specific settings)
- User and Group Configuration (Can be skipped if the app can ONLY run as a specific uid/gid and users can't choose)
- Network Configuration
- Storage Configuration
- Labels Configuration
- Resources Configuration
Also keep in mind that most groups and their fields structure is "fixed" and cannot be changed. Rendering library does lots of automated things based on this structure.
It uses a schema-based approach:
This is a simplified example - with only few of the above mentioned groups.
groups:
- name: My App Configuration
description: Configure MyApp Settings
- name: Network
description: Network Configuration
- name: Storage
description: Storage Configuration
questions:
- variable: myapp
label: ""
group: My App Configuration
schema:
type: dict
attrs:
- variable: admin_email
label: Administrator Email
description: Email address for the administrator
schema:
type: string
required: true
default: "[email protected]"
- variable: enable_feature
label: Enable Advanced Feature
schema:
type: boolean
default: false
- variable: additional_envs
label: Additional Environment Variables
schema:
type: list
default: []
items:
- variable: env
label: Environment Variable
schema:
type: dict
attrs:
- variable: name
label: Name
schema:
type: string
required: true
- variable: value
label: Value
schema:
type: string
# Network Configuration
- variable: network
label: ""
group: Network
schema:
type: dict
attrs:
# Also simplified, a port must have whole lot more attributes.
# See other apps for full example.
- variable: web_port
label: Web Port
description: Port for the web interface
schema:
type: dict
attrs:
- variable: port
label: Port Number
schema:
type: int
default: 8080
min: 1024
max: 65535
required: true
# Storage Configuration
- variable: storage
label: ""
group: Storage
schema:
type: dict
attrs:
# Also simplified, a storage must have whole lot more attributes.
# See other apps for full example.
- variable: config
label: App Configuration Storage
description: Stores application configuration
schema:
type: dict
attrs:
- variable: type
label: Type
schema:
type: string
default: "ix_volume"
enum:
- value: "host_path"
description: "Host Path"
- value: "ix_volume"
description: "TrueNAS Dataset"
- variable: ix_volume_config
label: Dataset Configuration
schema:
type: dict
show_if: [["type", "=", "ix_volume"]]
attrs:
- variable: acl_enable
label: Enable ACL
schema:
type: boolean
default: false
- variable: host_path_config
label: Host Path Configuration
schema:
type: dict
show_if: [["type", "=", "host_path"]]
attrs:
- variable: path
label: Host Path
schema:
type: hostpath
required: true
Schema types available:
string: Text inputint: Number inputboolean: Checkboxdict: Nested configuration (object)list: Array of itemsuri: URL inputpath: File/directory path on the systemhostpath: Path that must exist on the host
And more...
Important attributes:
required: Whether the field is mandatorydefault: Default valuemin/max: For numeric fieldsmin_length/max_length: For string fieldsenum: List of allowed valuesshow_if: Conditional display based on other field valuesprivate: Hides the value (for passwords)hidden: Completely hides the field but includes it in config
And more...
Choosing the default port:
Port defaults must be unique across the whole catalog. Do not pick one by hand, ask the script:
./.github/scripts/port_validation.py
# Next 5 available ports: [30489, 30490, 30491, 30492, 30493]
It prints the next free ports, and fails if a default collides with another app.
Which environment variables to expose:
The template owns anything it depends on, the user gets the rest through additional_envs.
- Set it in the template when the rendered compose relies on the value.
Ports and storage paths are the clearest cases: the published port, the healthcheck
and the volume targets are all built from them, so a user overriding
PORTor a data directory by hand would leave the app rendered against values that no longer match. The same applies to anything a probe depends on, such as a setting that switches the app between serving http and https. - Leave it to
additional_envswhen it only tunes runtime behaviour and nothing in the template reads it. These do not deserve a question each, and a question invites users to change things they have no reason to touch.
Set the values the template owns explicitly rather than relying on the image's ENV
defaults, those can move in an upstream release and quietly relocate a user's data.
Step 5: Create the Docker Compose Template
The templates/docker-compose.yaml file is a Jinja2 template that uses the library to generate configuration:
{# First, initialize the rendering system #}
{% set tpl = ix_lib.base.render.Render(values) %}
{# Define the main application container #}
{% set app = tpl.add_container(values.consts.app_container_name, "image") %}
{% do app.set_user(values.run_as.user, values.run_as.group) %}
{% do app.healthcheck.set_test("curl", {"port": 8080, "path": "/health"}) %}
{# Configure environment variables #}
{% do app.environment.add_env("APP_PORT", values.network.web_port.port) %}
{% do app.environment.add_env("ADMIN_EMAIL", values.myapp.admin_email) %}
{# Add port mappings #}
{% do app.add_port(values.network.web_port) %}
{# Configure storage #}
{% do app.add_storage("/config", values.storage.config) %}
{# Setup permissions container for storage initialization #}
{% set perms = tpl.deps.perms(values.consts.perms_container_name) %}
{% do perms.add_or_skip_action("config", values.storage.config, {"uid": 568, "gid": 568, "mode": "check"}) %}
{% if perms.has_actions() %}
{% do perms.activate() %}
{% do app.depends.add_dependency(values.consts.perms_container_name, "service_completed_successfully") %}
{% endif %}
{# Add portal for UI access #}
{% do tpl.portals.add(values.network.web_port, scheme="http", path="/") %}
{# Render the final configuration #}
{{ tpl.render() | tojson }}
Explore other apps for more examples.
Key library components:
tpl.add_container(name, image_key): Creates a new containerapp.set_user(uid, gid): Sets the user the container runs asapp.healthcheck.set_test(): Configures health checksapp.environment.add_env(): Adds environment variablesapp.add_port(): Maps ports to the hostapp.add_storage(): Mounts volumestpl.deps.perms(): Creates a permissions init containertpl.portals.add(): Adds a web UI portal link
Adding dependencies (database example):
{# Add PostgreSQL database #}
{% set pg_config = {
"user": values.consts.db_user,
"password": values.myapp.db_password,
"database": values.consts.db_name,
"volume": values.storage.postgres_data,
} %}
{% set postgres = tpl.deps.postgres("postgres", "postgres_image", pg_config, perms) %}
{% do app.depends.add_dependency("postgres", "service_healthy") %}
{# Use postgres.get_url() to generate the connection string #}
{% do app.environment.add_env("DATABASE_URL", postgres.get_url("postgresql")) %}
Step 6: Create Test Files
Create test value files in templates/test_values/ to test different configurations:
basic-values.yaml:
resources:
limits:
cpus: 2.0
memory: 4096
# Should contain all options that are exposed in the questions.yaml
myapp:
admin_email: [email protected]
enable_feature: false
additional_envs: []
network:
web_port:
bind_mode: published
port_number: 30080 # Use a non-default port for testing, catches issues early
ix_volumes:
test_myapp_config: /opt/tests/mnt/myapp/config
storage:
config:
type: ix_volume
ix_volume_config:
dataset_name: test_myapp_config
create_host_path: true
Note on test storage paths:
Most apps use directories like /opt/tests/** for storage in test files. This is because:
- macOS whitelists
/opt/by default for Docker - Linux doesn't have this restriction
- It prevents accidentally mounting sensitive directories
Make sure your test files won't mount any directories you don't want them to!
Step 7: Create README.md
Keep the README brief - just a title and short description. Link to upstream documentation for details:
# My Awesome Application
[My Awesome Application](https://myapp.com) is a tool for doing awesome things.
Configuration System Deep Dive
Understanding Values Hierarchy
When your template is rendered, values come from multiple sources merged in this order:
ix_values.yaml- Your static defaults- User input from
questions.yaml(or test values during testing)
Access values in templates using: values.path.to.variable
Templates and Rendering
Template Structure
Every template follows this basic structure:
{# 1. Initialize the rendering system #}
{% set tpl = ix_lib.base.render.Render(values) %}
{# 2. Add containers #}
{# 3. Configure containers #}
{# 4. Add dependencies #}
{# 5. Setup permissions #}
{# 6. Add portals #}
{# Final: Render the configuration #}
{{ tpl.render() | tojson }}
Library API Reference
Adding a Container
{% set app = tpl.add_container("container_name", "image_key") %}
- First argument: Container name (use value from
consts) - Second argument: Key in
ix_values.yamlimages section
Container Configuration
{# Set user and group - usually picked automatically from run_as_context in app.yaml #}
{# Only set manually if you need to override the default #}
{% do app.set_user(568, 568) %}
{# Set entrypoint and command #}
{% do app.set_entrypoint(["myapp"]) %}
{% do app.set_command(["--config", "/config/app.conf"]) %}
{# Add environment variables #}
{% do app.environment.add_env("KEY", "value") %}
{% do app.environment.add_env("PORT", 8080) %}
{# Add port mappings #}
{% do app.add_port(values.network.web_port) %}
{# Add storage mounts #}
{% do app.add_storage("/config", values.storage.config) %}
{% do app.add_storage("/data", values.storage.data) %}
{# Set capabilities #}
{% do app.add_caps(["NET_ADMIN", "SYS_ADMIN"]) %}
Health Checks
{# HTTP health check #}
{% do app.healthcheck.set_test("curl", {"port": 8080, "path": "/"}) %}
{# TCP health check #}
{% do app.healthcheck.set_test("netcat", {"port": 5432}) %}
{# Disable health check #}
{% do app.healthcheck.disable() %}
Dependencies
{# Wait for service to be healthy #}
{% do app.depends.add_dependency("database", "service_healthy") %}
{# Wait for init container to complete #}
{% do app.depends.add_dependency("init", "service_completed_successfully") %}
Permissions Container
{# Create permissions container #}
{% set perms = tpl.deps.perms("perms_container_name") %}
{% set perms_config = {"uid": 568, "gid": 568, "mode": "check"} %}
{# Add permission actions #}
{% do perms.add_or_skip_action("config", values.storage.config, perms_config) %}
{# Activate and add dependency #}
{% if perms.has_actions() %}
{% do perms.activate() %}
{% do app.depends.add_dependency("perms_container_name", "service_completed_successfully") %}
{% endif %}
Permission modes:
Both modes will skip if the directory is not empty.
check: Only fix if permissions are wrongalways: Always set permissions
Common Dependencies
PostgreSQL:
{% set pg_config = {
"user": "myapp",
"password": values.myapp.db_password,
"database": "myapp_db",
"volume": values.storage.postgres_data,
} %}
{% set postgres = tpl.deps.postgres("postgres", "postgres_image", pg_config, perms) %}
{% do app.depends.add_dependency("postgres", "service_healthy") %}
Redis:
{% set redis_config = {
"password": values.myapp.redis_password,
"volume": values.storage.redis_data,
} %}
{% set redis = tpl.deps.redis("redis", "redis_image", redis_config, perms) %}
{% do app.depends.add_dependency("redis", "service_healthy") %}
MariaDB:
{% set mariadb_config = {
"user": "myapp",
"password": values.myapp.db_password,
"database": "myapp_db",
"root_password": values.myapp.root_password,
"volume": values.storage.mariadb_data,
} %}
{% set mariadb = tpl.deps.mariadb("mariadb", "mariadb_image", mariadb_config, perms) %}
{% do app.depends.add_dependency("mariadb", "service_healthy") %}
Portals
Portals create clickable links in the TrueNAS UI:
{# Simple portal (HTTP, path="/") #}
{% do tpl.portals.add(values.network.web_port) %}
{# Custom portal #}
{% do tpl.portals.add(values.network.web_port, {"scheme": "https", "path": "/admin"}) %}
{# Multiple portals #}
{% do tpl.portals.add(values.network.web_port, {"scheme": "http"}) %}
{% do tpl.portals.add(values.network.api_port, {"name": "Custom Label", "path": "/api"}) %}
Notes
Add informational notes displayed to users:
{% do tpl.notes.add_info("First time setup requires visiting /setup") %}
{% do tpl.notes.add_warning("This app requires GPU passthrough") %}
What Gets Generated
Your template generates a standard Docker Compose file. Here's an example:
Input (your template):
{% set tpl = ix_lib.base.render.Render(values) %}
{% set app = tpl.add_container(values.consts.app_container_name, "image") %}
{% do app.set_user(568, 568) %}
{% do app.add_port(values.network.web_port) %}
{% do app.healthcheck.set_test("curl", {"port": 8080, "path": "/ping"}) %}
{% do app.add_storage("/config", values.storage.config) %}
{{ tpl.render() | tojson }}
Output (generated compose file):
services:
myapp:
cap_drop:
- ALL
deploy:
resources:
limits:
cpus: 2
memory: "4294967296"
environment:
GID: "568"
GROUP_ID: "568"
NVIDIA_VISIBLE_DEVICES: void
PGID: "568"
PUID: "568"
TZ: Etc/UTC
UID: "568"
UMASK: "002"
UMASK_SET: "002"
USER_ID: "568"
group_add:
- "568"
healthcheck:
test:
- CMD
- curl
- --request
- GET
- --silent
- --output
- /dev/null
- --show-error
- --fail
- http://127.0.0.1:8080/ping
timeout: 5s
interval: 30s
retries: 5
start_period: 15s
start_interval: 2s
image: some-repo/myapp:2.1.0
platform: linux/amd64
ports:
- mode: ingress
target: 8080
published: "8080"
protocol: tcp
restart: unless-stopped
security_opt:
- no-new-privileges=true
user: 568:568
volumes:
- type: bind
source: /opt/tests/mnt/config
target: /config
bind:
propagation: rprivate
create_host_path: true
x-notes: |
# <app_name>
## Security
**Read the following security precautions to ensure that you wish to continue using this application.**
---
### Container: [myapp]
#### Running user/group(s)
- User: 568
- Group: 568
- Supplementary Groups: apps
## Bug Reports and Feature Requests
If you find a bug in this app or have an idea for a new feature, please file an issue at
https://github.com/truenas/apps
x-portals:
- host: 0.0.0.0
name: Web UI
path: /
port: 8080
scheme: http
Local Testing
Before submitting your PR, thoroughly test your app locally using the CI script.
Running Tests
The .github/scripts/ci.py script is your primary testing tool:
# Basic test - deploys the app and waits for it to be healthy
./.github/scripts/ci.py --app myapp --train community --test-file basic-values.yaml
# Keep the app running for manual testing
./.github/scripts/ci.py --app myapp --train community --test-file basic-values.yaml --wait=true
# or devbox run app-test community myapp <optionally basic-values.yaml>
# Just render the compose file without deploying
./.github/scripts/ci.py --app myapp --train community --test-file basic-values.yaml --render-only=true
# or devbox run app-render community myapp <optionally basic-values.yaml>
Command Options
--app: Your app directory name--train: The train (community, stable, etc.)--test-file: Which test file fromtemplates/test_values/to use--wait=true: Keeps the app running until you stop it (Ctrl+C). Shows the web UI URL if available--render-only=true: Only renders the compose file without deploying
What the CI Script Does
When you run the CI script, it automatically:
- Generates
item.yaml: Creates the catalog entry - Updates library files: Copies the correct library version to
templates/library/based onlib_versioninapp.yaml - Updates
lib_version_hash: Calculates and sets the hash inapp.yaml - Renders the template: Processes your Jinja2 template with the test values
- Deploys with Docker Compose: Starts the containers (unless
--render-only) - Monitors health: Waits for containers to become healthy (times out after 10 minutes)
Metadata Generation / Validation
# Generates some metadata (like capabilities) for your app
./.github/scripts/generate_metadata.py --app myapp --train community
# Validates ports in questions.yaml are unique
./.github/scripts/port_validation.py
Testing Workflow
-
Start with basic test:
./.github/scripts/ci.py --app myapp --train community --test-file basic-values.yaml --wait=true -
Check the output:
- The script will show you the rendered compose file location
- It will display the web UI URL if configured
- Watch for any errors in container startup
-
Verify functionality:
- Open the web UI in your browser
- Test basic functionality
- Check logs:
docker logs myapp
-
Test different configurations:
- Create additional test files if needed (e.g.,
with-database-values.yaml,hostpath-values.yaml) - Test each configuration thoroughly
- Create additional test files if needed (e.g.,
-
Clean up:
- Press Ctrl+C to stop (with
--wait=true) - Or the script will auto-cleanup if it runs without
--wait - Manual cleanup if needed:
docker compose -f /path/to/rendered/docker-compose.yaml down -v
- Press Ctrl+C to stop (with
Troubleshooting
Containers won't start:
- Check
docker logs <container_name>for errors - Verify image names and tags in
ix_values.yaml - Check port conflicts:
docker psto see if ports are already in use
Permission errors:
- Verify
set_userin your template - Check permissions container configuration
- Ensure storage paths are accessible
Template errors:
- Use
--render-only=trueto see the rendered compose file - Check for Jinja2 syntax errors
- Verify all values paths exist in your test files
Health checks failing:
- Verify the health check command is correct
- Verify the application ships the binary used in the health check (ie
curl, etc.)
Testing on TrueNAS
Currently, there's no easy way to test directly on a TrueNAS system before your PR is merged. However:
- If it works on your local machine with Docker Compose, it should work on TrueNAS
- Exceptions include hardware-specific features (GPU, devices, etc.)
- Let the reviewer know about any special requirements in your PR
Testing questions.yaml
The questions.yaml schema is validated during CI, but also needs manual review:
- To see how different values affect the rendered compose file, modify your test files
- Test all conditional fields (
show_if) - Verify all enum options work correctly
- Test required vs optional fields
Updates and Migrations
When you update an existing app or change its configuration structure, you may need migrations to preserve user data and settings.
When to Use Migrations
Use migrations when:
- Changing the structure of configuration values
- Renaming configuration fields
Creating a Migration
Step 1: Create app_migrations.yaml
Define when your migration should run:
migrations:
- file: migrate_to_v2
from:
max_version: 1.0.10 # Applies to apps upgrading from version <= 1.0.10
target:
min_version: 2.0.0 # When upgrading to version >= 2.0.0
Step 2: Write the Migration Script
Create migrations/migrate_to_v2 (Python script):
#!/usr/bin/python3
import yaml
import sys
import os
def migrate(values):
"""
Transform old configuration to new configuration.
Old structure:
network:
web_port: 8080
New structure:
network:
web_port:
port: 8080
bind_mode: "published"
"""
# Check if old structure exists
if isinstance(values.get("network", {}).get("web_port"), int):
old_port = values["network"]["web_port"]
# Transform to new structure
values["network"]["web_port"] = {
"port": old_port,
"bind_mode": "published",
"host_ips": [],
}
return values
if __name__ == "__main__":
if len(sys.argv) != 2:
exit(1)
if os.path.exists(sys.argv[1]):
with open(sys.argv[1], "r") as f:
print(yaml.dump(migrate(yaml.safe_load(f.read()))))
Step 3: Make Script Executable
chmod +x migrations/migrate_to_v2
Step 4: Update app.yaml
Increment the version field to trigger the migration:
version: 2.0.0 # Incremented from 1.0.10
Migration Best Practices
- Handle Edge Cases: Check for missing or unexpected values
- Test Thoroughly: Test migrations with various old configurations
- Document Changes: Explain what changed in your PR description
- Preserve Data: Never delete user data without explicit consent
- Validate Output: Ensure migrated values match the new schema
Best Practices
Security
-
Run as Non-Root: Always define
run_as_contextwith a non-root userNote: this is only METADATA
run_as_context: - uid: 568 gid: 568 user_name: myapp group_name: myapp -
Use Secrets: Mark sensitive fields as private in
questions.yamlNote: this is just UI masking.
- variable: password schema: type: string private: true -
Minimal Capabilities: Only add capabilities if absolutely necessary
{% do app.set_capabilities(["NET_ADMIN"]) %} # Only if needed -
Validate Input: Use schema validation (min, max, enum) in
questions.yaml
Performance
-
Use Health Checks: Always define appropriate health checks
{% do app.healthcheck.set_test("curl", {"port": 8080, "path": "/health"}) %} -
Optimize Dependencies: Only add dependencies when needed
{% do app.depends.add_dependency("postgres", "service_healthy") %} -
Use Latest Library: Always use the newest non-v1 library version
User Experience
- Sensible Defaults: Provide good default values in
questions.yaml - Clear Descriptions: Add helpful descriptions to fields (where applicable)
- Group Related Settings: Use the existing groups to organize configuration
- Conditional Fields: Use
show_ifto hide irrelevant options - Add Portals: Always add web UI portals when applicable
Code Quality
- Start from Similar App: Copy a similar app rather than starting from scratch
- Follow Naming Conventions: Use consistent naming (snake_case for variables)
- Comment Complex Logic: Add comments in templates for clarity
- Test Multiple Scenarios: Create multiple test files
- Clean Up: Remove unused code from copied apps
Storage
-
Use ix_volume by Default: TrueNAS-managed datasets are preferred as defaults, as it makes it easy to spin up the app and test.
default: "ix_volume" -
Document Storage Needs: Explain what each storage mount is for
-
Test Paths: Use
/opt/tests/prefix in test files for compatibility
Versioning
-
Semantic Versioning: Use semantic versioning for
versionfield- Major: Breaking changes
- Minor: New features
- Patch: Bug fixes
-
Track Upstream: Keep
app_versionin sync with main container tag -
Increment on Every Change: Always bump
versionwhen modifying the app
Documentation
- Document Special Requirements: Note GPU, devices, or network requirements
- Add Configuration Notes: Include setup instructions if needed
- Link to Upstream Docs: Provide links to official documentation
Submission Guidelines
Before You Submit
Checklist before opening a PR:
- App works locally with all test files
app.yamlmetadata is complete and accuratequestions.yamlhas clear labels and descriptions- All test files pass successfully
- README.md is written
- Only files under
/ix-dev/or/library/are modified - No auto-generated files are included in the PR
- Icons/screenshots are ready (links provided in PR description)
PR Description Template
When you create a new pull request, GitHub will automatically populate it with our PR template. This template includes sections for:
- Description: Brief overview of the app and what it does
- App Information: Links to upstream repository, documentation, license, and version
- Testing: Checklist of test scenarios you've verified
- Icons and Screenshots: Visual assets for the app
- Special Notes: Any important setup or usage information
- Checklist: Final verification before submission
The template is located at .github/PULL_REQUEST_TEMPLATE/app_addition.md
and will be automatically loaded when you create a PR.
Simply fill in the placeholders with your app's specific information.
Review Process
What to expect:
-
Automated Checks: CI will run automatically
- Validates your app structure
- Runs test files
- Checks for errors
-
Human Review: A maintainer will review your PR
- Check code quality
- Verify app works as expected
- Suggest improvements
-
Iteration: You may need to make changes
- Address review comments
- Fix any issues found
- Update based on feedback
-
CDN Upload: Reviewer will upload icons/screenshots
- You'll receive CDN URLs
- Update your PR with the URLs
-
Merge: Once approved, your PR will be merged!
After Your PR is Merged
Congratulations! Your app is now in the catalog. 🎉
Notify Upstream Developers
It's a great idea to let the upstream app developers know that their app is now available on TrueNAS:
- Contact them politely: Via GitHub issue, email, or Discord
- Provide a quick how-to: Explain how to deploy their app on TrueNAS
- Suggest adding TrueNAS: To their supported platforms list
- Share the catalog link: Link to the TrueNAS Apps catalog
Getting Help
Resources
-
GitHub Discussions: https://github.com/truenas/apps/discussions
- Ask questions
- Share tips and tricks
- Collaborate with other developers
-
Library Tests:
/library/{version}/tests/- Real examples of library usage
- See how different features work
-
Existing Apps:
/ix-dev/community/- Browse real-world examples
- See how similar apps are structured
- Copy patterns that work
-
TrueNAS Forums: https://forums.truenas.com
- Community support
- User feedback
- General TrueNAS questions
Common Questions
Q: Which library version should I use?
A: Always use the latest non-v1 version from /library/. Check the directory for available versions and use the highest numbered 2.x.x version.
Q: My app needs a GPU. How do I configure that?
A: Check apps like plex or jellyfin that use GPU passthrough. You'll need to add device mappings and possibly capabilities.
Q: Can I test on TrueNAS before submitting? A: Currently there's no easy way. Test locally with Docker Compose—if it works there, it should work on TrueNAS.
Q: How do I handle questions.yaml migrations?
A: Create an app_migrations.yaml file and a Python migration script. See the "Updates and Migrations" section above.
Q: Can I add an app to the stable or enterprise train? A: You can't. This train is managed by TrueNAS maintainers only. Submit to the community train only.
Q: How do I update an existing app?
A: Fork the repo, make your changes, increment the version in app.yaml, and open a PR. Include what changed in the description.
Q: An app is outdated. Can I update it? A: If it's been more than 2 days that the upstream container tag has been released, and our bot hasn't picked it up yet, please open an issue. A fix has to be applied so the bot can pick it up.
Getting Involved
Want to do more than just contribute apps?
- Review PRs: Help review other contributors' apps
- Improve Documentation: Submit improvements to this guide
- Report Issues: Found a bug? Open an issue
- Help Users: Answer questions in Discussions / Issues
Contact
- GitHub Issues: For bugs and feature requests
- GitHub Discussions: For questions and general discussion
- Pull Requests: For contributions
Apps Catalog Inclusion Policy
The TrueNAS team maintains this catalog and reserves the right to reject, remove, or unpublish any application at its sole discretion, with or without a stated reason.
Meeting the technical requirements in this repository does not guarantee inclusion.
The following categories will not be accepted:
- Applications with known risks to data integrity. This includes applications that can silently corrupt or lose data, bypass or disable ZFS data protection features, or require unsafe configurations to function normally.
- Applications that interact with the TrueNAS host management layer or API.
- Applications must operate within their assigned container boundaries and use only the storage and network resources allocated to them.
If you are unsure whether a proposed application falls into either category, open an issue before submitting a pull request.
Thank You!
Thank you for contributing to the TrueNAS Apps catalog! Your work helps make self-hosting accessible to everyone. We appreciate your time and effort in making this ecosystem better.
Happy coding! 🚀