TrueNAS Apps

Configuring TrueNAS apps

Purpose of this article

  • Deploy a containerized application on TrueNAS with a granular permission structure

Background

I wanted a stack of self-hosted applications for managing my media library. Originally I planned to use a Proxmox-based VM with Docker, or a set of LXCs. Ultimately I deployed the apps as TrueNAS/iX Systems Applications (available only in TrueNAS Community/Enterprise; effectively Docker with extra steps).

I chose this method for these reasons:

  • Full convergence of compute and storage (no network transport for transcodes or file operations)
  • Single-pane-of-glass management
  • The apps I wanted were available from the TrueNAS Application repo
Apps not available in the repo can be added as custom apps. Manual configuration is similar to writing a Docker compose file but with some extra specifics for alignment with TrueNAS.

General process

Each app is installed and configured essentially the same way:

  • First, create a group that all related apps can join. This means apps that need to act on the same datasets - lets’s say we’re transcoding 100 TB of Blu-Rays you ripped from your physical collection, or we want to tag and organize a big digital music library. I named mine mediastack and gave it a meaningful GID that fits my schema.
  • Download any app, start configuration, and view its desired storage volumes. Note what it needs: does it want a config directory, a data directory, a transcodes directory? Consult its documentation.
  • Pick what type of volume(s) you’ll use:
    • The default is to create “iX Volumes” which are similar to Docker volumes.
    • My preference is to configure everything with bind mounts so that all data can be directly managed.
    • You can mix types - e.g. iX Volume for ephemeral data; bind mount for shared media library.
  • Create a UID/GID for each service, or shared by the services
  • Create storage volumes for the services
  • Configure and run the services

Procedure

The method described here is atomic. For an easier setup process, but less flexibility and security, you can create one or more service UID/GIDs and share them among one or more applications.

Build group(s) and user(s)

  • Create a group for the app (manually, so you can match the GID/UID)
    • Uncheck “SMB group”
  • Create a user for the app
    • Match the GID/UID of the group
    • Check “No password”, ensure no shell access, and uncheck “SMB user”
    • Set the primary group/GID to match the app group, and set mediastack (or your choice of shared group name, if applicable) as auxiliary group

Build datasets

  • Create a dataset for the app

    • It’s smart to put VMs on a dedicated pool - for general purpose use, an SSD mirror works perfectly
    • I organize all my pools in the following fashion:
      • vmpool/
        • vmpool/clroot
          • vmpool/clroot/<unencryptedVM>/config
          • vmpool/clroot/<unencryptedVM>/data
        • vmpool/encroot
          • vmpool/encroot/<encryptedVM>/config
          • vmpool/encroot/<encryptedVM>/data
    • Since the pool root dataset should never be (but can be) encrypted, I like to set up top-level “clear” and “encrypt” root datasets
    • I lay out the storage like this even if I only expect to use data of one type or the other
    • This allows for maximum flexibility as future needs change
  • Create datasets for each volume as shown above (../<VM>/config, ../<VM>/data, etc)


Set permissions

  • Grant the following permissions: on encroot and clroot to the mediastack group:

    • Special > Execute
      • This permission allows the VM user to traverse/descend only
    • Set recursive (applies new permissions to existing data)
    • DO NOT set to apply to child datasets
  • Grant the following permissions on the relevant <VM> dataset to either the mediastack group or to the relevant <VM> user/group combo:

    • Full control
    • DO apply to child datasets
  • Grant any additional required permissions to datasets the VM must access, for example:

    • mediapool/encroot/vid_shows
    • mediapool/encroot/vid_movies
    • mediapool/encroot/downloads
      • … etc

Configure the app

  • Go back to the app setup menu

  • Set up the app, and if you have the option to choose a run-as user, select the user you created and the relevant group (mediastack or the group that matches the app user)’

  • Configure networking

    • I like to set port numbers that correspond to or resemble the UID/GID and adhere to my personal schema
  • When you reach storage bindings, change “iX Volume” to “Mount”

  • Point the GUI at the desired directory (config > vmpool/clroot/<VM>/config, and so on)

    • No need to touch the permissions buttons as we’ve already configured them

After finalizing the changes the app should spin up nearly immediately and the GUI console should print a link to the web UI (usually applicable) socket.


Configure networking

This guide is intended for use in a private network. I am not an authority on securing publicly exposed web applications.

Your architecture is up to you. You can put all the related apps on their own internal Docker network and bind only Jellyfin to both the external network and the Docket network. You can let all your apps bind to to the host machine’s IP. The correct choice depends on your wants and needs.

I’ve implemented the second option. Networks are for using. Data does not move between the apps over the network because in my case the compute and storage are converged and my network isn’t shared. YMMV.

On to DNS and reverse proxying. I like to have friendly names for everything. This means going into my firewall (OPNsense) and setting up DNS mappings and Caddy reverse proxy handlers. I use subdomains (ex. jellyfin.example.internal). Most apps support using a URL base and paths (ex. example.internal/media/jellyfin) but this requires additional app-side configuration. Modern web frontends come locked-down to prevent exploits.

As I mentioned at the top of this section, I am not a web security expert, but here are some relevant links for you to learn more. X-Forwarded-For headers are relevant for proxy configuration even in a private network.

… etc.

Networking and proxy overview

The diagram below is an example of my network configuration as it applies to my individual media apps. Your needs may be totally different.

---
config:
  theme: 'obsidian'
---
	flowchart

		subgraph Flow
			style Flow fill:black
			direction TB
				subgraph Requester
					style Requester fill:black

					wantapp@{ shape: circle, label: "Wants app" } -- Step 1 --> askdns
					askdns@{ shape: lean-r, label: "<code style="background-color:darkred">DNS query:</code> <code style="background-color: saddlebrown;"> whois app.example.internal?</code>" }

					askhttp[HTTP request to <code style="background-color: green;">Caddy IP</code>] 
					

					wantapp -- Step 2 --> askhttp	    
				end
				
				
				subgraph DNS
					style DNS fill:black
					dns@{ shape: cyl, label: "DNS server" }
					dnstab@{ shape: win-pane, label: "Host entry in <b>dnsmasq</b> pointing to <b>Caddy<b>" }

					dns o==o dnstab
				end
				
				subgraph Caddy
					style Caddy fill:black
					direction TB
					app@{ shape: bow-rect, label: "App data<br /> <code>10.0.10.100:5003</code>" }
						style app fill:darkblue,stroke:navy,stroke-width:4px,font-size:14pt
					caddy@{ shape: stadium, label: "<code style="background-color: green;">Caddy</code>" }
						style caddy fill:#008000,stroke:#008000,font-size:16pt
						
					chandler@{ shape: subproc, label: "<b>Caddy handler rule</b><br /><br />Reverse proxies <code style="background-color: saddlebrown;">app.example.internal</code> to <code>10.0.10.100:5003</code>" }
				end
				
			
			
			chandler <==> caddy & app
			askdns  --> dns -- <code style="background-color: darkred;">DNS answer:</code> <code style="background-color: green;">Caddy IP</code> --> wantapp
			
			askhttp -- HTTP GET ---> caddy		
			caddy == HTTP 200 OK ==> wantapp	

		end