dbv 3 months ago
commit
a22d4c6679
100 changed files with 27852 additions and 0 deletions
  1. 121 0
      CONTRIBUTIONS.md
  2. 165 0
      LICENSE
  3. 144 0
      README.md
  4. 15780 0
      catalog.json
  5. 456 0
      cspell.config.yaml
  6. 23 0
      devbox.json
  7. 208 0
      devbox.lock
  8. 30 0
      features_capability.json
  9. 3 0
      ix-dev/community/actual-budget/README.md
  10. 39 0
      ix-dev/community/actual-budget/app.yaml
  11. 6 0
      ix-dev/community/actual-budget/app_migrations.yaml
  12. 10 0
      ix-dev/community/actual-budget/item.yaml
  13. 10 0
      ix-dev/community/actual-budget/ix_values.yaml
  14. 23 0
      ix-dev/community/actual-budget/migrations/ip_port_migration
  15. 409 0
      ix-dev/community/actual-budget/questions.yaml
  16. 42 0
      ix-dev/community/actual-budget/templates/docker-compose.yaml
  17. 0 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/__init__.py
  18. 70 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/client.py
  19. 86 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/configs.py
  20. 450 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/container.py
  21. 34 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/depends.py
  22. 24 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/deploy.py
  23. 47 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/deps.py
  24. 95 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/deps_elastic.py
  25. 81 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/deps_mariadb.py
  26. 85 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/deps_meilisearch.py
  27. 91 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/deps_mongodb.py
  28. 259 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/deps_perms.py
  29. 160 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/deps_postgres.py
  30. 90 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/deps_redis.py
  31. 31 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/device.py
  32. 54 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/device_cgroup_rules.py
  33. 71 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/devices.py
  34. 79 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/dns.py
  35. 112 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/environment.py
  36. 4 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/error.py
  37. 31 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/expose.py
  38. 33 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/extra_hosts.py
  39. 26 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/formatter.py
  40. 210 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/functions.py
  41. 268 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/healthcheck.py
  42. 37 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/labels.py
  43. 133 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/notes.py
  44. 73 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/portals.py
  45. 147 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/ports.py
  46. 99 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/render.py
  47. 115 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/resources.py
  48. 25 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/restart.py
  49. 52 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/security_opts.py
  50. 125 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/storage.py
  51. 38 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/sysctls.py
  52. 0 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/__init__.py
  53. 57 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_build_image.py
  54. 63 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_configs.py
  55. 458 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_container.py
  56. 54 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_depends.py
  57. 840 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_deps.py
  58. 150 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_device.py
  59. 79 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_device_cgroup_rules.py
  60. 64 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_dns.py
  61. 196 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_environment.py
  62. 46 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_expose.py
  63. 57 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_extra_hosts.py
  64. 13 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_formatter.py
  65. 133 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_functions.py
  66. 353 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_healthcheck.py
  67. 88 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_labels.py
  68. 230 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_notes.py
  69. 93 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_portal.py
  70. 383 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_ports.py
  71. 37 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_render.py
  72. 140 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_resources.py
  73. 57 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_restart.py
  74. 91 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_security_opts.py
  75. 62 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_sysctls.py
  76. 132 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_validations.py
  77. 746 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_volumes.py
  78. 75 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/tmpfs.py
  79. 344 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/validations.py
  80. 87 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/volume_mount.py
  81. 43 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/volume_mount_types.py
  82. 108 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/volume_sources.py
  83. 133 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/volume_types.py
  84. 61 0
      ix-dev/community/actual-budget/templates/library/base_v2_1_49/volumes.py
  85. 32 0
      ix-dev/community/actual-budget/templates/test_values/basic-values.yaml
  86. 31 0
      ix-dev/community/actual-budget/templates/test_values/hostnet-values.yaml
  87. 118 0
      ix-dev/community/actual-budget/templates/test_values/https-values.yaml
  88. 11 0
      ix-dev/community/adguard-home/README.md
  89. 48 0
      ix-dev/community/adguard-home/app.yaml
  90. 6 0
      ix-dev/community/adguard-home/app_migrations.yaml
  91. 9 0
      ix-dev/community/adguard-home/item.yaml
  92. 12 0
      ix-dev/community/adguard-home/ix_values.yaml
  93. 28 0
      ix-dev/community/adguard-home/migrations/ip_port_migration
  94. 596 0
      ix-dev/community/adguard-home/questions.yaml
  95. 44 0
      ix-dev/community/adguard-home/templates/docker-compose.yaml
  96. 0 0
      ix-dev/community/adguard-home/templates/library/base_v2_1_49/__init__.py
  97. 70 0
      ix-dev/community/adguard-home/templates/library/base_v2_1_49/client.py
  98. 86 0
      ix-dev/community/adguard-home/templates/library/base_v2_1_49/configs.py
  99. 450 0
      ix-dev/community/adguard-home/templates/library/base_v2_1_49/container.py
  100. 34 0
      ix-dev/community/adguard-home/templates/library/base_v2_1_49/depends.py

+ 121 - 0
CONTRIBUTIONS.md

@@ -0,0 +1,121 @@
+# Contributions
+
+Thank you for your interest in contributing to the Apps Catalog.
+
+If you start working on something, please either open an issue and state that you are working on it,
+make a comment on existing issues, or open a PR (even if it's empty!). This will help avoid
+duplicate work!
+
+## Library
+
+The library is written in Python and is located in the `/library/2.x.x` directory.
+You will rarely need to interact with the library directly.
+
+## New Apps
+
+If you want to add a new app to the catalog, the easier way is
+to copy an existing app that is similar to the one you want to add,
+then modify it to your needs. You can find usage examples in either,
+other apps that use the library, or in the `/library/2.x.x/tests` directory.
+
+For icons and screenshots that we store in our CDN, you can leave the links
+in the PR descriptions, and the PR reviewer will upload them and give you the links.
+
+⚠️ Make sure that you are only adding/modifying files under the `/ix-dev` or `/library` directories!
+All other files should be ignored, as they are mostly auto-generated.
+
+## Local Testing
+
+You will need to have `docker`, `docker compose` and `python` installed.
+Additionally, you need the following Python packages installed:
+`pyyaml, psutil, pytest, pytest-cov, bcrypt, pydantic`
+
+If you have `nix-shell` installed, you can run the following command to
+start a shell with all the required python packages installed:
+
+```bash
+nix-shell -p 'python3.withPackages (ps: with ps; [ pyyaml psutil pytest pytest-cov bcrypt pydantic ])'
+```
+
+The directory `test_values` contains test files for each app.
+To run a test against a test file, run the following command:
+
+```bash
+./.github/scripts/ci.py --app qbittorrent --train community --test-file basic-values.yaml --wait=true
+```
+
+`--wait=true` will start the container, and wait until you stop it. It will also show the URL of the web UI (if available).
+`--render-only=true` will just print the rendered compose file, without starting the container. (You can pipe the output to a file if you want to save it.)
+
+Both flags are optional. If you don't specify them, it will run the app against the test file.
+And stop it as soon as it becomes healthy. It will timeout after 10 minutes if it doesn't become healthy.
+If you manually `ctrl+c`, you will have to cleanup the leftover containers.
+
+The above command will also do the following:
+
+- Generate the `item.yaml` file
+- Update the contents of the `templates/library/` directory based on the `lib_version` on the `app.yaml` file
+- Update the `lib_version_hash` on the `app.yaml` file
+
+ie. if you just want to generate the above, just run the following command, test file can be any, but must exists:
+
+```bash
+./.github/scripts/ci.py --app qbittorrent --train community --test-file basic-values.yaml --render-only=true
+```
+
+## Testing on TrueNAS System
+
+There is no easy way currently to test on TrueNAS.
+As long as it works on your local machine, it should work on TrueNAS.
+There are some exceptions for things like devices etc, that might be different.
+In such case, let the reviewer know.
+
+`questions.yaml` is run through some validation during CI, but it still needs some manual check from the reviewer.
+If you want to test how the rendered compose file will look based on different values entered in the questions.yaml,
+you can enter the values you want in one of the test files and render it.
+
+## App structure
+
+```sh
+.
+├── app.yaml # Contains the app metadata
+├── item.yaml # It is automatically generated by the ci.py script
+├── ix_values.yaml # Contains static values that are used in the app always
+├── questions.yaml # Contains the questions that the user will be asked when deploying the app
+├── README.md # Contains a short description of the app
+└── templates
+    ├── docker-compose.yaml # The template file.
+    ├── library
+    │   └── base_v2_0_21 # Automatically copied when run the ci.py script based on the lib_version on the app.yaml file
+    ├── rendered # This is in the .gitignore and is only used as an intermediate step to deploy the app
+    │   └── docker-compose.yaml
+    └── test_values # Those are used in CI only, values in there are not used when deploying the app
+        ├── basic-values.yaml
+        └── hostnet-values.yaml
+```
+
+### app.yaml
+
+Things to note:
+
+- `name:` should match the directory name
+- `train:` should match the directory above the app directory
+- `app_version:` is the version of the container, usually the tag of the `image` if there are multiple kind of images
+- `lib_version:` must be one of the available versions in the top level `library` directory. (Do not use v1)
+
+⚠️ Some notes for the test files:
+
+Most apps will use a directory like `/opt/tests/**` for storage in test files.
+This is mostly because MacOS whitelists this directory by default. And linux does not have this restriction.
+
+Make sure before running the test, that is not going to mount any directories that you don't want to.
+
+## Notifying Upstream Developers
+
+After your app has been successfully published in the catalog and tested, it is a good idea 
+to politely contact the upstream app developers to let them know that TrueNAS now supports 
+simple deployment of their app. 
+You can provide a quick "how-to" for deploying their app on TrueNAS, to make it easier for their 
+team to share with their users. If the developer has a section on their website listing supported 
+platforms, suggest they add TrueNAS as one of the platforms capable of running their app 
+with easy deployment.

+ 165 - 0
LICENSE

@@ -0,0 +1,165 @@
+                   GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+  This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+  0. Additional Definitions.
+
+  As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+  "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+  An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+  A "Combined Work" is a work produced by combining or linking an
+Application with the Library.  The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+  The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+  The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+  1. Exception to Section 3 of the GNU GPL.
+
+  You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+  2. Conveying Modified Versions.
+
+  If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+   a) under this License, provided that you make a good faith effort to
+   ensure that, in the event an Application does not supply the
+   function or data, the facility still operates, and performs
+   whatever part of its purpose remains meaningful, or
+
+   b) under the GNU GPL, with none of the additional permissions of
+   this License applicable to that copy.
+
+  3. Object Code Incorporating Material from Library Header Files.
+
+  The object code form of an Application may incorporate material from
+a header file that is part of the Library.  You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+   a) Give prominent notice with each copy of the object code that the
+   Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the object code with a copy of the GNU GPL and this license
+   document.
+
+  4. Combined Works.
+
+  You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+   a) Give prominent notice with each copy of the Combined Work that
+   the Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the Combined Work with a copy of the GNU GPL and this license
+   document.
+
+   c) For a Combined Work that displays copyright notices during
+   execution, include the copyright notice for the Library among
+   these notices, as well as a reference directing the user to the
+   copies of the GNU GPL and this license document.
+
+   d) Do one of the following:
+
+       0) Convey the Minimal Corresponding Source under the terms of this
+       License, and the Corresponding Application Code in a form
+       suitable for, and under terms that permit, the user to
+       recombine or relink the Application with a modified version of
+       the Linked Version to produce a modified Combined Work, in the
+       manner specified by section 6 of the GNU GPL for conveying
+       Corresponding Source.
+
+       1) Use a suitable shared library mechanism for linking with the
+       Library.  A suitable mechanism is one that (a) uses at run time
+       a copy of the Library already present on the user's computer
+       system, and (b) will operate properly with a modified version
+       of the Library that is interface-compatible with the Linked
+       Version.
+
+   e) Provide Installation Information, but only if you would otherwise
+   be required to provide such information under section 6 of the
+   GNU GPL, and only to the extent that such information is
+   necessary to install and execute a modified version of the
+   Combined Work produced by recombining or relinking the
+   Application with a modified version of the Linked Version. (If
+   you use option 4d0, the Installation Information must accompany
+   the Minimal Corresponding Source and Corresponding Application
+   Code. If you use option 4d1, you must provide the Installation
+   Information in the manner specified by section 6 of the GNU GPL
+   for conveying Corresponding Source.)
+
+  5. Combined Libraries.
+
+  You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+   a) Accompany the combined library with a copy of the same work based
+   on the Library, uncombined with any other library facilities,
+   conveyed under the terms of this License.
+
+   b) Give prominent notice with the combined library that part of it
+   is a work based on the Library, and explaining where to find the
+   accompanying uncombined form of the same work.
+
+  6. Revised Versions of the GNU Lesser General Public License.
+
+  The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+  Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+  If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.

+ 144 - 0
README.md

@@ -0,0 +1,144 @@
+# TrueNAS Apps Catalog
+
+This repository contains the Docker-Compose based App catalog used by TrueNAS systems to render and update the Apps catalog.
+
+![image](https://github.com/user-attachments/assets/2f00c325-9d6a-46ff-8162-a080fd8a156a)
+
+## Deprecating Apps, Features, Configuration or Functionality
+
+When an `App`, `Feature`, `Configuration` or `Functionality` is deprecated, it will be marked as such.
+
+On an already installed app, you will see a deprecation notice in the `Notes` card in the TrueNAS UI.
+
+In the scenario that a whole app is deprecated (not just a configuration option).
+The deprecation notice will also be visible in the `Discover` tab of the TrueNAS UI, next to the app's title.
+
+**The deprecation period is 3 months.**
+
+After the deprecation period, the app, feature, configuration or functionality will be removed.
+
+## Contributing Applications
+
+The Apps catalog is open for contributions! We provide instructions on how to locally develop and test new applications in our [contributors guide](CONTRIBUTIONS.md).
+
+Questions on the development of applications? Please head over to our [discussions](https://github.com/truenas/apps/discussions) page to ask questions and collaborate with other App Developers.
+
+## Parity Status with Legacy K3's truenas/charts (100% 🚀)
+
+<details>
+<summary>Show Apps List</summary>
+
+| App                  | Train      | Added | Migrated                                                             |
+| -------------------- | ---------- | ----- | -------------------------------------------------------------------- |
+| collabora            | charts     | ✅    | ✅                                                                   |
+| diskoverdata         | charts     | ✅    | ✅                                                                   |
+| elastic-search       | charts     | ✅    | ✅                                                                   |
+| emby                 | charts     | ✅    | ✅                                                                   |
+| home-assistant       | charts     | ✅    | ✅ - [Manual steps needed](https://github.com/truenas/apps/pull/492) |
+| ix-chart             | charts     | ✅    | ✅                                                                   |
+| minio                | charts     | ✅    | ✅                                                                   |
+| netdata              | charts     | ✅    | ✅                                                                   |
+| nextcloud            | charts     | ✅    | ✅                                                                   |
+| photoprism           | charts     | ✅    | ✅                                                                   |
+| plex                 | charts     | ✅    | ✅                                                                   |
+| pihole               | charts     | ✅    | ✅                                                                   |
+| prometheus           | charts     | ✅    | ✅                                                                   |
+| storj                | charts     | ✅    | ✅                                                                   |
+| syncthing            | charts     | ✅    | ✅                                                                   |
+| wg-easy              | charts     | ✅    | ✅                                                                   |
+| actual-budget        | community  | ✅    | ✅                                                                   |
+| adguard-home         | community  | ✅    | ✅                                                                   |
+| audiobookshelf       | community  | ✅    | ✅                                                                   |
+| autobrr              | community  | ✅    | ✅                                                                   |
+| bazarr               | community  | ✅    | ✅                                                                   |
+| briefkasten          | community  | ✅    | ✅                                                                   |
+| castopod             | community  | ✅    | ✅                                                                   |
+| chia                 | community  | ✅    | ✅                                                                   |
+| clamav               | community  | ✅    | ✅                                                                   |
+| cloudflared          | community  | ✅    | ✅                                                                   |
+| dashy                | community  | ✅    | ✅                                                                   |
+| deluge               | community  | ✅    | ✅                                                                   |
+| ddns-updater         | community  | ✅    | ✅                                                                   |
+| distribution         | community  | ✅    | ✅                                                                   |
+| drawio               | community  | ✅    | ✅                                                                   |
+| filebrowser          | community  | ✅    | ✅                                                                   |
+| firefly-iii          | community  | ✅    | ✅                                                                   |
+| flame                | community  | ✅    | ✅                                                                   |
+| freshrss             | community  | ✅    | ✅                                                                   |
+| frigate              | community  | ✅    | ✅                                                                   |
+| fscrawler            | community  | ✅    | ✅                                                                   |
+| gitea                | community  | ✅    | ✅                                                                   |
+| handbrake            | community  | ✅    | ✅                                                                   |
+| grafana              | community  | ✅    | ✅                                                                   |
+| homarr               | community  | ✅    | ✅                                                                   |
+| homer                | community  | ✅    | ✅                                                                   |
+| homepage             | community  | ✅    | ✅                                                                   |
+| immich               | community  | ✅    | ✅                                                                   |
+| invidious            | community  | ✅    | ✅                                                                   |
+| ipfs                 | community  | ✅    | ✅                                                                   |
+| jellyfin             | community  | ✅    | ✅                                                                   |
+| jellyseerr           | community  | ✅    | ✅                                                                   |
+| jenkins              | community  | ✅    | ✅                                                                   |
+| joplin               | community  | ✅    | ✅                                                                   |
+| kapowarr             | community  | ✅    | ✅                                                                   |
+| kavita               | community  | ✅    | ✅                                                                   |
+| komga                | community  | ✅    | ✅                                                                   |
+| lidarr               | community  | ✅    | ✅                                                                   |
+| linkding             | community  | ✅    | ✅                                                                   |
+| listmonk             | community  | ✅    | ✅                                                                   |
+| logseq               | community  | ✅    | ✅                                                                   |
+| mealie               | community  | ✅    | ✅                                                                   |
+| metube               | community  | ✅    | ✅                                                                   |
+| minecraft            | community  | ✅    | ✅                                                                   |
+| mineos               | community  | ✅    | ✅                                                                   |
+| mumble               | community  | ✅    | ✅                                                                   |
+| n8n                  | community  | ✅    | ✅                                                                   |
+| navidrome            | community  | ✅    | ✅                                                                   |
+| nginx-proxy-manager  | community  | ✅    | ✅                                                                   |
+| netbootxyz           | community  | ✅    | ✅                                                                   |
+| node-red             | community  | ✅    | ✅                                                                   |
+| odoo                 | community  | ✅    | ✅                                                                   |
+| omada-controller     | community  | ✅    | ✅                                                                   |
+| organizr             | community  | ✅    | ✅                                                                   |
+| overseerr            | community  | ✅    | ✅                                                                   |
+| palworld             | community  | ✅    | ✅                                                                   |
+| paperless-ngx        | community  | ✅    | ✅                                                                   |
+| passbolt             | community  | ✅    | ✅                                                                   |
+| pgadmin              | community  | ✅    | ✅                                                                   |
+| pigallery2           | community  | ✅    | ✅                                                                   |
+| piwigo               | community  | ✅    | ✅                                                                   |
+| planka               | community  | ✅    | ✅                                                                   |
+| plex-auto-languages  | community  | ✅    | ✅                                                                   |
+| prowlarr             | community  | ✅    | ✅                                                                   |
+| radarr               | community  | ✅    | ✅                                                                   |
+| qbittorrent          | community  | ✅    | ✅                                                                   |
+| readarr              | community  | ✅    | ✅                                                                   |
+| recyclarr            | community  | ✅    | ✅                                                                   |
+| redis                | community  | ✅    | ✅                                                                   |
+| roundcube            | community  | ✅    | ✅                                                                   |
+| rsyncd               | community  | ✅    | ✅                                                                   |
+| rust-desk            | community  | ✅    | ✅                                                                   |
+| sabnzbd              | community  | ✅    | ✅                                                                   |
+| searxng              | community  | ✅    | ✅                                                                   |
+| sftpgo               | community  | ✅    | ✅                                                                   |
+| sonarr               | community  | ✅    | ✅                                                                   |
+| tailscale            | community  | ✅    | ✅ - [Manual steps needed](https://github.com/truenas/apps/pull/641) |
+| tautulli             | community  | ✅    | ✅                                                                   |
+| tdarr                | community  | ✅    | ✅                                                                   |
+| terraria             | community  | ✅    | ✅                                                                   |
+| tftpd-hpa            | community  | ✅    | ✅                                                                   |
+| tiny-media-manager   | community  | ✅    | ✅                                                                   |
+| transmission         | community  | ✅    | ✅                                                                   |
+| twofactor-auth       | community  | ✅    | ✅                                                                   |
+| unifi-controller     | community  | ✅    | ✅                                                                   |
+| unifi-protect-backup | community  | ✅    | ✅                                                                   |
+| vaultwarden          | community  | ✅    | ✅                                                                   |
+| vikunja              | community  | ✅    | ✅                                                                   |
+| webdav               | community  | ✅    | ✅                                                                   |
+| whoogle              | community  | ✅    | ✅                                                                   |
+| wordpress            | community  | ✅    | ✅                                                                   |
+| zerotier             | community  | ✅    | ✅                                                                   |
+| minio                | enterprise | ✅    | ✅                                                                   |
+| syncthing            | enterprise | ✅    | ✅                                                                   |
+
+</details>

+ 15780 - 0
catalog.json

@@ -0,0 +1,15780 @@
+{
+    "stable": {
+        "elastic-search": {
+            "app_readme": "<h1>Elastic Search</h1> <p>If you want to apply additional configuration you can by using additional environment variables.</p> <p>See the <a href=\"https://www.elastic.co/guide/en/elasticsearch/reference/master/docker.html#docker-configuration-methods\">Elastic Search documentation</a> for more information.</p>",
+            "categories": [
+                "monitoring"
+            ],
+            "description": "Elasticsearch is the distributed, RESTful search and analytics engine at the heart of the Elastic Stack.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.elastic.co",
+            "location": "/__w/apps/apps/trains/stable/elastic-search",
+            "latest_version": "1.3.13",
+            "latest_app_version": "9.1.3",
+            "latest_human_version": "9.1.3_1.3.13",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "elastic-search",
+            "recommended": false,
+            "title": "Elastic Search",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "search",
+                "elastic"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://www.elastic.co/",
+                "https://www.elastic.co/guide/en/elasticsearch/reference/master/docker.html#docker-configuration-methods"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/elastic-search/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Elastic Search runs as a non-root user.",
+                    "gid": 1000,
+                    "group_name": "elastic-search",
+                    "uid": 1000,
+                    "user_name": "elastic-search"
+                }
+            ]
+        },
+        "minio": {
+            "app_readme": "<h1>MinIO</h1> <p><a href=\"https://min.io\">MinIO</a> is a High Performance Object Storage released under the AGPLv3 License. It is API compatible with Amazon S3 cloud storage service. Use MinIO to build high performance infrastructure for machine learning, analytics and application data workloads.</p>",
+            "categories": [
+                "storage"
+            ],
+            "description": "The Object Store for AI Data Infrastructure",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://min.io",
+            "location": "/__w/apps/apps/trains/stable/minio",
+            "latest_version": "1.3.8",
+            "latest_app_version": "RELEASE.2025-07-23T15-54-02Z",
+            "latest_human_version": "RELEASE.2025-07-23T15-54-02Z_1.3.8",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "minio",
+            "recommended": false,
+            "title": "MinIO",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "storage",
+                "object-storage",
+                "S3"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/minio/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/minio/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/minio/screenshots/screenshot3.png"
+            ],
+            "sources": [
+                "https://github.com/minio/minio"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/minio/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Minio runs as any non-root user.",
+                    "gid": 473,
+                    "group_name": "minio",
+                    "uid": 473,
+                    "user_name": "minio"
+                }
+            ]
+        },
+        "diskoverdata": {
+            "app_readme": "<h1>Diskover Data</h1> <p><a href=\"https://github.com/diskoverdata/diskover-community\">Diskover Data</a> is used to monitor size/volumes of distributed dataset.</p>",
+            "categories": [
+                "monitoring"
+            ],
+            "description": "Diskover is used to monitor size/volumes of distributed dataset.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/diskoverdata/diskover-community",
+            "location": "/__w/apps/apps/trains/stable/diskoverdata",
+            "latest_version": "1.5.15",
+            "latest_app_version": "2.3.2",
+            "latest_human_version": "2.3.2_1.5.15",
+            "last_update": "2025-09-03 12:56:43",
+            "name": "diskoverdata",
+            "recommended": false,
+            "title": "Diskover Data",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "storage",
+                "monitoring",
+                "management",
+                "discovery"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/diskoverdata/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/diskoverdata/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/diskoverdata/screenshots/screenshot3.png"
+            ],
+            "sources": [
+                "https://github.com/diskoverdata/diskover-community",
+                "https://github.com/linuxserver/docker-diskover"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/diskoverdata/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Diskover Data is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Diskover Data is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Diskover Data is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Diskover Data is able to bind to privileged ports (< 1024)",
+                    "name": "NET_BIND_SERVICE"
+                },
+                {
+                    "description": "Diskover Data is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Diskover Data is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Diskover runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Elastic Search runs as a non-root user.",
+                    "gid": 1000,
+                    "group_name": "elastic-search",
+                    "uid": 1000,
+                    "user_name": "elastic-search"
+                }
+            ]
+        },
+        "syncthing": {
+            "app_readme": "<h1>Syncthing</h1> <p><a href=\"https://syncthing.net/\">Syncthing</a> is a continuous file synchronization program. It synchronizes files between two or more computers in real time, safely protected from prying eyes. Your data is your data alone and you deserve to choose where it is stored, whether it is shared with some third party, and how it's transmitted over the internet.</p> <blockquote> <p><strong>WARNING</strong> Do check out <a href=\"https://docs.syncthing.net/users/faq.html#what-things-are-synced\">official docs</a> to see what is synced.</p> </blockquote>",
+            "categories": [
+                "storage"
+            ],
+            "description": "Syncthing is a continuous file synchronization program.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://syncthing.net/",
+            "location": "/__w/apps/apps/trains/stable/syncthing",
+            "latest_version": "1.2.17",
+            "latest_app_version": "2.0.7",
+            "latest_human_version": "2.0.7_1.2.17",
+            "last_update": "2025-09-05 14:25:56",
+            "name": "syncthing",
+            "recommended": false,
+            "title": "Syncthing",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "sync",
+                "file-sharing",
+                "backup"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/syncthing/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/syncthing/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/syncthing/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/syncthing/screenshots/screenshot4.png"
+            ],
+            "sources": [
+                "https://syncthing.net/",
+                "https://github.com/syncthing/syncthing",
+                "https://hub.docker.com/r/syncthing/syncthing"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/syncthing/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Syncthing is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Syncthing is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Syncthing is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Syncthing is able to set file capabilities on other files",
+                    "name": "SETFCAP"
+                },
+                {
+                    "description": "Syncthing is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Syncthing is able to transfer capabilities between processes",
+                    "name": "SETPCAP"
+                },
+                {
+                    "description": "Syncthing is able to change user ID of processes",
+                    "name": "SETUID"
+                },
+                {
+                    "description": "Syncthing is able to perform system administration operations",
+                    "name": "SYS_ADMIN"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Syncthing runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "nextcloud": {
+            "app_readme": "<h1>Nextcloud</h1> <p><a href=\"https://nextcloud.com/\">Nextcloud</a> is a file sharing server that puts the control and security of your own data back into your hands.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "A file sharing server that puts the control and security of your own data back into your hands.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://nextcloud.com/",
+            "location": "/__w/apps/apps/trains/stable/nextcloud",
+            "latest_version": "2.0.25",
+            "latest_app_version": "31.0.8",
+            "latest_human_version": "31.0.8_2.0.25",
+            "last_update": "2025-09-05 14:25:56",
+            "name": "nextcloud",
+            "recommended": false,
+            "title": "Nextcloud",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "nextcloud",
+                "storage",
+                "sync",
+                "http",
+                "web",
+                "php"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/nextcloud/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/nextcloud/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/nextcloud/screenshots/screenshot3.png"
+            ],
+            "sources": [
+                "https://github.com/nextcloud/docker"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/nextcloud/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Cron, Nextcloud, Nginx are able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Cron, Nextcloud, Nginx are able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Cron, Nextcloud, Nginx are able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Cron, Nextcloud, Nginx are able to bind to privileged ports (< 1024)",
+                    "name": "NET_BIND_SERVICE"
+                },
+                {
+                    "description": "Cron, Nextcloud, Nginx are able to use raw and packet sockets",
+                    "name": "NET_RAW"
+                },
+                {
+                    "description": "Cron, Nextcloud, Nginx are able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Cron, Nextcloud, Nginx are able to change user ID of processes",
+                    "name": "SETUID"
+                },
+                {
+                    "description": "Imaginary is able to modify process scheduling priority",
+                    "name": "SYS_NICE"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Nextcloud runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                },
+                {
+                    "description": "Nginx runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Imaginary runs as non-root user.",
+                    "gid": 568,
+                    "group_name": "imaginary",
+                    "uid": 568,
+                    "user_name": "imaginary"
+                }
+            ]
+        },
+        "pihole": {
+            "app_readme": "<h1>Pi-hole</h1> <p><a href=\"https://pi-hole.net/\">Pi-hole</a> is a black hole for Internet advertisements</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "DNS and Ad-filtering for your network.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://pi-hole.net",
+            "location": "/__w/apps/apps/trains/stable/pihole",
+            "latest_version": "1.3.12",
+            "latest_app_version": "2025.08.0",
+            "latest_human_version": "2025.08.0_1.3.12",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "pihole",
+            "recommended": false,
+            "title": "Pi-hole",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "networking",
+                "dns"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/pihole/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://pi-hole.net/",
+                "https://github.com/pi-hole/docker-pi-hole"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/pihole/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Pi-hole is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Pi-hole is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Pi-hole is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Pi-hole is able to send signals to any process",
+                    "name": "KILL"
+                },
+                {
+                    "description": "Pi-hole is able to perform network administration tasks",
+                    "name": "NET_ADMIN"
+                },
+                {
+                    "description": "Pi-hole is able to bind to privileged ports (< 1024)",
+                    "name": "NET_BIND_SERVICE"
+                },
+                {
+                    "description": "Pi-hole is able to use raw and packet sockets",
+                    "name": "NET_RAW"
+                },
+                {
+                    "description": "Pi-hole is able to set file capabilities on other files",
+                    "name": "SETFCAP"
+                },
+                {
+                    "description": "Pi-hole is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Pi-hole is able to transfer capabilities between processes",
+                    "name": "SETPCAP"
+                },
+                {
+                    "description": "Pi-hole is able to change user ID of processes",
+                    "name": "SETUID"
+                },
+                {
+                    "description": "Pi-hole is able to modify process scheduling priority",
+                    "name": "SYS_NICE"
+                },
+                {
+                    "description": "Pi-hole is able to set system clock and real-time clock",
+                    "name": "SYS_TIME"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Pi-hole runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "collabora": {
+            "app_readme": "<h1>Collabora</h1> <p><a href=\"https://www.collaboraoffice.com/\">Collabora</a> is a collaborative online office suite based on LibreOffice technology</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Collabora is a collaborative online office suite based on LibreOffice technology",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.collaboraoffice.com/",
+            "location": "/__w/apps/apps/trains/stable/collabora",
+            "latest_version": "1.3.14",
+            "latest_app_version": "25.04.5.1.1",
+            "latest_human_version": "25.04.5.1.1_1.3.14",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "collabora",
+            "recommended": false,
+            "title": "Collabora",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "office",
+                "documents",
+                "productivity"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/collabora/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://www.collaboraoffice.com/",
+                "https://github.com/CollaboraOnline/online",
+                "https://hub.docker.com/r/collabora/code"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/collabora/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Collabora, Nginx are able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Collabora, Nginx are able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Collabora, Nginx are able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Collabora is able to create special files using mknod()",
+                    "name": "MKNOD"
+                },
+                {
+                    "description": "Collabora is able to set file capabilities on other files",
+                    "name": "SETFCAP"
+                },
+                {
+                    "description": "Collabora, Nginx are able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Collabora, Nginx are able to change user ID of processes",
+                    "name": "SETUID"
+                },
+                {
+                    "description": "Collabora is able to use chroot() system call",
+                    "name": "SYS_CHROOT"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Collabora runs as non-root user.",
+                    "gid": 101,
+                    "group_name": "cool",
+                    "uid": 100,
+                    "user_name": "cool"
+                },
+                {
+                    "description": "Nginx runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "wg-easy": {
+            "app_readme": "<h1>WG-Easy</h1> <p><a href=\"https://github.com/wg-easy/wg-easy\">WG-Easy (WireGuard Easy)</a> is the easiest way to install &amp; manage WireGuard!</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "WG-Easy is the easiest way to install & manage WireGuard!",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/wg-easy/wg-easy",
+            "location": "/__w/apps/apps/trains/stable/wg-easy",
+            "latest_version": "2.0.7",
+            "latest_app_version": "15.1.0",
+            "latest_human_version": "15.1.0_2.0.7",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "wg-easy",
+            "recommended": false,
+            "title": "WG Easy",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "wireguard",
+                "network",
+                "vpn"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/wg-easy/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/wg-easy/wg-easy"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/wg-easy/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "WG Easy is able to perform network administration tasks",
+                    "name": "NET_ADMIN"
+                },
+                {
+                    "description": "WG Easy is able to use raw and packet sockets",
+                    "name": "NET_RAW"
+                },
+                {
+                    "description": "WG Easy is able to load and unload kernel modules",
+                    "name": "SYS_MODULE"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "WG Easy runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "home-assistant": {
+            "app_readme": "<h1>Home Assistant</h1> <p><a href=\"https://www.home-assistant.io/\">Home Assistant</a> is an open source home automation that puts local control and privacy first.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Home Assistant is an open source home automation that puts local control and privacy first.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.home-assistant.io/",
+            "location": "/__w/apps/apps/trains/stable/home-assistant",
+            "latest_version": "1.5.26",
+            "latest_app_version": "2025.9.1",
+            "latest_human_version": "2025.9.1_1.5.26",
+            "last_update": "2025-09-05 14:25:56",
+            "name": "home-assistant",
+            "recommended": false,
+            "title": "Home Assistant",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "home-automation",
+                "assistant"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/home-assistant/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/home-assistant/home-assistant"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/home-assistant/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Home Assistant is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Home Assistant is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Home Assistant is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Home Assistant is able to bind to privileged ports (< 1024)",
+                    "name": "NET_BIND_SERVICE"
+                },
+                {
+                    "description": "Home Assistant is able to use raw and packet sockets",
+                    "name": "NET_RAW"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Home Assistant runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "ix-app": {
+            "app_readme": "<h1>iX App</h1> <p>iX App is designed to let user deploy a docker image in TrueNAS SCALE with a simple wizard.</p>",
+            "categories": [
+                "custom"
+            ],
+            "description": "An application for deploying simple containers.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.truenas.com/",
+            "location": "/__w/apps/apps/trains/stable/ix-app",
+            "latest_version": "1.2.8",
+            "latest_app_version": "1.2.5",
+            "latest_human_version": "1.2.5_1.2.8",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "ix-app",
+            "recommended": false,
+            "title": "iX App",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [],
+            "screenshots": [],
+            "sources": [],
+            "icon_url": "https://media.sys.truenas.net/apps/ix-chart/icons/icon.webp",
+            "capabilities": [],
+            "run_as_context": []
+        },
+        "prometheus": {
+            "app_readme": "<h1>Prometheus</h1> <p><a href=\"https://prometheus.io/\">Prometheus</a> - Monitoring system and time series database.</p> <p>The configuration file is located at <code>/config/prometheus.yml</code> inside the container. Data is stored at <code>/data</code> inside the container.</p>",
+            "categories": [
+                "monitoring"
+            ],
+            "description": "The Prometheus monitoring system and time series database.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://prometheus.io",
+            "location": "/__w/apps/apps/trains/stable/prometheus",
+            "latest_version": "1.3.8",
+            "latest_app_version": "v3.5.0",
+            "latest_human_version": "v3.5.0_1.3.8",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "prometheus",
+            "recommended": false,
+            "title": "Prometheus",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "metrics",
+                "prometheus"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/prometheus/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/prometheus/prometheus",
+                "https://prometheus.io"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/prometheus/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Prometheus runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "prometheus",
+                    "uid": 568,
+                    "user_name": "prometheus"
+                }
+            ]
+        },
+        "plex": {
+            "app_readme": "<h1>Plex</h1> <p><a href=\"https://plex.tv\">Plex</a> is a media server that allows you to stream your media to any Plex client.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Plex is a media server that allows you to stream your media to any Plex client.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://plex.tv",
+            "location": "/__w/apps/apps/trains/stable/plex",
+            "latest_version": "1.2.10",
+            "latest_app_version": "1.42.1.10060-4e8b05daf",
+            "latest_human_version": "1.42.1.10060-4e8b05daf_1.2.10",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "plex",
+            "recommended": false,
+            "title": "Plex",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "plex",
+                "media",
+                "entertainment",
+                "movies",
+                "series",
+                "tv",
+                "streaming"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/plex/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/plex/screenshots/screenshot2.png"
+            ],
+            "sources": [
+                "https://plex.tv",
+                "https://hub.docker.com/r/plexinc/pms-docker"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/plex/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Plex is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Plex is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Plex is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Plex is able to send signals to any process",
+                    "name": "KILL"
+                },
+                {
+                    "description": "Plex is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Plex is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Plex runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "netdata": {
+            "app_readme": "<h1>Netdata</h1> <p><a href=\"https://www.netdata.cloud/\">Netdata</a> is a fast, easy monitoring and troubleshooting system.</p>",
+            "categories": [
+                "monitoring"
+            ],
+            "description": "Real-time performance monitoring, done right!",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.netdata.cloud/",
+            "location": "/__w/apps/apps/trains/stable/netdata",
+            "latest_version": "1.3.13",
+            "latest_app_version": "v2.6.3",
+            "latest_human_version": "v2.6.3_1.3.13",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "netdata",
+            "recommended": false,
+            "title": "Netdata",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "alerting",
+                "metric",
+                "monitoring"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/netdata/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/netdata/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/netdata/screenshots/screenshot3.png"
+            ],
+            "sources": [
+                "https://www.netdata.cloud/",
+                "https://github.com/netdata/helmchart",
+                "https://hub.docker.com/r/netdata/netdata",
+                "https://github.com/netdata/netdata"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/netdata/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Netdata is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Netdata is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Netdata is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Netdata is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Netdata is able to change user ID of processes",
+                    "name": "SETUID"
+                },
+                {
+                    "description": "Netdata is able to trace and control other processes",
+                    "name": "SYS_PTRACE"
+                },
+                {
+                    "description": "Netdata is able to perform raw I/O operations",
+                    "name": "SYS_RAWIO"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Netdata runs as a root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "storj": {
+            "app_readme": "<h1>Storj</h1> <p><a href=\"https://storj.io/\">Storj</a> - a Storj Storage Node, which is a part of the decentralized cloud storage network Storj.</p>",
+            "categories": [
+                "storage"
+            ],
+            "description": "Share your storage on the internet and earn.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.storj.io",
+            "location": "/__w/apps/apps/trains/stable/storj",
+            "latest_version": "1.3.9",
+            "latest_app_version": "6f87ea801-v1.71.2-go1.18.8",
+            "latest_human_version": "6f87ea801-v1.71.2-go1.18.8_1.3.9",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "storj",
+            "recommended": false,
+            "title": "Storj",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "storage",
+                "networking",
+                "financial",
+                "file-sharing"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/storj/screenshots/screenshot1.jpeg"
+            ],
+            "sources": [
+                "https://www.storj.io"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/storj/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Storj runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "storj",
+                    "uid": 568,
+                    "user_name": "storj"
+                }
+            ]
+        },
+        "emby": {
+            "app_readme": "<h1>Emby</h1> <p><a href=\"https://emby.media/\">Emby</a> is designed to help you manage your personal media library, such as home videos and photos</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Emby is designed to help you manage your personal media library, such as home videos and photos",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://emby.media/",
+            "location": "/__w/apps/apps/trains/stable/emby",
+            "latest_version": "1.3.25",
+            "latest_app_version": "4.9.1.25",
+            "latest_human_version": "4.9.1.25_1.3.25",
+            "last_update": "2025-09-05 14:25:56",
+            "name": "emby",
+            "recommended": false,
+            "title": "Emby Server",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "emby",
+                "media",
+                "entertainment",
+                "movies",
+                "series",
+                "tv",
+                "streaming"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/emby/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/emby/screenshots/screenshot2.png"
+            ],
+            "sources": [
+                "https://emby.media/",
+                "https://hub.docker.com/r/emby/embyserver"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/emby/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Emby is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Emby is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Emby is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Emby is able to send signals to any process",
+                    "name": "KILL"
+                },
+                {
+                    "description": "Emby is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Emby is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Emby runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "photoprism": {
+            "app_readme": "<h1>PhotoPrism</h1> <p><a href=\"https://photoprism.app/\">PhotoPrism</a> is a server-based application for browsing, organizing and sharing your personal photo collection.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "AI-powered app for browsing, organizing & sharing your photo collection.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://photoprism.app/",
+            "location": "/__w/apps/apps/trains/stable/photoprism",
+            "latest_version": "1.3.8",
+            "latest_app_version": "250707",
+            "latest_human_version": "250707_1.3.8",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "photoprism",
+            "recommended": false,
+            "title": "Photoprism",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "photos",
+                "image"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/photoprism/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/photoprism/screenshots/screenshot2.png"
+            ],
+            "sources": [
+                "https://photoprism.app/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/photoprism/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Photoprism is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Photoprism is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Photoprism is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Photoprism is able to send signals to any process",
+                    "name": "KILL"
+                },
+                {
+                    "description": "Photoprism is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Photoprism is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Photoprism runs as a root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        }
+    },
+    "enterprise": {
+        "minio": {
+            "app_readme": "<h1>MinIO</h1> <p><a href=\"https://min.io\">MinIO</a> is a High Performance Object Storage released under the AGPLv3 License. It is API compatible with Amazon S3 cloud storage service. Use MinIO to build high performance infrastructure for machine learning, analytics and application data workloads.</p>",
+            "categories": [
+                "storage"
+            ],
+            "description": "High Performance, Kubernetes Native Object Storage",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://min.io",
+            "location": "/__w/apps/apps/trains/enterprise/minio",
+            "latest_version": "1.3.7",
+            "latest_app_version": "RELEASE.2025-04-22T22-12-26Z",
+            "latest_human_version": "RELEASE.2025-04-22T22-12-26Z_1.3.7",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "minio",
+            "recommended": false,
+            "title": "MinIO",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "object storage",
+                "minio",
+                "cloud",
+                "s3"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/adguard-home/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/adguard-home/screenshots/screenshot2.png"
+            ],
+            "sources": [
+                "https://github.com/minio/minio"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/minio/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "MinIO runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "minio",
+                    "uid": 568,
+                    "user_name": "minio"
+                }
+            ]
+        },
+        "syncthing": {
+            "app_readme": "<h1>Syncthing</h1> <p><a href=\"https://syncthing.net/\">Syncthing</a> is a file synchronization program.</p> <p>At each startup of the application, the following settings are applied:</p> <ul> <li>Disable automatic upgrades</li> <li>Disable anonymous usage reporting</li> <li>Disable NAT traversal</li> <li>Disable global discovery</li> <li>Disable local discovery</li> <li>Disable relaying</li> <li>Disable announcing LAN addresses</li> </ul> <p>Additionally, the following defaults are set for new syncthing \"folders\":</p> <ul> <li>Max total size of <code>xattr</code>: 10 MiB</li> <li>Max size per <code>xattr</code>: 2 MiB</li> <li>Enable <code>send</code> and <code>sync</code> of <code>xattr</code></li> <li>Enable <code>send</code> and <code>sync</code> of <code>ownership</code></li> </ul>",
+            "categories": [
+                "storage"
+            ],
+            "description": "Syncthing is a continuous file synchronization program.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://syncthing.net",
+            "location": "/__w/apps/apps/trains/enterprise/syncthing",
+            "latest_version": "1.2.9",
+            "latest_app_version": "1.30.0",
+            "latest_human_version": "1.30.0_1.2.9",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "syncthing",
+            "recommended": false,
+            "title": "Syncthing",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "sync",
+                "file-sharing"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/syncthing/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/syncthing/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/syncthing/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/syncthing/screenshots/screenshot4.png"
+            ],
+            "sources": [
+                "https://syncthing.net/",
+                "https://github.com/syncthing/syncthing",
+                "https://hub.docker.com/r/syncthing/syncthing"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/syncthing/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Syncthing is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Syncthing is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Syncthing is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Syncthing is able to set file capabilities on other files",
+                    "name": "SETFCAP"
+                },
+                {
+                    "description": "Syncthing is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Syncthing is able to transfer capabilities between processes",
+                    "name": "SETPCAP"
+                },
+                {
+                    "description": "Syncthing is able to change user ID of processes",
+                    "name": "SETUID"
+                },
+                {
+                    "description": "Syncthing is able to perform system administration operations",
+                    "name": "SYS_ADMIN"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Syncthing runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "asigra-ds-system": {
+            "app_readme": "<h1>Asigra DS-System</h1> <p><a href=\"https://hub.docker.com/r/asigra/ds-system\">Asigra DS-System</a> - DS-System software enables you to offer a robust, scalable service to multiple customers.</p> <p>Ease of use comes from the agentless architecture: customers only need to install the DS-Client on one LAN computer, thereby eliminating the need to install software on each target backup / restore computer. As long as the DS-Client is networked with the target backup / restore computers, you will be able to browse data, back it up, and restore it as required. Customers can take advantage of automatic and unattended backups for data environments ranging from single-user standalone computers up to enterprise-wide LANs and WANs. During backups, the DS-Client extracts changed data, compresses, and encrypts the items specified for backup. Only new or modified data is backed up, thereby accelerating the backup transmission time. The backup data is sent via the Internet, Intranet, or direct dial-up to the secure, off-site Data Center that hosts the DS-System Vault. Restores are performed on demand, via the same DS-Client, once the DS-Client's security measures have been cleared.</p>",
+            "categories": [
+                "storage"
+            ],
+            "description": "DS-System software enables you to offer a robust, scalable service to multiple customers. Ease of use comes from the agentless architecture: customers only need to install the DS-Client on one LAN computer, thereby eliminating the need to install software on each target backup / restore computer",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://hub.docker.com/r/asigra/ds-system",
+            "location": "/__w/apps/apps/trains/enterprise/asigra-ds-system",
+            "latest_version": "1.1.14",
+            "latest_app_version": "14.2.0.9",
+            "latest_human_version": "14.2.0.9_1.1.14",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "asigra-ds-system",
+            "recommended": false,
+            "title": "Asigra DS-System",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "backup",
+                "restore",
+                "asigra"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://hub.docker.com/r/asigra/ds-system"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/asigra-ds-system/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "DS System is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "DS System is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "DS System is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Asigra DS-System runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                },
+                {
+                    "description": "HAProxy runs as non-root user.",
+                    "gid": 568,
+                    "group_name": "haproxy",
+                    "uid": 568,
+                    "user_name": "haproxy"
+                }
+            ]
+        },
+        "ix-remote-assist": {
+            "app_readme": "<h1>Remote Assist</h1> <p><a href=\"https://truenas.com\">Remote Assist</a> iX support remote assistance for Enterprise Systems</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "Invite the TrueNAS team to remotely assist with your TrueNAS system.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://truenas.com",
+            "location": "/__w/apps/apps/trains/enterprise/ix-remote-assist",
+            "latest_version": "1.0.18",
+            "latest_app_version": "v1.86.5",
+            "latest_human_version": "v1.86.5_1.0.18",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "ix-remote-assist",
+            "recommended": false,
+            "title": "Remote Assist",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "Remote Assistance",
+                "VPN"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://truenas.com/",
+                "https://hub.docker.com/r/tailscale/tailscale"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/ix-chart/icons/icon.webp",
+            "capabilities": [
+                {
+                    "description": "Remote Assist is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Remote Assist is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Remote Assist is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Remote Assist is able to perform network administration tasks",
+                    "name": "NET_ADMIN"
+                },
+                {
+                    "description": "Remote Assist is able to use raw and packet sockets",
+                    "name": "NET_RAW"
+                },
+                {
+                    "description": "Remote Assist is able to load and unload kernel modules",
+                    "name": "SYS_MODULE"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Remote Assist runs as a root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        }
+    },
+    "community": {
+        "lazylibrarian": {
+            "app_readme": "<h1>LazyLibrarian</h1> <p><a href=\"https://gitlab.com/LazyLibrarian/LazyLibrarian\">LazyLibrarian</a> is a program to follow authors and grab metadata for all your digital reading needs.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "LazyLibrarian is a program to follow authors and grab metadata for all your digital reading needs.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://gitlab.com/LazyLibrarian/LazyLibrarian",
+            "location": "/__w/apps/apps/trains/community/lazylibrarian",
+            "latest_version": "1.0.7",
+            "latest_app_version": "e65abd21-ls144",
+            "latest_human_version": "e65abd21-ls144_1.0.7",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "lazylibrarian",
+            "recommended": false,
+            "title": "LazyLibrarian",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "ebooks"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/linuxserver/docker-lazylibrarian",
+                "https://gitlab.com/LazyLibrarian/LazyLibrarian"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/lazylibrarian/icons/icon.webp",
+            "capabilities": [
+                {
+                    "description": "LazyLibrarian is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "LazyLibrarian is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "LazyLibrarian is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "LazyLibrarian is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "LazyLibrarian is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "LazyLibrarian runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "gaseous-server": {
+            "app_readme": "<h1>Gaseous-Server</h1> <p><a href=\"https://github.com/gaseous-project/gaseous-server\">Gaseous-Server</a> - Host your ROMS library and emulate them in-browser.</p> <p>This is the server for the Gaseous system. It offers ROM and title management, as well as some basic in browser emulation of those ROMs.</p>",
+            "categories": [
+                "games"
+            ],
+            "description": "This is the server for the Gaseous system. It offers ROM and title management, as well as some basic in browser emulation of those ROMs.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/gaseous-project/gaseous-server",
+            "location": "/__w/apps/apps/trains/community/gaseous-server",
+            "latest_version": "1.1.12",
+            "latest_app_version": "v1.7.14",
+            "latest_human_version": "v1.7.14_1.1.12",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "gaseous-server",
+            "recommended": false,
+            "title": "Gaseous Server",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "games",
+                "emulation"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/gaseous-server/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/gaseous-server/screenshots/screenshot2.png"
+            ],
+            "sources": [
+                "https://github.com/gaseous-project/gaseous-server"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/gaseous-server/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Gaseous Server is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Gaseous Server is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Gaseous Server is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Gaseous Server runs as the root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "MariaDB runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "mariadb",
+                    "uid": 999,
+                    "user_name": "mariadb"
+                }
+            ]
+        },
+        "channels-dvr": {
+            "app_readme": "<h1>Channels DVR</h1> <p><a href=\"https://getchannels.com/dvr-server/\">Channels DVR</a> Watch and record your favorite shows and movies from every TV and device. From your living room, second home, hotel room, or even on your commute, Channels lets you watch your personal media your way.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Watch and record your favorite shows and movies from every TV and device. From your living room, second home, hotel room, or even on your commute, Channels lets you watch your personal media your way.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://getchannels.com/dvr-server/",
+            "location": "/__w/apps/apps/trains/community/channels-dvr",
+            "latest_version": "1.0.6",
+            "latest_app_version": "latest",
+            "latest_human_version": "latest_1.0.6",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "channels-dvr",
+            "recommended": false,
+            "title": "Channels DVR",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "channels",
+                "livetv",
+                "streaming"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://hub.docker.com/r/fancybits/channels-dvr"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/channels-dvr/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Channels DVR runs as non-root user.",
+                    "gid": 568,
+                    "group_name": "channels",
+                    "uid": 568,
+                    "user_name": "channels"
+                }
+            ]
+        },
+        "handbrake-web": {
+            "app_readme": "<h1>Handbrake Web</h1> <p><a href=\"https://github.com/TheNickOfTime/handbrake-web\">Handbrake Web</a> is a self-hosted platform to use <a href=\"https://handbrake.fr/\">HandBrake</a> on your headless devices via a bespoke web interface.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "A self-hosted platform to use HandBrake on your headless devices via a bespoke web interface. Harness the processing power of multiple devices to work on a single queue.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/TheNickOfTime/handbrake-web",
+            "location": "/__w/apps/apps/trains/community/handbrake-web",
+            "latest_version": "1.0.14",
+            "latest_app_version": "0.7.3",
+            "latest_human_version": "0.7.3_1.0.14",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "handbrake-web",
+            "recommended": false,
+            "title": "Handbrake Web",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "video",
+                "transcoder"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/handbrake-web/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/handbrake-web/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/handbrake-web/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/handbrake-web/screenshots/screenshot4.png",
+                "https://media.sys.truenas.net/apps/handbrake-web/screenshots/screenshot5.png",
+                "https://media.sys.truenas.net/apps/handbrake-web/screenshots/screenshot6.png"
+            ],
+            "sources": [
+                "https://github.com/TheNickOfTime/handbrake-web"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/handbrake-web/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Handbrake Web runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "handbrake-web",
+                    "uid": 568,
+                    "user_name": "handbrake-web"
+                }
+            ]
+        },
+        "dockge": {
+            "app_readme": "<h1>Dockge</h1> <p><a href=\"https://dockge.kuma.pet\">Dockge</a> is a fancy, easy-to-use and reactive self-hosted docker compose.yaml stack-oriented manager</p>",
+            "categories": [
+                "management"
+            ],
+            "description": "A fancy, easy-to-use and reactive self-hosted docker compose.yaml stack-oriented manager",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://dockge.kuma.pet",
+            "location": "/__w/apps/apps/trains/community/dockge",
+            "latest_version": "1.2.7",
+            "latest_app_version": "1.5.0",
+            "latest_human_version": "1.5.0_1.2.7",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "dockge",
+            "recommended": false,
+            "title": "Dockge",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "docker",
+                "compose"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/dockge/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://dockge.kuma.pet",
+                "https://github.com/louislam/dockge"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/dockge/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Dockge is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Dockge is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Dockge is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Dockge is able to send signals to any process",
+                    "name": "KILL"
+                },
+                {
+                    "description": "Dockge is able to set file capabilities on other files",
+                    "name": "SETFCAP"
+                },
+                {
+                    "description": "Dockge is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Dockge is able to transfer capabilities between processes",
+                    "name": "SETPCAP"
+                },
+                {
+                    "description": "Dockge is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Dockge runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "change-detection": {
+            "app_readme": "<h1>ChangeDetection.io</h1> <p><a href=\"https://changedetection.io/\">ChangeDetection.io</a> is a Free and Open-Source Python website change detection and stock monitoring tool.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Page change monitoring with alerts a breeze. The best way to monitor website changes.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://changedetection.io/",
+            "location": "/__w/apps/apps/trains/community/change-detection",
+            "latest_version": "1.0.19",
+            "latest_app_version": "0.50.11",
+            "latest_human_version": "0.50.11_1.0.19",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "change-detection",
+            "recommended": false,
+            "title": "ChangeDetection.io",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "change-detection",
+                "page monitoring",
+                "price tracker",
+                "stock tracker"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/change-detection/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/dgtlmoon/changedetection.io"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/change-detection/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "ChangeDetection.io runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "change-detection",
+                    "uid": 568,
+                    "user_name": "change-detection"
+                }
+            ]
+        },
+        "luanti": {
+            "app_readme": "<h1>Luanti</h1> <p><a href=\"https://www.luanti.org/\">Luanti</a> is an open source voxel game-creation platform with easy modding and game creation</p>",
+            "categories": [
+                "games"
+            ],
+            "description": "Luanti is an open source voxel game-creation platform with easy modding and game creation",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.luanti.org/",
+            "location": "/__w/apps/apps/trains/community/luanti",
+            "latest_version": "1.0.4",
+            "latest_app_version": "5.13.0",
+            "latest_human_version": "5.13.0_1.0.4",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "luanti",
+            "recommended": false,
+            "title": "Luanti",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "minetest",
+                "luanti"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://www.luanti.org/",
+                "https://github.com/luanti-org/luanti"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/luanti/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Luanti runs as non-root user.",
+                    "gid": 30000,
+                    "group_name": "luanti",
+                    "uid": 30000,
+                    "user_name": "luanti"
+                }
+            ]
+        },
+        "n8n": {
+            "app_readme": "<h1>n8n</h1> <p><a href=\"https://n8n.io/\">n8n</a> is an extendable workflow automation tool.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "n8n is an extendable workflow automation tool.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://n8n.io/",
+            "location": "/__w/apps/apps/trains/community/n8n",
+            "latest_version": "1.6.41",
+            "latest_app_version": "1.110.1",
+            "latest_human_version": "1.110.1_1.6.41",
+            "last_update": "2025-09-03 12:56:43",
+            "name": "n8n",
+            "recommended": false,
+            "title": "n8n",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "workflows",
+                "automation"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/n8n/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/n8n/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/n8n/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/n8n/screenshots/screenshot4.png"
+            ],
+            "sources": [
+                "https://github.com/n8n-io/n8n",
+                "https://hub.docker.com/r/n8nio/n8n"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/n8n/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "n8n runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "n8n",
+                    "uid": 568,
+                    "user_name": "n8n"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                }
+            ]
+        },
+        "jellystat": {
+            "app_readme": "<h1>Jellystat</h1> <p><a href=\"https://github.com/CyferShepard/Jellystat\">Jellystat</a> is a free and open source Statistics App for Jellyfin.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Jellystat is a free and open source Statistics App for Jellyfin",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/CyferShepard/Jellystat",
+            "location": "/__w/apps/apps/trains/community/jellystat",
+            "latest_version": "1.0.9",
+            "latest_app_version": "1.1.6",
+            "latest_human_version": "1.1.6_1.0.9",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "jellystat",
+            "recommended": false,
+            "title": "Jellystat",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "statistics",
+                "jellyfin"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/jellystat/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/jellystat/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/jellystat/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/jellystat/screenshots/screenshot4.png",
+                "https://media.sys.truenas.net/apps/jellystat/screenshots/screenshot5.png"
+            ],
+            "sources": [
+                "https://github.com/CyferShepard/Jellystat",
+                "https://hub.docker.com/r/cyfershepard/jellystat"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/jellystat/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Jellystat runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "whoogle": {
+            "app_readme": "<h1>Whoogle</h1> <p><a href=\"https://github.com/benbusby/whoogle-search\">Whoogle</a> is a self-hosted, ad-free, privacy-respecting metasearch engine</p> <p>See <a href=\"https://github.com/benbusby/whoogle-search?tab=readme-ov-file#environment-variables\">Whoogle's Docs</a> for a list of available environment variables. Note that all configuration via WebUI will be reset if the container is restarted. Only config changes made via environment variables will persist.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Whoogle is a self-hosted, ad-free, privacy-respecting metasearch engine",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/benbusby/whoogle-search",
+            "location": "/__w/apps/apps/trains/community/whoogle",
+            "latest_version": "1.2.6",
+            "latest_app_version": "0.9.3",
+            "latest_human_version": "0.9.3_1.2.6",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "whoogle",
+            "recommended": false,
+            "title": "Whoogle",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "search",
+                "engine"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/whoogle/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/whoogle/screenshots/screenshot2.png"
+            ],
+            "sources": [
+                "https://github.com/benbusby/whoogle-search",
+                "https://hub.docker.com/r/benbusby/whoogle-search"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/whoogle/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Whoogle runs as non-root user.",
+                    "gid": 927,
+                    "group_name": "whoogle",
+                    "uid": 927,
+                    "user_name": "whoogle"
+                }
+            ]
+        },
+        "duplicati": {
+            "app_readme": "<h1>Duplicati</h1> <p><a href=\"https://duplicati.com\">Duplicati</a> is a backup client that securely stores encrypted backups in the cloud!</p>",
+            "categories": [
+                "backup"
+            ],
+            "description": "Store securely encrypted backups in the cloud!",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "http://duplicati.com",
+            "location": "/__w/apps/apps/trains/community/duplicati",
+            "latest_version": "1.0.15",
+            "latest_app_version": "2.1.0.5",
+            "latest_human_version": "2.1.0.5_1.0.15",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "duplicati",
+            "recommended": false,
+            "title": "Duplicati",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "backup"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/duplicati/duplicati",
+                "https://hub.docker.com/r/duplicati/duplicati"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/duplicati/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Duplicati runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "duplicati",
+                    "uid": 568,
+                    "user_name": "duplicati"
+                }
+            ]
+        },
+        "keycloak": {
+            "app_readme": "<h1>Keycloak</h1> <p><a href=\"https://www.keycloak.org/\">Keycloak</a> is an Open Source Identity and Access Management For Modern Applications and Services</p>",
+            "categories": [
+                "security"
+            ],
+            "description": "Open Source Identity and Access Management For Modern Applications and Services",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.keycloak.org",
+            "location": "/__w/apps/apps/trains/community/keycloak",
+            "latest_version": "1.0.6",
+            "latest_app_version": "26.3.3",
+            "latest_human_version": "26.3.3_1.0.6",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "keycloak",
+            "recommended": false,
+            "title": "Keycloak",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "authentication",
+                "authorization",
+                "sso"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/keycloak/keycloak",
+                "https://www.keycloak.org/server/all-config#category-database"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/keycloak/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Keycloak runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "keycloak",
+                    "uid": 1000,
+                    "user_name": "keycloak"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "wger": {
+            "app_readme": "<h1>Wger</h1> <p><a href=\"https://wger.de\">Wger</a> is a self hosted FLOSS fitness/workout, nutrition and weight tracker.</p>",
+            "categories": [
+                "health"
+            ],
+            "description": "Wger is a self hosted FLOSS fitness/workout, nutrition and weight tracker",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://wger.de",
+            "location": "/__w/apps/apps/trains/community/wger",
+            "latest_version": "1.0.15",
+            "latest_app_version": "2.4-dev",
+            "latest_human_version": "2.4-dev_1.0.15",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "wger",
+            "recommended": false,
+            "title": "Wger",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "fitness",
+                "nutrition",
+                "tracker",
+                "workout"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/wger-project/wger",
+                "https://github.com/wger-project/docker"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/wger/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Wger runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "wger",
+                    "uid": 1000,
+                    "user_name": "wger"
+                },
+                {
+                    "description": "Nginx runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "nginx",
+                    "uid": 1000,
+                    "user_name": "nginx"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                }
+            ]
+        },
+        "homer": {
+            "app_readme": "<h1>Homer</h1> <p><a href=\"https://github.com/bastienwirtz/homer\">Homer</a> is a dead simple static HOMepage for your servER to keep your services on hand, from a simple yaml configuration file.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Homer is a dead simple static HOMepage for your servER to keep your services on hand, from a simple yaml configuration file.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/bastienwirtz/homer",
+            "location": "/__w/apps/apps/trains/community/homer",
+            "latest_version": "2.2.7",
+            "latest_app_version": "v25.08.1",
+            "latest_human_version": "v25.08.1_2.2.7",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "homer",
+            "recommended": false,
+            "title": "Homer",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/homer/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://hub.docker.com/r/b4bz/homer",
+                "https://github.com/bastienwirtz/homer"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/homer/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Homer runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "homer",
+                    "uid": 568,
+                    "user_name": "homer"
+                }
+            ]
+        },
+        "pterodactyl-panel": {
+            "app_readme": "<h1>Pterodactyl</h1> <p><a href=\"https://pterodactyl.io/\">Pterodactyl</a> is a free, open-source game server management panel built with PHP, React, and Go. Designed with security in mind, Pterodactyl runs all game servers in isolated Docker containers while exposing a beautiful and intuitive UI to end users.</p>",
+            "categories": [
+                "games"
+            ],
+            "description": "Pterodactyl is a free, open-source game server management panel built with PHP, React, and Go. Designed with security in mind, Pterodactyl runs all game servers in isolated Docker containers while exposing a beautiful and intuitive UI to end users.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://pterodactyl.io/",
+            "location": "/__w/apps/apps/trains/community/pterodactyl-panel",
+            "latest_version": "1.0.18",
+            "latest_app_version": "v1.11.11",
+            "latest_human_version": "v1.11.11_1.0.18",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "pterodactyl-panel",
+            "recommended": false,
+            "title": "Pterodactyl Panel",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "pterodactyl",
+                "games",
+                "servers"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/pterodactyl/panel"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/pterodactyl-panel/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Pterodactyl is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Pterodactyl is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Pterodactyl is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Pterodactyl is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Pterodactyl is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Pterodactyl Panel runs as a root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "MariaDB runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "mariadb",
+                    "uid": 999,
+                    "user_name": "mariadb"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                }
+            ]
+        },
+        "gramps-web": {
+            "app_readme": "<h1>Gramps Web</h1> <p><a href=\"https://www.grampsweb.org/\">Gramps Web</a> is a free, open-source genealogy system for building your family tree \u2013 together. Use it standalone or as a companion to Gramps Desktop, with full control over your data and privacy as the top priority.</p>",
+            "categories": [
+                "management"
+            ],
+            "description": "is a free, open-source genealogy system for building your family tree \u2013 together. Use it standalone or as a companion to Gramps Desktop, with full control over your data and privacy as the top priority.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.grampsweb.org/",
+            "location": "/__w/apps/apps/trains/community/gramps-web",
+            "latest_version": "1.1.14",
+            "latest_app_version": "25.8.0",
+            "latest_human_version": "25.8.0_1.1.14",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "gramps-web",
+            "recommended": false,
+            "title": "Gramps Web",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "gramps",
+                "genealogy",
+                "familytree"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/gramps-project/gramps-web"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/gramps-web/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Gramps Web runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                }
+            ]
+        },
+        "castopod": {
+            "app_readme": "<h1>Castopod</h1> <p><a href=\"https://castopod.org\">Castopod</a> is an open-source hosting platform made for podcasters who want engage and interact with their audience.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Castopod is an open-source hosting platform made for podcasters who want engage and interact with their audience.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://castopod.org",
+            "location": "/__w/apps/apps/trains/community/castopod",
+            "latest_version": "1.2.13",
+            "latest_app_version": "1.13.5",
+            "latest_human_version": "1.13.5_1.2.13",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "castopod",
+            "recommended": false,
+            "title": "Castopod",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "podcast"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/castopod/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://hub.docker.com/r/castopod/castopod",
+                "https://code.castopod.org/adaures/castopod"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/castopod/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Castopod, Castopod Web are able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Castopod is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Castopod is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Castopod, Castopod Web are able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Castopod, Castopod Web are able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Castopod runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "MariaDB runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "mariadb",
+                    "uid": 999,
+                    "user_name": "mariadb"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                }
+            ]
+        },
+        "portainer": {
+            "app_readme": "<h1>Portainer</h1> <p><a href=\"https://www.portainer.io\">Portainer</a> is a lightweight service delivery platform for containerized applications that can be used to manage Docker, Swarm, Kubernetes and ACI environments.</p>",
+            "categories": [
+                "management"
+            ],
+            "description": "Container management made easy",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.portainer.io",
+            "location": "/__w/apps/apps/trains/community/portainer",
+            "latest_version": "1.4.16",
+            "latest_app_version": "2.33.1",
+            "latest_human_version": "2.33.1_1.4.16",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "portainer",
+            "recommended": false,
+            "title": "Portainer",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "docker",
+                "compose",
+                "container"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/portainer/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/portainer/screenshots/screenshot2.png"
+            ],
+            "sources": [
+                "https://www.portainer.io",
+                "https://github.com/portainer/portainer"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/portainer/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Portainer is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Portainer is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Portainer is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Portainer is able to send signals to any process",
+                    "name": "KILL"
+                },
+                {
+                    "description": "Portainer is able to set file capabilities on other files",
+                    "name": "SETFCAP"
+                },
+                {
+                    "description": "Portainer is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Portainer is able to transfer capabilities between processes",
+                    "name": "SETPCAP"
+                },
+                {
+                    "description": "Portainer is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Portainer runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "minio-console": {
+            "app_readme": "<h1>Minio Console</h1> <p><a href=\"https://github.com/huncrys/minio-console\">Minio Console</a> is a simple UI for MinIO Object Storage.</p>",
+            "categories": [
+                "storage"
+            ],
+            "description": "Simple UI for MinIO Object Storage",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/huncrys/minio-console",
+            "location": "/__w/apps/apps/trains/community/minio-console",
+            "latest_version": "1.0.4",
+            "latest_app_version": "v1.8.1",
+            "latest_human_version": "v1.8.1_1.0.4",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "minio-console",
+            "recommended": false,
+            "title": "MinIO Console",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "minio",
+                "console"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/minio/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/minio/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/minio/screenshots/screenshot3.png"
+            ],
+            "sources": [
+                "https://github.com/huncrys/minio-console"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/minio-console/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Minio Console runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "minio-console",
+                    "uid": 568,
+                    "user_name": "minio-console"
+                }
+            ]
+        },
+        "minecraft": {
+            "app_readme": "<h1>Minecraft</h1> <p><a href=\"https://www.minecraft.net/en-us\">Minecraft</a> is a sandbox game</p> <p>Depending on the <code>Type</code> of server selected, you might need to add additional custom environment variables to the application.</p> <p>More info can be found <a href=\"https://itzg.github.io/docker-minecraft-docs/java/server-types/bukkit-spigot/\">here</a> Select the type on the sidebar.</p> <p>Note that some values are only applicable during the world generation. More info can be found <a href=\"https://itzg.github.io/docker-minecraft-docs/\">here</a></p>",
+            "categories": [
+                "games"
+            ],
+            "description": "Minecraft dedicated server for Java platform hosts a world for multiplayer game.\nPlayers can join the server using the Java client on Desktops.\n",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.minecraft.net/en-us",
+            "location": "/__w/apps/apps/trains/community/minecraft",
+            "latest_version": "1.13.12",
+            "latest_app_version": "2025.9.0",
+            "latest_human_version": "2025.9.0_1.13.12",
+            "last_update": "2025-09-02 15:29:51",
+            "name": "minecraft",
+            "recommended": false,
+            "title": "Minecraft Server (Java)",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "world",
+                "building"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://www.minecraft.net/en-us",
+                "https://github.com/itzg/docker-minecraft-server"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/minecraft/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Minecraft is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Minecraft is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Minecraft is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Minecraft is able to use raw and packet sockets",
+                    "name": "NET_RAW"
+                },
+                {
+                    "description": "Minecraft is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Minecraft is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Minecraft runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "filestash": {
+            "app_readme": "<h1>Filestash</h1> <p><a href=\"https://github.com/mickael-kerjean/filestash\">Filestash</a> - a file manager / web client</p> <p>Filestash is a file manager / web client for SFTP, S3, FTP, WebDAV, Git, Minio, LDAP, CalDAV, CardDAV, Mysql, Backblaze, ...</p>",
+            "categories": [
+                "storage"
+            ],
+            "description": "Filestash is a file manager / web client for SFTP, S3, FTP, WebDAV, Git, Minio, LDAP, CalDAV, CardDAV, Mysql, Backblaze, ...",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.filestash.app/",
+            "location": "/__w/apps/apps/trains/community/filestash",
+            "latest_version": "1.1.10",
+            "latest_app_version": "latest",
+            "latest_human_version": "latest_1.1.10",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "filestash",
+            "recommended": false,
+            "title": "Filestash",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "storage",
+                "file manager"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/filestash/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/filestash/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/filestash/screenshots/screenshot3.png"
+            ],
+            "sources": [
+                "https://www.filestash.app/",
+                "https://github.com/mickael-kerjean/filestash"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/filestash/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Filestash runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "filestash",
+                    "uid": 1000,
+                    "user_name": "filestash"
+                }
+            ]
+        },
+        "webdav": {
+            "app_readme": "<h1>WebDAV</h1> <p><a href=\"http://webdav.org/\">WebDAV</a> is a set of extensions to the HTTP protocol which allows users to collaboratively edit and manage files on remote web servers.</p>",
+            "categories": [
+                "storage"
+            ],
+            "description": "WebDAV is a set of extensions to the HTTP protocol which allows users to collaboratively edit and manage files on remote web servers.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "http://www.webdav.org/",
+            "location": "/__w/apps/apps/trains/community/webdav",
+            "latest_version": "1.2.6",
+            "latest_app_version": "2.4.65",
+            "latest_human_version": "2.4.65_1.2.6",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "webdav",
+            "recommended": false,
+            "title": "WebDAV",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "webdav",
+                "file-sharing"
+            ],
+            "screenshots": [],
+            "sources": [
+                "http://www.webdav.org/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/webdav/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "WebDAV runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "webdav",
+                    "uid": 568,
+                    "user_name": "webdav"
+                }
+            ]
+        },
+        "sickgear": {
+            "app_readme": "<h1>SickGear</h1> <p><a href=\"https://github.com/SickGear/SickGear\">SickGear</a> has proven the most reliable stable TV fork of the great Sick-Beard to fully automate TV enjoyment with innovation.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "SickGear has proven the most reliable stable TV fork of the great Sick-Beard to fully automate TV enjoyment with innovation.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/SickGear/SickGear",
+            "location": "/__w/apps/apps/trains/community/sickgear",
+            "latest_version": "1.0.3",
+            "latest_app_version": "latest",
+            "latest_human_version": "latest_1.0.3",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "sickgear",
+            "recommended": false,
+            "title": "SickGear",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "sickgear",
+                "sickbeard",
+                "tv"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/SickGear/SickGear"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/sickgear/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "SickGear is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "SickGear is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "SickGear is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "SickGear is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "SickGear is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "SickGear runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "unpackerr": {
+            "app_readme": "<h1>Unpackerr</h1> <p><a href=\"https://unpackerr.zip\">Unpackerr</a> extracts downloads for Radarr, Sonarr, Lidarr, Readarr, and/or a Watch</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Extracts downloads for Radarr, Sonarr, Lidarr, Readarr, and/or a Watch folder - Deletes extracted files after import",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://unpackerr.zip/",
+            "location": "/__w/apps/apps/trains/community/unpackerr",
+            "latest_version": "1.0.4",
+            "latest_app_version": "0.14.5",
+            "latest_human_version": "0.14.5_1.0.4",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "unpackerr",
+            "recommended": false,
+            "title": "Unpackerr",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "downloads",
+                "unpack",
+                "extraction"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/Unpackerr/unpackerr"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/unpackerr/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Unpackerr runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "unpackerr",
+                    "uid": 568,
+                    "user_name": "unpackerr"
+                }
+            ]
+        },
+        "joplin": {
+            "app_readme": "<h1>Joplin</h1> <p><a href=\"https://joplinapp.org\">Joplin</a> is an open source note-taking app. Capture your thoughts and securely access them from any device</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Joplin is an open source note-taking app. Capture your thoughts and securely access them from any device",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://joplinapp.org/",
+            "location": "/__w/apps/apps/trains/community/joplin",
+            "latest_version": "1.4.11",
+            "latest_app_version": "amd64-3.4.2",
+            "latest_human_version": "amd64-3.4.2_1.4.11",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "joplin",
+            "recommended": false,
+            "title": "Joplin",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "notes"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/joplin/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/joplin/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/joplin/screenshots/screenshot3.png"
+            ],
+            "sources": [
+                "https://github.com/laurent22/joplin",
+                "https://hub.docker.com/r/joplin/server/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/joplin/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Joplin runs as non-root user.",
+                    "gid": 1001,
+                    "group_name": "joplin",
+                    "uid": 1001,
+                    "user_name": "joplin"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "speedtest-tracker": {
+            "app_readme": "<h1>Speedtest Tracker</h1> <p><a href=\"https://speedtest-tracker.dev\">Speedtest Tracker</a> is a self-hosted application that monitors the performance and uptime of your internet connection.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Speedtest Tracker is a self-hosted application that monitors the performance and uptime of your internet connection.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://speedtest-tracker.dev",
+            "location": "/__w/apps/apps/trains/community/speedtest-tracker",
+            "latest_version": "1.0.8",
+            "latest_app_version": "1.6.6",
+            "latest_human_version": "1.6.6_1.0.8",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "speedtest-tracker",
+            "recommended": false,
+            "title": "Speedtest Tracker",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "speedtest",
+                "tracker"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/linuxserver/docker-speedtest-tracker",
+                "https://github.com/alexjustesen/speedtest-tracker"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/speedtest-tracker/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Speedtest Tracker is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Speedtest Tracker is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Speedtest Tracker is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Speedtest Tracker is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Speedtest Tracker is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Speedtest Tracker runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "ddns-updater": {
+            "app_readme": "<h1>DDNS Updater</h1> <p><a href=\"https://github.com/qdm12/ddns-updater\">DDNS Updater</a> is a lightweight universal DDNS Updater with web UI</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "Lightweight universal DDNS Updater with web UI",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/qdm12/ddns-updater",
+            "location": "/__w/apps/apps/trains/community/ddns-updater",
+            "latest_version": "1.2.8",
+            "latest_app_version": "v2.9.0",
+            "latest_human_version": "v2.9.0_1.2.8",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "ddns-updater",
+            "recommended": false,
+            "title": "DDNS Updater",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "ddns-updater",
+                "ddns"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/ddns-updater/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/qdm12/ddns-updater",
+                "https://hub.docker.com/r/qmcgaw/ddns-updater"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/ddns-updater/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "DDNS Updater runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "ddns-updater",
+                    "uid": 568,
+                    "user_name": "ddns-updater"
+                }
+            ]
+        },
+        "listmonk": {
+            "app_readme": "<h1>Listmonk</h1> <p><a href=\"https://listmonk.app/\">Listmonk</a> is a self-hosted newsletter and mailing list manager.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Listmonk is a self-hosted newsletter and mailing list manager.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://listmonk.app/",
+            "location": "/__w/apps/apps/trains/community/listmonk",
+            "latest_version": "1.3.11",
+            "latest_app_version": "v5.0.3",
+            "latest_human_version": "v5.0.3_1.3.11",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "listmonk",
+            "recommended": false,
+            "title": "Listmonk",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "mailing-list",
+                "newsletter"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/listmonk/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/listmonk/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/listmonk/screenshots/screenshot3.png"
+            ],
+            "sources": [
+                "https://hub.docker.com/r/listmonk/listmonk",
+                "https://github.com/knadh/listmonk"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/listmonk/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Listmonk is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Listmonk is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Listmonk is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Listmonk is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Listmonk is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Listmonk runs as root user.",
+                    "gid": 0,
+                    "group_name": "listmonk",
+                    "uid": 0,
+                    "user_name": "listmonk"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "kiwix-server": {
+            "app_readme": "<h1>Kiwix Server</h1> <p><a href=\"https://github.com/kiwix/kiwix-tools\">Kiwix Server</a> is a ZIM compatible Web server.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Kiwix Server is a ZIM compatible Web server.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/kiwix/kiwix-tools",
+            "location": "/__w/apps/apps/trains/community/kiwix-server",
+            "latest_version": "1.0.3",
+            "latest_app_version": "3.7.0",
+            "latest_human_version": "3.7.0_1.0.3",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "kiwix-server",
+            "recommended": false,
+            "title": "Kiwix Server",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "kiwix",
+                "server",
+                "zim",
+                "wiki"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/kiwix/kiwix-tools"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/kiwix-server/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Kiwix Server runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "kiwix",
+                    "uid": 568,
+                    "user_name": "kiwix"
+                }
+            ]
+        },
+        "ipfs": {
+            "app_readme": "<h1>IPFS</h1> <p><a href=\"https://ipfs.tech\">Interplanetary Filesystem</a> - the Web3 standard for content-addressing, interoperable with HTTP</p>",
+            "categories": [
+                "storage"
+            ],
+            "description": "Interplanetary Filesystem - the Web3 standard for content-addressing, interoperable with HTTP",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://ipfs.tech/",
+            "location": "/__w/apps/apps/trains/community/ipfs",
+            "latest_version": "1.2.10",
+            "latest_app_version": "v0.37.0",
+            "latest_human_version": "v0.37.0_1.2.10",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "ipfs",
+            "recommended": false,
+            "title": "IPFS",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "storage",
+                "ipfs",
+                "file-sharing",
+                "kubo"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/ipfs/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/ipfs/screenshots/screenshot2.png"
+            ],
+            "sources": [
+                "https://github.com/ipfs/kubo",
+                "https://ipfs.tech/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/ipfs/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "IPFS runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "ipfs",
+                    "uid": 568,
+                    "user_name": "ipfs"
+                }
+            ]
+        },
+        "woodpecker-ci": {
+            "app_readme": "<h1>Woodpecker CI</h1> <p><a href=\"https://woodpecker-ci.org/\">Woodpecker CI</a> is a simple, yet powerful CI/CD engine with great extensibility.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Woodpecker CI is a simple, yet powerful CI/CD engine with great extensibility.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://woodpecker-ci.org",
+            "location": "/__w/apps/apps/trains/community/woodpecker-ci",
+            "latest_version": "1.0.13",
+            "latest_app_version": "v3.9.0",
+            "latest_human_version": "v3.9.0_1.0.13",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "woodpecker-ci",
+            "recommended": false,
+            "title": "Woodpecker CI",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "automation",
+                "ci"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://woodpecker-ci.org",
+                "https://github.com/woodpecker-ci/woodpecker",
+                "https://hub.docker.com/r/woodpeckerci/woodpecker-server",
+                "https://hub.docker.com/r/woodpeckerci/woodpecker-agent"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/woodpecker-ci/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Woodpecker CI Server runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "woodpecker",
+                    "uid": 568,
+                    "user_name": "woodpecker"
+                },
+                {
+                    "description": "Woodpecker CI Agent runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "woodpecker",
+                    "uid": 568,
+                    "user_name": "woodpecker"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "linkwarden": {
+            "app_readme": "<h1>Linkwarden</h1> <p><a href=\"https://docs.linkwarden.app/\">Linkwarden</a> is a self-hosted, open-source collaborative bookmark manager to collect, read, annotate, and fully preserve what matters, all in one place.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Linkwarden is a self-hosted, open-source collaborative bookmark manager to collect, read, annotate, and fully preserve what matters, all in one place.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://docs.linkwarden.app/",
+            "location": "/__w/apps/apps/trains/community/linkwarden",
+            "latest_version": "1.0.14",
+            "latest_app_version": "v2.12.2",
+            "latest_human_version": "v2.12.2_1.0.14",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "linkwarden",
+            "recommended": false,
+            "title": "Linkwarden",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "links",
+                "bookmarks"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://docs.linkwarden.app/",
+                "https://github.com/linkwarden/linkwarden"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/linkwarden/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Linkwarden runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                },
+                {
+                    "description": "Meilisearch runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "meilisearch",
+                    "uid": 568,
+                    "user_name": "meilisearch"
+                }
+            ]
+        },
+        "tvheadend": {
+            "app_readme": "<h1>TVHeadend</h1> <p><a href=\"https://tvheadend.org\">TVHeadend</a> works as a proxy server: is a TV streaming server and recorder for Linux, FreeBSD and Android supporting DVB-S, DVB-S2, DVB-C, DVB-T, ATSC, ISDB-T, IPTV, SAT&gt;IP and HDHomeRun as input sources. Tvheadend offers the HTTP (VLC, MPlayer), HTSP (Kodi, Movian) and SAT&gt;IP streaming. Multiple EPG sources are supported (over-the-air DVB and ATSC including OpenTV DVB extensions, XMLTV, PyXML).</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "TVHeadend works as a proxy server, is a TV streaming server and recorder",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://tvheadend.org",
+            "location": "/__w/apps/apps/trains/community/tvheadend",
+            "latest_version": "1.0.18",
+            "latest_app_version": "b3974f7d-ls260",
+            "latest_human_version": "b3974f7d-ls260_1.0.18",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "tvheadend",
+            "recommended": false,
+            "title": "TVHeadend",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "tvheadend",
+                "livetv",
+                "streaming"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/linuxserver/docker-tvheadend"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/tvheadend/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "TVHeadend is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "TVHeadend is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "TVHeadend is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "TVHeadend is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "TVHeadend is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "TVheadend runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "many-notes": {
+            "app_readme": "<h1>Many Notes</h1> <p><a href=\"https://github.com/brufdev/many-notes\">Many Notes</a> is a Markdown note-taking web application designed for simplicity! Easily create or import your vaults and organize your thoughts right away.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Many Notes is a Markdown note-taking web application designed for simplicity! Easily create or import your vaults and organize your thoughts right away.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/brufdev/many-notes",
+            "location": "/__w/apps/apps/trains/community/many-notes",
+            "latest_version": "1.0.5",
+            "latest_app_version": "0.12",
+            "latest_human_version": "0.12_1.0.5",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "many-notes",
+            "recommended": false,
+            "title": "Many Notes",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "documentation",
+                "knowledgebase"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/brufdev/many-notes"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/many-notes/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Many Notes runs as non-root user.",
+                    "gid": 33,
+                    "group_name": "manynotes",
+                    "uid": 33,
+                    "user_name": "manynotes"
+                }
+            ]
+        },
+        "paperless-ai": {
+            "app_readme": "<h1>Paperless AI</h1> <p><a href=\"https://github.com/clusterzx/paperless-ai\">Paperless AI</a> is the most advanced AI companion for Paperless-NGX. Transform your document management with powerful AI-driven automation and insights.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "The most advanced AI companion for Paperless-NGX. Transform your document management with powerful AI-driven automation and insights.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://clusterzx.github.io/paperless-ai/",
+            "location": "/__w/apps/apps/trains/community/paperless-ai",
+            "latest_version": "1.0.4",
+            "latest_app_version": "3.0.7",
+            "latest_human_version": "3.0.7_1.0.4",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "paperless-ai",
+            "recommended": false,
+            "title": "Paperless AI",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "paperless",
+                "ai",
+                "automation"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://hub.docker.com/r/clusterzx/paperless-ai",
+                "https://github.com/clusterzx/paperless-ai"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/paperless-ai/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Paperless AI runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "archisteamfarm": {
+            "app_readme": "<h1>ArchiSteamFarm</h1> <p><a href=\"https://github.com/JustArchiNET/ArchiSteamFarm\">ArchiSteamFarm</a> is a tool for automatically farming Steam trading cards on multiple accounts simultaneously.</p>",
+            "categories": [
+                "games"
+            ],
+            "description": "ArchiSteamFarm is a tool for automatically farming Steam trading cards on multiple accounts simultaneously",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/JustArchiNET/ArchiSteamFarm",
+            "location": "/__w/apps/apps/trains/community/archisteamfarm",
+            "latest_version": "1.0.32",
+            "latest_app_version": "6.2.2.0",
+            "latest_human_version": "6.2.2.0_1.0.32",
+            "last_update": "2025-09-05 14:25:56",
+            "name": "archisteamfarm",
+            "recommended": false,
+            "title": "ArchiSteamFarm",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "archisteamfarm",
+                "steam",
+                "asf"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/JustArchiNET/ArchiSteamFarm/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/archisteamfarm/icons/icon.webp",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "ArchiSteamFarm runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "archisteamfarm",
+                    "uid": 568,
+                    "user_name": "archisteamfarm"
+                }
+            ]
+        },
+        "homebox": {
+            "app_readme": "<h1>Homebox</h1> <p><a href=\"https://homebox.software/en/\">Homebox</a> is the inventory and organization system built for the Home User! With a focus on simplicity and ease of use, Homebox is the perfect solution for your home inventory, organization, and management needs.</p>",
+            "categories": [
+                "management"
+            ],
+            "description": "Homebox is the inventory and organization system built for the Home User!",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://homebox.software",
+            "location": "/__w/apps/apps/trains/community/homebox",
+            "latest_version": "1.0.10",
+            "latest_app_version": "0.21.0-rootless",
+            "latest_human_version": "0.21.0-rootless_1.0.10",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "homebox",
+            "recommended": false,
+            "title": "Homebox",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "inventory",
+                "organization",
+                "management"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/homebox/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/sysadminsmedia/homebox",
+                "https://homebox.software"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/homebox/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Homebox runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "homebox",
+                    "uid": 568,
+                    "user_name": "homebox"
+                }
+            ]
+        },
+        "authentik": {
+            "app_readme": "<h1>Authentik</h1> <p><a href=\"https://goauthentik.io/\">Authentik</a> is an open-source Identity Provider that emphasizes flexibility and versatility, with support for a wide set of protocols.</p>",
+            "categories": [
+                "authentication"
+            ],
+            "description": "The authentication glue you need.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://goauthentik.io/",
+            "location": "/__w/apps/apps/trains/community/authentik",
+            "latest_version": "1.0.35",
+            "latest_app_version": "2025.8.1",
+            "latest_human_version": "2025.8.1_1.0.35",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "authentik",
+            "recommended": false,
+            "title": "Authentik",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "authentik",
+                "auth"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/authentik/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/authentik/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/authentik/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/authentik/screenshots/screenshot4.png"
+            ],
+            "sources": [
+                "https://goauthentik.io/",
+                "https://github.com/goauthentik/authentik"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/authentik/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Authentik runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "authentik",
+                    "uid": 568,
+                    "user_name": "authentik"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                }
+            ]
+        },
+        "minecraft-bedrock": {
+            "app_readme": "<h1>Minecraft Bedrock</h1> <p><a href=\"https://www.minecraft.net/en-us\">Minecraft Bedrock</a> Dedicated Server</p>",
+            "categories": [
+                "games"
+            ],
+            "description": "Minecraft dedicated server for Bedrock platform hosts a world for multiplayer game.\nPlayers can join the server using the Bedrock client on Desktops, Mobiles, and consoles.\n",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.minecraft.net/en-us",
+            "location": "/__w/apps/apps/trains/community/minecraft-bedrock",
+            "latest_version": "1.0.27",
+            "latest_app_version": "2025.9.0",
+            "latest_human_version": "2025.9.0_1.0.27",
+            "last_update": "2025-09-02 15:29:51",
+            "name": "minecraft-bedrock",
+            "recommended": false,
+            "title": "Minecraft Server (Bedrock)",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "world",
+                "building"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://www.minecraft.net/en-us",
+                "https://github.com/itzg/docker-minecraft-bedrock-server"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/minecraft-bedrock/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Minecraft runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "minecraft",
+                    "uid": 568,
+                    "user_name": "minecraft"
+                }
+            ]
+        },
+        "tianji": {
+            "app_readme": "<h1>Tianji</h1> <p><a href=\"https://github.com/msgbyte/tianji\">Tianji</a> - Insight into everything, Website Analytics + Uptime Monitor + Server Status. not only another GA alternatives</p>",
+            "categories": [
+                "monitoring"
+            ],
+            "description": "Tianji - Insight into everything, Website Analytics + Uptime Monitor + Server Status. not only another GA alternatives",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://tianji.msgbyte.com",
+            "location": "/__w/apps/apps/trains/community/tianji",
+            "latest_version": "1.0.79",
+            "latest_app_version": "1.24.26",
+            "latest_human_version": "1.24.26_1.0.79",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "tianji",
+            "recommended": false,
+            "title": "Tianji",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "analytics",
+                "monitoring",
+                "uptime",
+                "status"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/tianji/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/tianji/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/tianji/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/tianji/screenshots/screenshot4.png",
+                "https://media.sys.truenas.net/apps/tianji/screenshots/screenshot5.png",
+                "https://media.sys.truenas.net/apps/tianji/screenshots/screenshot6.png"
+            ],
+            "sources": [
+                "https://github.com/msgbyte/tianji"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/tianji/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Tianji runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "notifiarr": {
+            "app_readme": "<h1>Notifiarr</h1> <p><a href=\"https://notifiarr.com/\">Notifiarr</a> is a client for Notifiarr.com</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Client for Notifiarr.com",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://notifiarr.com/",
+            "location": "/__w/apps/apps/trains/community/notifiarr",
+            "latest_version": "1.0.6",
+            "latest_app_version": "v0.8.3",
+            "latest_human_version": "v0.8.3_1.0.6",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "notifiarr",
+            "recommended": false,
+            "title": "Notifiarr",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "notifiarr"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/Notifiarr/notifiarr"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/notifiarr/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Notifiarr runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "notifiarr",
+                    "uid": 568,
+                    "user_name": "notifiarr"
+                }
+            ]
+        },
+        "adguard-home": {
+            "app_readme": "<h1>AdGuard Home</h1> <p><a href=\"https://github.com/AdguardTeam/AdGuardHome\">AdGuard Home</a> is a network-wide ads &amp; trackers blocking DNS server</p> <p>During the setup wizard, AdGuard Home presents an option to select on which port the web interface will be available. (Defaults to 80. Which is a privileged port and also usually the TrueNAS SCALE UI uses that port) Because of that, App will force the webUI to listen to port 30000 (or the port selected by user in the TrueNAS SCALE UI).</p> <p>If you select a different port in the wizard, the Dashboard will not work initially but after a couple of minutes container will automatically restart and the Dashboard will be available on the port you selected on the TrueNAS SCALE UI.</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "Free and open source, powerful network-wide ads & trackers blocking DNS server.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/AdguardTeam/AdGuardHome",
+            "location": "/__w/apps/apps/trains/community/adguard-home",
+            "latest_version": "1.2.9",
+            "latest_app_version": "v0.107.65",
+            "latest_human_version": "v0.107.65_1.2.9",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "adguard-home",
+            "recommended": false,
+            "title": "AdGuard Home",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "dns",
+                "adblock"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/adguard-home/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/adguard-home/screenshots/screenshot2.png"
+            ],
+            "sources": [
+                "https://github.com/AdguardTeam/AdGuardHome",
+                "https://hub.docker.com/r/adguard/adguardhome"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/adguard-home/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Adguard is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Adguard is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Adguard is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Adguard is able to bind to privileged ports (< 1024)",
+                    "name": "NET_BIND_SERVICE"
+                },
+                {
+                    "description": "Adguard is able to use raw and packet sockets",
+                    "name": "NET_RAW"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "AdGuard Home runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "flame": {
+            "app_readme": "<h1>Flame</h1> <p><a href=\"https://github.com/pawelmalak/flame\">Flame</a> is a self-hosted start page for your server.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Flame is a self-hosted start page for your server.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/pawelmalak/flame",
+            "location": "/__w/apps/apps/trains/community/flame",
+            "latest_version": "1.2.6",
+            "latest_app_version": "2.3.1",
+            "latest_human_version": "2.3.1_1.2.6",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "flame",
+            "recommended": false,
+            "title": "Flame",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "startpage"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/flame/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://hub.docker.com/r/pawelmalak/flame",
+                "https://github.com/pawelmalak/flame"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/flame/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Flame is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Flame is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Flame is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Flame runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "scrutiny": {
+            "app_readme": "<h1>Scrutiny</h1> <p><a href=\"https://github.com/AnalogJ/scrutiny\">Scrutiny</a> - Hard Drive S.M.A.R.T Monitoring, Historical Trends &amp; Real World Failure Thresholds</p> <p>Scrutiny is tool for Hard Drive S.M.A.R.T Monitoring, Historical Trends &amp; Real World Failure Thresholds</p>",
+            "categories": [
+                "monitoring"
+            ],
+            "description": "Scrutiny is tool for Hard Drive S.M.A.R.T Monitoring, Historical Trends & Real World Failure Thresholds",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/AnalogJ/scrutiny",
+            "location": "/__w/apps/apps/trains/community/scrutiny",
+            "latest_version": "1.2.8",
+            "latest_app_version": "v0.8.1-omnibus",
+            "latest_human_version": "v0.8.1-omnibus_1.2.8",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "scrutiny",
+            "recommended": false,
+            "title": "Scrutiny",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "disk",
+                "monitoring"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/scrutiny/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/AnalogJ/scrutiny"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/scrutiny/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Scrutiny is able to write records to audit log",
+                    "name": "AUDIT_WRITE"
+                },
+                {
+                    "description": "Scrutiny is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Scrutiny is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Scrutiny is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Scrutiny is able to preserve set-user-ID and set-group-ID bits",
+                    "name": "FSETID"
+                },
+                {
+                    "description": "Scrutiny is able to send signals to any process",
+                    "name": "KILL"
+                },
+                {
+                    "description": "Scrutiny is able to create special files using mknod()",
+                    "name": "MKNOD"
+                },
+                {
+                    "description": "Scrutiny is able to bind to privileged ports (< 1024)",
+                    "name": "NET_BIND_SERVICE"
+                },
+                {
+                    "description": "Scrutiny is able to use raw and packet sockets",
+                    "name": "NET_RAW"
+                },
+                {
+                    "description": "Scrutiny is able to set file capabilities on other files",
+                    "name": "SETFCAP"
+                },
+                {
+                    "description": "Scrutiny is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Scrutiny is able to transfer capabilities between processes",
+                    "name": "SETPCAP"
+                },
+                {
+                    "description": "Scrutiny is able to change user ID of processes",
+                    "name": "SETUID"
+                },
+                {
+                    "description": "Scrutiny is able to use chroot() system call",
+                    "name": "SYS_CHROOT"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Scrutiny runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "dashy": {
+            "app_readme": "<h1>Dashy</h1> <p><a href=\"https://dashy.to/\">Dashy</a> is a self-hostable personal dashboard built for you.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Dashy is a self-hostable personal dashboard built for you.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://dashy.to/",
+            "location": "/__w/apps/apps/trains/community/dashy",
+            "latest_version": "1.2.6",
+            "latest_app_version": "3.1.0",
+            "latest_human_version": "3.1.0_1.2.6",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "dashy",
+            "recommended": false,
+            "title": "Dashy",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "dashboard"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/dashy/screenshots/screenshot1.gif",
+                "https://media.sys.truenas.net/apps/dashy/screenshots/screenshot2.gif"
+            ],
+            "sources": [
+                "https://dashy.to/",
+                "https://github.com/lissy93/dashy"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/dashy/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Dashy runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "transmission": {
+            "app_readme": "<h1>Transmission</h1> <p><a href=\"https://transmissionbt.com/\">Transmission</a> is designed for easy, powerful use.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Transmission is designed for easy, powerful use.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://transmissionbt.com/",
+            "location": "/__w/apps/apps/trains/community/transmission",
+            "latest_version": "1.2.8",
+            "latest_app_version": "4.0.6",
+            "latest_human_version": "4.0.6_1.2.8",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "transmission",
+            "recommended": false,
+            "title": "Transmission",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "torrent",
+                "download"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://docs.linuxserver.io/images/docker-transmission/#parameters",
+                "https://transmissionbt.com/",
+                "https://github.com/linuxserver/docker-transmission"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/transmission/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Transmission is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Transmission is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Transmission is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Transmission is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Transmission is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Transmission runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "piwigo": {
+            "app_readme": "<h1>Piwigo</h1> <p><a href=\"https://piwigo.org/\">Piwigo</a> is a photo gallery software for the web that comes with powerful features to publish and manage your collection of pictures.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Piwigo is a photo gallery software for the web that comes with powerful features to publish and manage your collection of pictures.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://piwigo.org",
+            "location": "/__w/apps/apps/trains/community/piwigo",
+            "latest_version": "1.2.10",
+            "latest_app_version": "15.6.0",
+            "latest_human_version": "15.6.0_1.2.10",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "piwigo",
+            "recommended": false,
+            "title": "Piwigo",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "photo",
+                "gallery"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/piwigo/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/piwigo/screenshots/screenshot2.png"
+            ],
+            "sources": [
+                "https://github.com/linuxserver/docker-piwigo"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/piwigo/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Piwigo is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Piwigo is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Piwigo is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Piwigo is able to bind to privileged ports (< 1024)",
+                    "name": "NET_BIND_SERVICE"
+                },
+                {
+                    "description": "Piwigo is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Piwigo is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Piwigo runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "MariaDB runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "mariadb",
+                    "uid": 999,
+                    "user_name": "mariadb"
+                }
+            ]
+        },
+        "calibre-web": {
+            "app_readme": "<h1>Calibre Web</h1> <p><a href=\"https://github.com/janeczku/calibre-web\">Calibre Web</a> is a web app for browsing, reading and downloading eBooks stored in a Calibre database</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Calibre Web is a web app for browsing, reading and downloading eBooks stored in a Calibre database",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/janeczku/calibre-web",
+            "location": "/__w/apps/apps/trains/community/calibre-web",
+            "latest_version": "2.0.16",
+            "latest_app_version": "0.6.25",
+            "latest_human_version": "0.6.25_2.0.16",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "calibre-web",
+            "recommended": false,
+            "title": "Calibre Web",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "ebooks"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/calibre-web/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/linuxserver/docker-calibre-web",
+                "https://github.com/janeczku/calibre-web"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/calibre-web/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Calibre Web is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Calibre Web is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Calibre Web is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Calibre Web is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Calibre Web is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Calibre Web runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "radarr": {
+            "app_readme": "<h1>Radarr</h1> <p><a href=\"https://github.com/Radarr/Radarr\">Radarr</a> is a movie collection manager for Usenet and BitTorrent users.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Radarr is a movie collection manager for Usenet and BitTorrent users.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/Radarr/Radarr",
+            "location": "/__w/apps/apps/trains/community/radarr",
+            "latest_version": "1.3.15",
+            "latest_app_version": "5.27.5.10198",
+            "latest_human_version": "5.27.5.10198_1.3.15",
+            "last_update": "2025-09-04 12:07:56",
+            "name": "radarr",
+            "recommended": false,
+            "title": "Radarr",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "movies"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/radarr/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/radarr/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/radarr/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/radarr/screenshots/screenshot4.png"
+            ],
+            "sources": [
+                "https://github.com/home-operations/containers/tree/main/apps/radarr",
+                "https://github.com/Radarr/Radarr"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/radarr/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Radarr runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "radarr",
+                    "uid": 568,
+                    "user_name": "radarr"
+                }
+            ]
+        },
+        "sftpgo": {
+            "app_readme": "<h1>SFTPGo</h1> <p><a href=\"https://github.com/drakkan/sftpgo\">SFTPGo</a> is a fully featured and highly configurable SFTP server with optional HTTP/S, FTP/S and WebDAV support - S3, Google Cloud Storage, Azure Blob</p>",
+            "categories": [
+                "storage"
+            ],
+            "description": "SFTPGo is a fully featured and highly configurable SFTP server with optional HTTP/S, FTP/S and WebDAV support - S3, Google Cloud Storage, Azure Blob",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/drakkan/sftpgo",
+            "location": "/__w/apps/apps/trains/community/sftpgo",
+            "latest_version": "1.2.7",
+            "latest_app_version": "v2.6.6",
+            "latest_human_version": "v2.6.6_1.2.7",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "sftpgo",
+            "recommended": false,
+            "title": "SFTPGo",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "sftp"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://hub.docker.com/r/drakkan/sftpgo",
+                "https://github.com/drakkan/sftpgo"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/sftpgo/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "SFTPGo runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "sftpgo",
+                    "uid": 568,
+                    "user_name": "sftpgo"
+                }
+            ]
+        },
+        "frigate": {
+            "app_readme": "<h1>Frigate</h1> <p><a href=\"https://github.com/blakeblackshear/frigate\">Frigate</a> is an NVR With Realtime Object Detection for IP Cameras</p> <blockquote> <p>Note: Coral <strong>m.2</strong> TPU devices is not supported.</p> </blockquote>",
+            "categories": [
+                "security"
+            ],
+            "description": "Frigate is an NVR With Realtime Object Detection for IP Cameras",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://frigate.video/",
+            "location": "/__w/apps/apps/trains/community/frigate",
+            "latest_version": "1.2.16",
+            "latest_app_version": "0.16.1",
+            "latest_human_version": "0.16.1_1.2.16",
+            "last_update": "2025-09-04 12:07:56",
+            "name": "frigate",
+            "recommended": false,
+            "title": "Frigate",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "camera",
+                "nvr"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/frigate/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/frigate/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/frigate/screenshots/screenshot3.png"
+            ],
+            "sources": [
+                "https://github.com/blakeblackshear/frigate"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/frigate/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Frigate is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Frigate is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Frigate is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Frigate is able to send signals to any process",
+                    "name": "KILL"
+                },
+                {
+                    "description": "Frigate is able to access performance monitoring interfaces",
+                    "name": "PERFMON"
+                },
+                {
+                    "description": "Frigate is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Frigate is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Frigate runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "freshrss": {
+            "app_readme": "<h1>FreshRSS</h1> <p><a href=\"https://freshrss.org/\">FreshRSS</a> is a free, self-hostable news aggregator</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "FreshRSS is a free, self-hostable news aggregator",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://freshrss.org/",
+            "location": "/__w/apps/apps/trains/community/freshrss",
+            "latest_version": "1.4.13",
+            "latest_app_version": "1.27.0",
+            "latest_human_version": "1.27.0_1.4.13",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "freshrss",
+            "recommended": false,
+            "title": "FreshRSS",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "rss",
+                "news"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/freshrss/screenshots/screenshot1.webp"
+            ],
+            "sources": [
+                "https://github.com/FreshRSS/FreshRSS",
+                "https://hub.docker.com/r/freshrss/freshrss"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/freshrss/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "FreshRSS is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "FreshRSS is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "FreshRSS is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "FreshRSS runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "tautulli": {
+            "app_readme": "<h1>Tautulli</h1> <p><a href=\"https://tautulli.com/\">Tautulli</a> is a python based web application for monitoring, analytics and notifications for Plex Media Server.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Tautulli is a python based web application for monitoring, analytics and notifications for Plex Media Server.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://tautulli.com",
+            "location": "/__w/apps/apps/trains/community/tautulli",
+            "latest_version": "1.2.7",
+            "latest_app_version": "v2.15.3",
+            "latest_human_version": "v2.15.3_1.2.7",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "tautulli",
+            "recommended": false,
+            "title": "Tautulli",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "analytics",
+                "notifications"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/tautulli/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/tautulli/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/tautulli/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/tautulli/screenshots/screenshot4.png"
+            ],
+            "sources": [
+                "https://github.com/Tautulli/Tautulli"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/tautulli/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Tautulli runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "tautulli",
+                    "uid": 568,
+                    "user_name": "tautulli"
+                }
+            ]
+        },
+        "tftpd-hpa": {
+            "app_readme": "<h1>TFTP</h1> <p><a href=\"https://manpages.debian.org/testing/tftpd-hpa/tftpd.8.en.html\">TFTP</a> is a server for the Trivial File Transfer Protocol.</p> <p>The app runs as <code>root</code> user and drops privileges to <code>tftp</code> (9069) user for the TFTP service.</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "A lightweight tftp-server",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/truenas/containers/tree/main/apps/tftpd-hpa",
+            "location": "/__w/apps/apps/trains/community/tftpd-hpa",
+            "latest_version": "1.2.5",
+            "latest_app_version": "1.0.0",
+            "latest_human_version": "1.0.0_1.2.5",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "tftpd-hpa",
+            "recommended": false,
+            "title": "TFTP Server",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "tftp",
+                "netboot"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/truenas/containers/tree/main/apps/tftpd-hpa",
+                "https://hub.docker.com/r/ixsystems/tftpd-hpa"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/tftpd-hpa/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Tftpd is able to bind to privileged ports (< 1024)",
+                    "name": "NET_BIND_SERVICE"
+                },
+                {
+                    "description": "Tftpd is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Tftpd is able to change user ID of processes",
+                    "name": "SETUID"
+                },
+                {
+                    "description": "Tftpd is able to use chroot() system call",
+                    "name": "SYS_CHROOT"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "TFTP requires root privileges to start it's processes.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "lyrion-music-server": {
+            "app_readme": "<h1>Lyrion Music Server (AKA LMS)</h1> <p><a href=\"https://lyrion.org/\">Lyrion Music Server</a> - software to control Squeezebox audio players</p> <p>Lyrion Music Server (formerly Logitech Media Server) is open-source server software which controls a wide range of Squeezebox audio players. Lyrion can stream your local music collection, internet radio stations, and content from many streaming services (with and without subscriptions).</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Lyrion Music Server - controls a wide range of Squeezebox audio players",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://lyrion.org/",
+            "location": "/__w/apps/apps/trains/community/lyrion-music-server",
+            "latest_version": "1.0.21",
+            "latest_app_version": "9.0.2",
+            "latest_human_version": "9.0.2_1.0.21",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "lyrion-music-server",
+            "recommended": false,
+            "title": "Lyrion Music Server",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "entertainment",
+                "music",
+                "streaming"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/lyrion-music-server/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/lms-community/slimserver"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/lyrion-music-server/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Lyrion Media Server is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Lyrion Media Server is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Lyrion Media Server is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Lyrion Music Server runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "open-speed-test": {
+            "app_readme": "<h1>Open Speed Test</h1> <p><a href=\"https://openspeedtest.com\">SpeedTest</a> by OpenSpeedTest\u2122 is a Free and Open-Source HTML5 Network Performance Estimation Tool Written in Vanilla Javascript and only uses built-in Web APIs like XMLHttpRequest (XHR), HTML, CSS, JS, &amp; SVG. No Third-Party frameworks or libraries are Required. Started in 2011 and moved to OpenSpeedTest.com dedicated Project/Domain Name in 2013.</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "SpeedTest by OpenSpeedTest\u2122 is a Free and Open-Source HTML5 Network Performance Estimation Tool",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://openspeedtest.com",
+            "location": "/__w/apps/apps/trains/community/open-speed-test",
+            "latest_version": "1.0.15",
+            "latest_app_version": "v2.0.6",
+            "latest_human_version": "v2.0.6_1.0.15",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "open-speed-test",
+            "recommended": false,
+            "title": "Open Speed Test",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "speedtest"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/open-speed-test/screenshots/screenshot1.gif"
+            ],
+            "sources": [
+                "https://github.com/openspeedtest/Speed-Test"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/open-speed-test/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Open Speed Test runs as non-root user.",
+                    "gid": 101,
+                    "group_name": "nginx",
+                    "uid": 101,
+                    "user_name": "nginx"
+                }
+            ]
+        },
+        "filebrowser": {
+            "app_readme": "<h1>Filebrowser</h1> <p><a href=\"https://filebrowser.org\">Filebrowser</a> provides a file managing interface within a specified directory and it can be used to upload, delete, preview, rename and edit your files.</p> <p>You can configure further the settings by using Environment Variables. See <a href=\"https://filebrowser.org/cli/filebrowser\">Filebrowser Documentation</a> for more information. Use the format <code>FB_OPTION_NAME</code> where the option name is the name of the option you want to set.</p> <p>You can also edit the configuration file <code>/config/filebrowser.json</code>.</p> <p>Note that the following options are already set and will always take precedence over the environment variables and the configuration file:</p> <ul> <li><code>FB_ROOT</code>/<code>--root</code> is set to <code>/data</code> (Any additional volume mounted will be under this directory)</li> <li><code>FB_PORT</code>/<code>--port</code> is set to <code>30044</code> (Or the port you configured in the installation wizard)</li> <li><code>FB_ADDRESS</code>/<code>--address</code> is set to <code>0.0.0.0</code> (It will listen on all interfaces <strong>inside</strong> the container)</li> <li><code>FB_DATABASE</code>/<code>--database</code> is set to <code>/config/filebrowser.db</code></li> <li><code>FB_CONFIG</code>/<code>--config</code> is set to <code>/config/filebrowser.json</code></li> </ul> <p>Also when a certificate is selected</p> <ul> <li><code>FB_CERT</code>/<code>--cert</code> is set to <code>/config/certs/tls.crt</code></li> <li><code>FB_KEY</code>/<code>--key</code> is set to <code>/config/certs/tls.key</code></li> </ul>",
+            "categories": [
+                "storage"
+            ],
+            "description": "File Browser provides a file managing interface within a specified directory and it can be used to upload, delete, preview, rename and edit your files.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://filebrowser.org",
+            "location": "/__w/apps/apps/trains/community/filebrowser",
+            "latest_version": "1.3.30",
+            "latest_app_version": "v2.42.5",
+            "latest_human_version": "v2.42.5_1.3.30",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "filebrowser",
+            "recommended": false,
+            "title": "File Browser",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "files",
+                "browser"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/filebrowser/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/filebrowser/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/filebrowser/screenshots/screenshot3.png"
+            ],
+            "sources": [
+                "https://github.com/filebrowser/filebrowser",
+                "https://hub.docker.com/r/filebrowser/filebrowser"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/filebrowser/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "File Browser runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "filebrowser runs as any non-root user.",
+                    "uid": 568,
+                    "user_name": "filebrowser runs as any non-root user."
+                }
+            ]
+        },
+        "cockpit-ws": {
+            "app_readme": "<h1>Cockpit WS</h1> <p><a href=\"https://cockpit-project.org/\">Cockpit</a> is a web-based graphical interface for Linux servers.</p>",
+            "categories": [
+                "management"
+            ],
+            "description": "Cockpit is a web-based graphical interface for Linux servers.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://cockpit-project.org/",
+            "location": "/__w/apps/apps/trains/community/cockpit-ws",
+            "latest_version": "1.0.18",
+            "latest_app_version": "345",
+            "latest_human_version": "345_1.0.18",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "cockpit-ws",
+            "recommended": false,
+            "title": "Cockpit WS",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "cockpit"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://quay.io/repository/cockpit/ws"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/cockpit-ws/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Cockpit WS runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "wizarr": {
+            "app_readme": "<h1>Wizarr</h1> <p><a href=\"https://github.com/wizarrrr/wizarr\">Wizarr</a> is an advanced user invitation and management system for Jellyfin, Plex, Emby etc.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Wizarr is an advanced user invitation and management system for Jellyfin, Plex, Emby etc.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/wizarrrr/wizarr",
+            "location": "/__w/apps/apps/trains/community/wizarr",
+            "latest_version": "1.0.9",
+            "latest_app_version": "2025.8.3",
+            "latest_human_version": "2025.8.3_1.0.9",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "wizarr",
+            "recommended": false,
+            "title": "Wizarr",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "invitation",
+                "management"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/wizarrrr/wizarr"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/wizarr/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Wizarr runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "wizarr",
+                    "uid": 568,
+                    "user_name": "wizarr"
+                }
+            ]
+        },
+        "windmill": {
+            "app_readme": "<h1>Windmill</h1> <p><a href=\"https://www.windmill.dev/\">Windmill</a> is an open-source developer platform and workflow engine. It turns scripts into auto-generated UIs, APIs, and cron jobs, allowing you to compose them as workflows or data pipelines to build complex, data-intensive apps with ease.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Open-source developer platform and workflow engine",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.windmill.dev/",
+            "location": "/__w/apps/apps/trains/community/windmill",
+            "latest_version": "1.0.69",
+            "latest_app_version": "1.538.0",
+            "latest_human_version": "1.538.0_1.0.69",
+            "last_update": "2025-09-02 15:29:51",
+            "name": "windmill",
+            "recommended": false,
+            "title": "Windmill",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "workflows",
+                "automation",
+                "scripts",
+                "developer"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/windmill-labs/windmill",
+                "https://www.windmill.dev/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/windmill/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Worker Reports is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Worker Reports is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Worker Reports is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Caddy is able to bind to privileged ports (< 1024)",
+                    "name": "NET_BIND_SERVICE"
+                },
+                {
+                    "description": "Worker Reports is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Worker Reports is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Windmill runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "windmill",
+                    "uid": 1000,
+                    "user_name": "windmill"
+                },
+                {
+                    "description": "Windmill LSP runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "windmill",
+                    "uid": 1000,
+                    "user_name": "windmill"
+                },
+                {
+                    "description": "Windmill Worker runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "windmill",
+                    "uid": 1000,
+                    "user_name": "windmill"
+                },
+                {
+                    "description": "Windmill Native Worker runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "windmill",
+                    "uid": 1000,
+                    "user_name": "windmill"
+                },
+                {
+                    "description": "Windmill Reports Worker runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Caddy runs as non-root user",
+                    "gid": 1000,
+                    "group_name": "caddy",
+                    "uid": 1000,
+                    "user_name": "caddy"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "paperless-ngx": {
+            "app_readme": "<h1>Paperless-ngx</h1> <p><a href=\"https://docs.paperless-ngx.com\">Paperless-ngx</a> is a document management system that transforms your physical documents into a searchable online archive so you can keep, well, less paper.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Paperless-ngx is a document management system that transforms your physical documents into a searchable online archive so you can keep, well, less paper.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://docs.paperless-ngx.com",
+            "location": "/__w/apps/apps/trains/community/paperless-ngx",
+            "latest_version": "1.3.22",
+            "latest_app_version": "2.18.3",
+            "latest_human_version": "2.18.3_1.3.22",
+            "last_update": "2025-09-02 15:29:51",
+            "name": "paperless-ngx",
+            "recommended": false,
+            "title": "Paperless-ngx",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "document",
+                "management"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/paperless-ngx/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/paperless-ngx/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/paperless-ngx/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/paperless-ngx/screenshots/screenshot4.png",
+                "https://media.sys.truenas.net/apps/paperless-ngx/screenshots/screenshot5.png",
+                "https://media.sys.truenas.net/apps/paperless-ngx/screenshots/screenshot6.png",
+                "https://media.sys.truenas.net/apps/paperless-ngx/screenshots/screenshot7.png",
+                "https://media.sys.truenas.net/apps/paperless-ngx/screenshots/screenshot8.png"
+            ],
+            "sources": [
+                "https://hub.docker.com/r/paperlessngx/paperless-ngx",
+                "https://github.com/paperless-ngx/paperless-ngx"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/paperless-ngx/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Paperless is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Paperless is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Paperless is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Paperless is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Paperless is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Paperless-ngx runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                },
+                {
+                    "description": "Tika runs as a non-root user.",
+                    "gid": 35002,
+                    "group_name": "tika",
+                    "uid": 35002,
+                    "user_name": "tika"
+                },
+                {
+                    "description": "Gotenberg runs as a non-root user.",
+                    "gid": 1001,
+                    "group_name": "gotenberg",
+                    "uid": 1001,
+                    "user_name": "gotenberg"
+                }
+            ]
+        },
+        "tdarr": {
+            "app_readme": "<h1>Tdarr</h1> <p><a href=\"https://home.tdarr.io/\">Tdarr</a> is a Distributed Transcoding System</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Tdarr is a Distributed Transcoding System",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://home.tdarr.io/",
+            "location": "/__w/apps/apps/trains/community/tdarr",
+            "latest_version": "1.2.10",
+            "latest_app_version": "2.46.01",
+            "latest_human_version": "2.46.01_1.2.10",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "tdarr",
+            "recommended": false,
+            "title": "Tdarr",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "encode",
+                "transcode"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/tdarr/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/tdarr/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/tdarr/screenshots/screenshot3.png"
+            ],
+            "sources": [
+                "https://home.tdarr.io/",
+                "https://docs.tdarr.io/docs"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/tdarr/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Tdarr is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Tdarr is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Tdarr is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Tdarr is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Tdarr runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "eclipse-mosquitto": {
+            "app_readme": "<h1>Eclipse Mosquitto</h1> <p><a href=\"https://mosquitto.org/\">Eclipse Mosquitto</a> is an open source MQTT broker</p> <p>Eclipse Mosquitto is an open source MQTT broker</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "Eclipse Mosquitto is an open source MQTT broker",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://mosquitto.org/",
+            "location": "/__w/apps/apps/trains/community/eclipse-mosquitto",
+            "latest_version": "1.1.7",
+            "latest_app_version": "2.0.22",
+            "latest_human_version": "2.0.22_1.1.7",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "eclipse-mosquitto",
+            "recommended": false,
+            "title": "Eclipse Mosquitto",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "networking",
+                "mqtt"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/eclipse-mosquitto/mosquitto",
+                "https://hub.docker.com/_/eclipse-mosquitto"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/eclipse-mosquitto/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Mosquitto runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "mosquitto",
+                    "uid": 568,
+                    "user_name": "mosquitto"
+                }
+            ]
+        },
+        "urbackup": {
+            "app_readme": "<h1>UrBackup</h1> <p><a href=\"https://github.com/uroni/urbackup_backend\">UrBackup</a> UrBackup is an easy to setup Open Source client/server backup system, that through a combination of image and file backups accomplishes both data safety and a fast restoration time</p>",
+            "categories": [
+                "backup"
+            ],
+            "description": "UrBackup is an easy to setup Open Source client/server backup system, that through a combination of image and file backups accomplishes both data safety and a fast restoration time.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/uroni/urbackup_backend",
+            "location": "/__w/apps/apps/trains/community/urbackup",
+            "latest_version": "1.0.12",
+            "latest_app_version": "2.5.x",
+            "latest_human_version": "2.5.x_1.0.12",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "urbackup",
+            "recommended": false,
+            "title": "UrBackup",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "backup"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/uroni/urbackup_backend",
+                "https://hub.docker.com/r/uroni/urbackup-server"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/urbackup/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Urbackup Server is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Urbackup Server is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Urbackup Server is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Urbackup Server is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Urbackup Server is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "UrBackup runs as the root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "calibre": {
+            "app_readme": "<h1>Calibre</h1> <p><a href=\"https://calibre-ebook.com/\">Calibre</a> is the one stop solution for all your e-book needs.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Calibre is the one stop solution for all your e-book needs.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://calibre-ebook.com/",
+            "location": "/__w/apps/apps/trains/community/calibre",
+            "latest_version": "1.1.12",
+            "latest_app_version": "8.10.0",
+            "latest_human_version": "8.10.0_1.1.12",
+            "last_update": "2025-09-05 14:25:56",
+            "name": "calibre",
+            "recommended": false,
+            "title": "Calibre",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "ebooks"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/calibre/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/calibre/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/calibre/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/calibre/screenshots/screenshot4.png",
+                "https://media.sys.truenas.net/apps/calibre/screenshots/screenshot5.png",
+                "https://media.sys.truenas.net/apps/calibre/screenshots/screenshot6.png"
+            ],
+            "sources": [
+                "https://github.com/linuxserver/docker-calibre",
+                "https://github.com/kovidgoyal/calibre",
+                "https://calibre-ebook.com/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/calibre/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Calibre is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Calibre is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Calibre is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Calibre is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Calibre is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Calibre runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "fireshare": {
+            "app_readme": "<h1>Fireshare</h1> <p><a href=\"https://fireshare.net\">Fireshare</a> is a self-hosted file sharing platform that allows you to share your files with others.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Share your game clips, videos, or other media via unique links.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/ShaneIsrael/fireshare",
+            "location": "/__w/apps/apps/trains/community/fireshare",
+            "latest_version": "1.0.12",
+            "latest_app_version": "v1.2.30",
+            "latest_human_version": "v1.2.30_1.0.12",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "fireshare",
+            "recommended": false,
+            "title": "Fireshare",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "file-sharing",
+                "video-sharing"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/fireshare/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/ShaneIsrael/fireshare",
+                "https://hub.docker.com/r/shaneisrael/fireshare"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/fireshare/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Fireshare is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Fireshare is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Fireshare is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Fireshare is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Fireshare is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Fireshare runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "dozzle": {
+            "app_readme": "<h1>Dozzle</h1> <p><a href=\"https://dozzle.dev\">Dozzle</a> - Realtime log viewer for docker containers.</p>",
+            "categories": [
+                "monitoring"
+            ],
+            "description": "Realtime log viewer for docker containers.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://dozzle.dev",
+            "location": "/__w/apps/apps/trains/community/dozzle",
+            "latest_version": "1.0.54",
+            "latest_app_version": "v8.13.12",
+            "latest_human_version": "v8.13.12_1.0.54",
+            "last_update": "2025-09-04 12:07:56",
+            "name": "dozzle",
+            "recommended": false,
+            "title": "Dozzle",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "logs"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/amir20/dozzle"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/dozzle/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Dozzle runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "dozzle",
+                    "uid": 568,
+                    "user_name": "dozzle"
+                }
+            ]
+        },
+        "prowlarr": {
+            "app_readme": "<h1>Prowlarr</h1> <p><a href=\"https://github.com/Prowlarr/Prowlarr\">Prowlarr</a> is a music collection manager for Usenet and BitTorrent users.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Prowlarr is an indexer manager/proxy to integrate with your various PVR apps.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://prowlarr.com",
+            "location": "/__w/apps/apps/trains/community/prowlarr",
+            "latest_version": "1.4.12",
+            "latest_app_version": "2.0.5.5160",
+            "latest_human_version": "2.0.5.5160_1.4.12",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "prowlarr",
+            "recommended": false,
+            "title": "Prowlarr",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "indexer"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/prowlarr/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/home-operations/containers/tree/main/apps/prowlarr",
+                "https://prowlarr.com"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/prowlarr/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Prowlarr runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "prowlarr",
+                    "uid": 568,
+                    "user_name": "prowlarr"
+                }
+            ]
+        },
+        "convertx": {
+            "app_readme": "<h1>ConvertX</h1> <p><a href=\"https://github.com/C4illin/ConvertX\">ConvertX</a> is a self-hosted online file converter with support for over 1000 formats</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "ConvertX is a self-hosted online file converter with support for over 1000 formats",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/C4illin/ConvertX",
+            "location": "/__w/apps/apps/trains/community/convertx",
+            "latest_version": "1.0.8",
+            "latest_app_version": "v0.14.1",
+            "latest_human_version": "v0.14.1_1.0.8",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "convertx",
+            "recommended": false,
+            "title": "ConvertX",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "convertx",
+                "files",
+                "conversion"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/C4illin/ConvertX"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/convertx/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "ConvertX runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "convertx",
+                    "uid": 568,
+                    "user_name": "convertx"
+                }
+            ]
+        },
+        "terraria": {
+            "app_readme": "<h1>Terraria</h1> <p><a href=\"https://terraria.org/\">Terraria</a> is a land of adventure! A land of mystery! A land that's yours to shape, defend, and enjoy.</p> <p><strong>NOTE:</strong> The following applies only for the <code>TShock</code> image. On the first run, you have to check the logs to get the server token. You will find something like this:</p> <p><code>text Login before join enabled. Users may be prompted for an account specific password instead of a server password on connect. Login using UUID enabled. Users automatically login via UUID. A malicious server can easily steal a user's UUID. You may consider turning this option off if you run a public server. TShock Notice: setup-code.txt is still present, and the code located in that file will be used. To setup the server, join the game and type /setup 424041 This token will display until disabled by verification. (/setup)</code></p> <p>Join the server and run <code>/setup &lt;token&gt;</code></p>",
+            "categories": [
+                "games"
+            ],
+            "description": "Terraria is a land of adventure! A land of mystery! A land that's yours to shape, defend, and enjoy.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://terraria.org/",
+            "location": "/__w/apps/apps/trains/community/terraria",
+            "latest_version": "1.2.6",
+            "latest_app_version": "tshock-1.4.4.9-5.2.4",
+            "latest_human_version": "tshock-1.4.4.9-5.2.4_1.2.6",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "terraria",
+            "recommended": false,
+            "title": "Terraria",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "adventure",
+                "building",
+                "game",
+                "terraria",
+                "world"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://terraria.org/",
+                "https://github.com/ryansheehan/terraria"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/terraria/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Terraria runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "odoo": {
+            "app_readme": "<h1>Odoo</h1> <p><a href=\"https://odoo.com\">Odoo</a> is a suite of web based open source business apps.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Odoo is a suite of web based open source business apps.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.odoo.com/",
+            "location": "/__w/apps/apps/trains/community/odoo",
+            "latest_version": "1.3.10",
+            "latest_app_version": "18.0-20250218",
+            "latest_human_version": "18.0-20250218_1.3.10",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "odoo",
+            "recommended": false,
+            "title": "Odoo",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "erp",
+                "odoo"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/odoo/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/odoo/screenshots/screenshot2.png"
+            ],
+            "sources": [
+                "https://hub.docker.com/_/odoo",
+                "https://github.com/odoo/odoo"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/odoo/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Odoo runs as non-root user.",
+                    "gid": 101,
+                    "group_name": "odoo",
+                    "uid": 100,
+                    "user_name": "odoo"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "esphome": {
+            "app_readme": "<h1>ESPHome</h1> <p><a href=\"https://github.com/esphome/esphome\">ESPHome</a> is a system to control your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems.</p>",
+            "categories": [
+                "home-automation"
+            ],
+            "description": "ESPHome is a system to control your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://esphome.io",
+            "location": "/__w/apps/apps/trains/community/esphome",
+            "latest_version": "1.1.37",
+            "latest_app_version": "2025.8.3",
+            "latest_human_version": "2025.8.3_1.1.37",
+            "last_update": "2025-09-04 12:07:56",
+            "name": "esphome",
+            "recommended": false,
+            "title": "ESPHome",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "home-automation"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/esphome/esphome"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/esphome/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "ESPHome is able to use raw and packet sockets",
+                    "name": "NET_RAW"
+                },
+                {
+                    "description": "ESPHome is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "ESPHome runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "esphome",
+                    "uid": 568,
+                    "user_name": "esphome"
+                }
+            ]
+        },
+        "tiny-media-manager": {
+            "app_readme": "<h1>tinyMediaManager</h1> <p><a href=\"https://www.tinymediamanager.org/\">tinyMediaManager</a> is a media management tool written in Java/Swing.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Tiny Media Manager is a media management tool written in Java/Swing.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.tinymediamanager.org/",
+            "location": "/__w/apps/apps/trains/community/tiny-media-manager",
+            "latest_version": "1.2.9",
+            "latest_app_version": "5.2.0",
+            "latest_human_version": "5.2.0_1.2.9",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "tiny-media-manager",
+            "recommended": false,
+            "title": "Tiny Media Manager",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "tv-shows",
+                "movies"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/tiny-media-manager/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/tiny-media-manager/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/tiny-media-manager/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/tiny-media-manager/screenshots/screenshot4.png"
+            ],
+            "sources": [
+                "https://www.tinymediamanager.org/",
+                "https://hub.docker.com/r/tinymediamanager/tinymediamanager"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/tiny-media-manager/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Tiny Media Manager is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Tiny Media Manager is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Tiny Media Manager is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Tiny Media Manager runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "forgejo-runner": {
+            "app_readme": "<h1>Forgejo Runner</h1> <p><a href=\"https://forgejo.org/docs/latest/admin/actions/runner-installation/\">Forgejo Runner</a> is a runner for Forgejo.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "A runner for Forgejo.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://forgejo.org/",
+            "location": "/__w/apps/apps/trains/community/forgejo-runner",
+            "latest_version": "1.0.5",
+            "latest_app_version": "10.0.1",
+            "latest_human_version": "10.0.1_1.0.5",
+            "last_update": "2025-09-02 13:52:07",
+            "name": "forgejo-runner",
+            "recommended": false,
+            "title": "Forgejo Runner",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "forgejo",
+                "actions",
+                "runner"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://forgejo.org/docs/latest/admin/actions/runner-installation/",
+                "https://code.forgejo.org/forgejo/runner"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/forgejo-runner/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Forgejo Runner runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "forgejo-runner",
+                    "uid": 568,
+                    "user_name": "forgejo-runner"
+                }
+            ]
+        },
+        "mumble": {
+            "app_readme": "<h1>Mumble</h1> <p><a href=\"https://www.mumble.info/\">Mumble</a> is an Open Source, Low Latency, High Quality Voice Chat Home Downloads Documentation Blog Contribute About</p> <p>You can change the server configuration by adding additional environment variables. Prefix the configuration variable with <code>MUMBLE_CONFIG_</code> and it will be added to the configuration file. View the <a href=\"https://wiki.mumble.info/wiki/Murmur.ini\">Mumble Configuration File</a> for more information.</p> <p>For example you can set <code>autobanAttempts</code> like this:</p> <ul> <li>Name: <code>MUMBLE_CONFIG_autobanAttempts</code></li> <li>Value: <code>5</code></li> </ul>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Mumble is a free, open source, low latency, high quality voice chat application.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.mumble.info/",
+            "location": "/__w/apps/apps/trains/community/mumble",
+            "latest_version": "1.3.5",
+            "latest_app_version": "v1.5.735",
+            "latest_human_version": "v1.5.735_1.3.5",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "mumble",
+            "recommended": false,
+            "title": "Mumble",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "voice"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/mumble-voip/mumble-docker",
+                "https://www.mumble.info/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/mumble/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Mumble runs as a non-root user.",
+                    "gid": 1000,
+                    "group_name": "mumble",
+                    "uid": 1000,
+                    "user_name": "mumble"
+                }
+            ]
+        },
+        "readarr": {
+            "app_readme": "<h1>Readarr</h1> <p><a href=\"https://github.com/Readarr/Readarr\">Readarr</a> is an ebook and audiobook collection manager for Usenet and BitTorrent users.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Readarr is an ebook and audiobook collection manager for Usenet and BitTorrent users.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/Readarr/Readarr",
+            "location": "/__w/apps/apps/trains/community/readarr",
+            "latest_version": "1.2.10",
+            "latest_app_version": "0.4.18.2805",
+            "latest_human_version": "0.4.18.2805_1.2.10",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "readarr",
+            "recommended": false,
+            "title": "Readarr (Deprecated)",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "ebook",
+                "audiobook"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/readarr/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/readarr/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/readarr/screenshots/screenshot3.png"
+            ],
+            "sources": [
+                "https://github.com/Readarr/Readarr",
+                "https://github.com/home-operations/containers/tree/main/apps/readarr"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/readarr/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Readarr runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "readarr",
+                    "uid": 568,
+                    "user_name": "readarr"
+                }
+            ]
+        },
+        "kerberos-agent": {
+            "app_readme": "<h1>Kerberos.io Agent</h1> <p><a href=\"https://kerberos.io/\">Kerberos.io</a> is an open and scalable video surveillance system for anyone making this world a better and more peaceful place.</p>",
+            "categories": [
+                "cameras"
+            ],
+            "description": "An open and scalable video surveillance system for anyone making this world a better and more peaceful place.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://kerberos.io/",
+            "location": "/__w/apps/apps/trains/community/kerberos-agent",
+            "latest_version": "1.0.29",
+            "latest_app_version": "v3.5.5",
+            "latest_human_version": "v3.5.5_1.0.29",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "kerberos-agent",
+            "recommended": false,
+            "title": "Kerberos.io Agent",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "kerberos",
+                "security",
+                "video"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/kerberos-agent/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/kerberos-agent/screenshots/screenshot2.png"
+            ],
+            "sources": [
+                "https://github.com/kerberos-io/agent"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/kerberos-agent/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Kerberos Agent is able to bind to privileged ports (< 1024)",
+                    "name": "NET_BIND_SERVICE"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Kerberos Agent runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "kerberos-agent",
+                    "uid": 568,
+                    "user_name": "kerberos-agent"
+                }
+            ]
+        },
+        "pocket-id": {
+            "app_readme": "<h1>Pocket ID</h1> <p><a href=\"https://pocket-id.org\">Pocket ID</a> is a simple and easy-to-use OIDC provider that allows users to authenticate with their passkeys to your services.</p>",
+            "categories": [
+                "authentication"
+            ],
+            "description": "Pocket ID is a simple and easy-to-use OIDC provider that allows users to authenticate with their passkeys to your services.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://pocket-id.org",
+            "location": "/__w/apps/apps/trains/community/pocket-id",
+            "latest_version": "1.0.10",
+            "latest_app_version": "v1.10.0",
+            "latest_human_version": "v1.10.0_1.0.10",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "pocket-id",
+            "recommended": false,
+            "title": "Pocket ID",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "authentication",
+                "oidc",
+                "passkeys",
+                "openid"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/pocket-id/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/pocket-id/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/pocket-id/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/pocket-id/screenshots/screenshot4.png",
+                "https://media.sys.truenas.net/apps/pocket-id/screenshots/screenshot5.png"
+            ],
+            "sources": [
+                "https://github.com/pocket-id/pocket-id"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/pocket-id/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Pocket ID runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "pocket-id",
+                    "uid": 568,
+                    "user_name": "pocket-id"
+                }
+            ]
+        },
+        "three-proxy": {
+            "app_readme": "<h1>3proxy</h1> <p><a href=\"https://3proxy.org/\">3proxy</a> is a powerful and lightweight HTTP/SOCKS proxy server.</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "3proxy is a powerful and lightweight HTTP/SOCKS proxy server.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/3proxy/3proxy",
+            "location": "/__w/apps/apps/trains/community/three-proxy",
+            "latest_version": "1.0.3",
+            "latest_app_version": "1.12.0",
+            "latest_human_version": "1.12.0_1.0.3",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "three-proxy",
+            "recommended": false,
+            "title": "3proxy",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "3proxy",
+                "http",
+                "socks"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/3proxy/3proxy/",
+                "https://github.com/tarampampam/3proxy-docker"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/three-proxy/icons/icon.webp",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "3proxy runs as non-root user.",
+                    "gid": 10001,
+                    "group_name": "3proxy",
+                    "uid": 10001,
+                    "user_name": "3proxy"
+                }
+            ]
+        },
+        "playwright": {
+            "app_readme": "<h1>Playwright</h1> <p><a href=\"https://playwright.dev/\">Playwright</a> is a testing framework for end-to-end testing of web applications. Playwright can automate user interactions in Chromium, Firefox and WebKit browsers with a single API.</p>",
+            "categories": [
+                "development"
+            ],
+            "description": "Playwright enables reliable end-to-end testing for modern web apps.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://playwright.dev/",
+            "location": "/__w/apps/apps/trains/community/playwright",
+            "latest_version": "1.0.15",
+            "latest_app_version": "v1.55.0-noble",
+            "latest_human_version": "v1.55.0-noble_1.0.15",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "playwright",
+            "recommended": false,
+            "title": "Playwright",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "development",
+                "testing",
+                "automation"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/playwright/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/playwright/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/playwright/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/playwright/screenshots/screenshot4.png"
+            ],
+            "sources": [
+                "https://playwright.dev/docs/test-ui-mode",
+                "https://playwright.dev/docs/docker",
+                "https://mcr.microsoft.com/en-us/artifact/mar/playwright"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/playwright/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Playwright runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "pwuser",
+                    "uid": 568,
+                    "user_name": "pwuser"
+                }
+            ]
+        },
+        "dawarich": {
+            "app_readme": "<h1>Dawarich</h1> <p><a href=\"https://dawarich.app/\">Dawarich</a> is a self-hostable alternative to Google Location History (Google Maps Timeline)</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Dawarich is a self-hostable alternative to Google Location History (Google Maps Timeline)",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://dawarich.app/",
+            "location": "/__w/apps/apps/trains/community/dawarich",
+            "latest_version": "1.1.23",
+            "latest_app_version": "0.30.12",
+            "latest_human_version": "0.30.12_1.1.23",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "dawarich",
+            "recommended": false,
+            "title": "Dawarich",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "location",
+                "history"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/dawarich/screenshots/screenshot1.jpg",
+                "https://media.sys.truenas.net/apps/dawarich/screenshots/screenshot2.jpg",
+                "https://media.sys.truenas.net/apps/dawarich/screenshots/screenshot3.jpg"
+            ],
+            "sources": [
+                "https://github.com/Freika/dawarich"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/dawarich/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Dawarich runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                }
+            ]
+        },
+        "scrypted": {
+            "app_readme": "<h1>Scrypted</h1> <p><a href=\"https://www.scrypted.app/\">Scrypted</a> is a high performance video integration and automation platform.</p>",
+            "categories": [
+                "home-automation"
+            ],
+            "description": "Scrypted is a high performance video integration and automation platform",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.scrypted.app/",
+            "location": "/__w/apps/apps/trains/community/scrypted",
+            "latest_version": "1.0.26",
+            "latest_app_version": "v0.142.6-noble-full",
+            "latest_human_version": "v0.142.6-noble-full_1.0.26",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "scrypted",
+            "recommended": false,
+            "title": "Scrypted",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "video",
+                "automation"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/koush/scrypted"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/scrypted/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Scrypted runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "bitcoind": {
+            "app_readme": "<h1>Bitcoin Node</h1> <p>Run your personal node powered by <a href=\"https://bitcoincore.org/\">Bitcoin Core</a>.</p>",
+            "categories": [
+                "financial"
+            ],
+            "description": "Run your personal node powered by Bitcoin Core.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://bitcoin.org",
+            "location": "/__w/apps/apps/trains/community/bitcoind",
+            "latest_version": "1.0.12",
+            "latest_app_version": "29.0",
+            "latest_human_version": "29.0_1.0.12",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "bitcoind",
+            "recommended": false,
+            "title": "Bitcoin Node",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "bitcoin",
+                "cryptocurrency",
+                "blockchain"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://bitcoin.org",
+                "https://github.com/sethforprivacy/docker-bitcoind"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/bitcoind/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Bitcoin Node runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "bitcoin",
+                    "uid": 1000,
+                    "user_name": "bitcoin"
+                }
+            ]
+        },
+        "penpot": {
+            "app_readme": "<h1>Penpot</h1> <p><a href=\"https://penpot.app/\">Penpot</a> - The open-source design tool for design and code collaboration</p> <p>Penpot is the first open-source design tool for design and code collaboration. Designers can create stunning designs, interactive prototypes, design systems at scale, while developers enjoy ready-to-use code and make their workflow easy and fast. And all of this with no handoff drama.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Penpot - The open-source design tool for design and code collaboration",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://penpot.app/",
+            "location": "/__w/apps/apps/trains/community/penpot",
+            "latest_version": "1.2.16",
+            "latest_app_version": "2.9.0",
+            "latest_human_version": "2.9.0_1.2.16",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "penpot",
+            "recommended": false,
+            "title": "Penpot",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "design"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/penpot/screenshots/screenshot1.webp",
+                "https://media.sys.truenas.net/apps/penpot/screenshots/screenshot2.webp",
+                "https://media.sys.truenas.net/apps/penpot/screenshots/screenshot3.webp",
+                "https://media.sys.truenas.net/apps/penpot/screenshots/screenshot4.webp",
+                "https://media.sys.truenas.net/apps/penpot/screenshots/screenshot5.webp",
+                "https://media.sys.truenas.net/apps/penpot/screenshots/screenshot6.webp"
+            ],
+            "sources": [
+                "https://github.com/penpot/penpot"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/penpot/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Penpot runs as non-root user.",
+                    "gid": 1001,
+                    "group_name": "penpot",
+                    "uid": 1001,
+                    "user_name": "penpot"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                }
+            ]
+        },
+        "autobrr": {
+            "app_readme": "<h1>Autobrr</h1> <p><a href=\"https://github.com/autobrr/autobrr\">Autobrr</a> is the modern download automation tool for torrents and usenet.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Autobrr is the modern download automation tool for torrents and usenet.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/autobrr/autobrr",
+            "location": "/__w/apps/apps/trains/community/autobrr",
+            "latest_version": "1.3.10",
+            "latest_app_version": "v1.65.0",
+            "latest_human_version": "v1.65.0_1.3.10",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "autobrr",
+            "recommended": false,
+            "title": "Autobrr",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "torrent",
+                "usenet"
+            ],
+            "screenshots": [
+                "https://github.com/autobrr/autobrr/raw/develop/.github/images/autobrr-front.png"
+            ],
+            "sources": [
+                "https://autobrr.com/installation/docker",
+                "https://github.com/autobrr/autobrr"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/autobrr/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Autobrr runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "autobrr",
+                    "uid": 568,
+                    "user_name": "autobrr"
+                }
+            ]
+        },
+        "distribution": {
+            "app_readme": "<h1>Distribution</h1> <p><a href=\"https://github.com/distribution/distribution\">Distribution</a> is a toolkit to pack, ship, store, and deliver container content</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Distribution is a toolkit to pack, ship, store, and deliver container content",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/distribution/distribution",
+            "location": "/__w/apps/apps/trains/community/distribution",
+            "latest_version": "1.2.5",
+            "latest_app_version": "3.0.0",
+            "latest_human_version": "3.0.0_1.2.5",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "distribution",
+            "recommended": false,
+            "title": "Distribution",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "registry",
+                "container"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://hub.docker.com/_/registry",
+                "https://distribution.github.io/distribution/",
+                "https://github.com/distribution/distribution"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/distribution/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Distribution runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "distribution",
+                    "uid": 568,
+                    "user_name": "distribution"
+                }
+            ]
+        },
+        "redis": {
+            "app_readme": "<h1>Redis</h1> <p><a href=\"https://redis.io/\">Redis</a>. The open source, in-memory data store used by millions of developers as a database, cache, streaming engine, and message broker.</p>",
+            "categories": [
+                "database"
+            ],
+            "description": "Redis. The open source, in-memory data store used by millions of developers as a database, cache, streaming engine, and message broker.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://redis.io/",
+            "location": "/__w/apps/apps/trains/community/redis",
+            "latest_version": "1.2.10",
+            "latest_app_version": "8.2.1",
+            "latest_human_version": "8.2.1_1.2.10",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "redis",
+            "recommended": false,
+            "title": "Redis",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "cache"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://hub.docker.com/r/bitnami/redis",
+                "https://github.com/bitnami/containers/tree/main/bitnami/redis",
+                "https://redis.io/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/redis/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Redis runs as a any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                }
+            ]
+        },
+        "monitee-agent": {
+            "app_readme": "<h1>Monitee-agent</h1> <p><a href=\"https://github.com/Krillsson/monitee-agent\">Monitee-agent</a> is a cross-platform system monitoring tool.</p>",
+            "categories": [
+                "monitoring"
+            ],
+            "description": "Monitee-agent is a android app based system monitoring tool. See https://monitee.app/get-started/",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/Krillsson/monitee-agent",
+            "location": "/__w/apps/apps/trains/community/monitee-agent",
+            "latest_version": "1.0.14",
+            "latest_app_version": "0.39.1",
+            "latest_human_version": "0.39.1_1.0.14",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "monitee-agent",
+            "recommended": false,
+            "title": "Monitee Agent",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "metric",
+                "monitoring"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://hub.docker.com/r/krillsson/sys-api"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/monitee-agent/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Monitee Agent is able to perform raw I/O operations",
+                    "name": "SYS_RAWIO"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Monitee agent runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "gotify": {
+            "app_readme": "<h1>Gotify</h1> <p><a href=\"https://gotify.net\">Gotify</a> is a simple server for sending and receiving messages in real-time per WebSocket. (Includes a sleek web-ui)</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "A simple server for sending and receiving messages in real-time per WebSocket. (Includes a sleek web-ui)",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://gotify.net",
+            "location": "/__w/apps/apps/trains/community/gotify",
+            "latest_version": "1.0.3",
+            "latest_app_version": "2.6.3",
+            "latest_human_version": "2.6.3_1.0.3",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "gotify",
+            "recommended": false,
+            "title": "Gotify",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "notifications"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/gotify/server"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/gotify/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Gotify runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "gotify",
+                    "uid": 568,
+                    "user_name": "gotify"
+                }
+            ]
+        },
+        "monero-wallet-rpc": {
+            "app_readme": "<h1>Monero Wallet RPC</h1> <p><a href=\"https://www.getmonero.org/\">Monero Wallet RPC</a> is RPC server that allows interaction with Monero wallets programmatically.</p>",
+            "categories": [
+                "financial"
+            ],
+            "description": "A RPC server that allows interaction with Monero wallets programmatically.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.getmonero.org",
+            "location": "/__w/apps/apps/trains/community/monero-wallet-rpc",
+            "latest_version": "1.0.13",
+            "latest_app_version": "v0.18.4.2",
+            "latest_human_version": "v0.18.4.2_1.0.13",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "monero-wallet-rpc",
+            "recommended": false,
+            "title": "Monero Wallet RPC",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "monero",
+                "cryptocurrency",
+                "wallet",
+                "rpc",
+                "blockchain",
+                "privacy"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://www.getmonero.org",
+                "https://github.com/sethforprivacy/simple-monero-wallet-rpc-docker"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/monero-wallet-rpc/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Monero Wallet RPC runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "monero",
+                    "uid": 1000,
+                    "user_name": "monero"
+                }
+            ]
+        },
+        "requestrr": {
+            "app_readme": "<h1>Requestrr</h1> <p><a href=\"https://github.com/thomst08/requestrr\">Requestrr</a> is a Discord chatbot used to connect users to Sonarr/Radarr/Lidarr/Overseerr/Ombi!</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Requestrr is a Discord chatbot used to connect users to Sonarr/Radarr/Lidarr/Overseerr/Ombi!",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/thomst08/requestrr",
+            "location": "/__w/apps/apps/trains/community/requestrr",
+            "latest_version": "1.0.3",
+            "latest_app_version": "v2.1.8",
+            "latest_human_version": "v2.1.8_1.0.3",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "requestrr",
+            "recommended": false,
+            "title": "Requestrr",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/thomst08/requestrr",
+                "https://hub.docker.com/r/thomst08/requestrr"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/requestrr/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Requestrr runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "requestrr",
+                    "uid": 568,
+                    "user_name": "requestrr"
+                }
+            ]
+        },
+        "electrs": {
+            "app_readme": "<h1>Electrs</h1> <p><a href=\"https://github.com/romanz/electrs\">Electrs</a> is an efficient re-implementation of Electrum Server in Rust.</p>",
+            "categories": [
+                "financial"
+            ],
+            "description": "An efficient re-implementation of Electrum Server in Rust.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/romanz/electrs/",
+            "location": "/__w/apps/apps/trains/community/electrs",
+            "latest_version": "1.0.6",
+            "latest_app_version": "v0.10.9",
+            "latest_human_version": "v0.10.9_1.0.6",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "electrs",
+            "recommended": false,
+            "title": "Electrs",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "bitcoin",
+                "electrum",
+                "cryptocurrency",
+                "blockchain"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/romanz/electrs",
+                "https://github.com/getumbrel/docker-electrs"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/electrs/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Electrs runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "electrs",
+                    "uid": 1000,
+                    "user_name": "electrs"
+                }
+            ]
+        },
+        "maintainerr": {
+            "app_readme": "<h1>Maintainerr</h1> <p><a href=\"https://maintainerr.info\">Maintainerr</a> is a media collection maintenance tool for the Plex ecosystem.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Maintainerr is a media collection maintenance tool for the Plex ecosystem.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://maintainerr.info/",
+            "location": "/__w/apps/apps/trains/community/maintainerr",
+            "latest_version": "1.0.12",
+            "latest_app_version": "2.19.0",
+            "latest_human_version": "2.19.0_1.0.12",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "maintainerr",
+            "recommended": false,
+            "title": "Maintainerr",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "plex"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/jorenn92/Maintainerr"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/maintainerr/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Maintainerr runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "maintainerr",
+                    "uid": 568,
+                    "user_name": "maintainerr"
+                }
+            ]
+        },
+        "portracker": {
+            "app_readme": "<h1>portracker</h1> <p><a href=\"https://github.com/mostafa-wahied/portracker\">portracker</a> - A self-hosted, real-time port monitoring and discovery tool</p> <p>portracker provides a live, accurate map of your network by auto-discovering services and their ports. It helps eliminate manual tracking and prevents deployment failures caused by port conflicts.</p>",
+            "categories": [
+                "monitoring"
+            ],
+            "description": "A self-hosted, real-time port monitoring and discovery tool.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/mostafa-wahied/portracker",
+            "location": "/__w/apps/apps/trains/community/portracker",
+            "latest_version": "1.0.9",
+            "latest_app_version": "1.1.0",
+            "latest_human_version": "1.1.0_1.0.9",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "portracker",
+            "recommended": false,
+            "title": "portracker",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "portracker",
+                "ports",
+                "monitoring",
+                "discovery"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/portracker/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/portracker/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/portracker/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/portracker/screenshots/screenshot4.png"
+            ],
+            "sources": [
+                "https://github.com/mostafa-wahied/portracker",
+                "https://hub.docker.com/r/mostafawahied/portracker"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/portracker/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "portracker is able to use raw and packet sockets",
+                    "name": "NET_RAW"
+                },
+                {
+                    "description": "portracker is able to trace and control other processes",
+                    "name": "SYS_PTRACE"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "portracker runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "planka": {
+            "app_readme": "<h1>Planka</h1> <p><a href=\"https://github.com/plankanban/planka\">Planka</a> is an Elegant open source project tracking</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Planka is an Elegant open source project tracking",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/plankanban/planka",
+            "location": "/__w/apps/apps/trains/community/planka",
+            "latest_version": "1.3.10",
+            "latest_app_version": "1.26.3",
+            "latest_human_version": "1.26.3_1.3.10",
+            "last_update": "2025-09-04 14:56:19",
+            "name": "planka",
+            "recommended": false,
+            "title": "Planka",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "kanban",
+                "project",
+                "task"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/planka/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/plankanban/planka"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/planka/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Planka runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "planka",
+                    "uid": 1000,
+                    "user_name": "planka"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "kapowarr": {
+            "app_readme": "<h1>Kapowarr</h1> <p><a href=\"https://casvt.github.io/Kapowarr/\">Kapowarr</a> is a software to build and manage a comic book library, fitting in the *arr suite of software.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Kapowarr is a software to build and manage a comic book library, fitting in the *arr suite of software.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://casvt.github.io/Kapowarr/",
+            "location": "/__w/apps/apps/trains/community/kapowarr",
+            "latest_version": "1.2.6",
+            "latest_app_version": "v1.2.0",
+            "latest_human_version": "v1.2.0_1.2.6",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "kapowarr",
+            "recommended": false,
+            "title": "Kapowarr",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "comic",
+                "media"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/kapowarr/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/kapowarr/screenshots/screenshot2.png"
+            ],
+            "sources": [
+                "https://hub.docker.com/r/mrcas/kapowarr",
+                "https://github.com/Casvt/Kapowarr"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/kapowarr/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Kapowarr runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "mitmproxy": {
+            "app_readme": "<h1>mitmproxy</h1> <p><a href=\"https://mitmproxy.org\">mitmproxy</a> is your swiss-army knife for debugging, testing, privacy measurements, and penetration testing.</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "mitmproxy is your swiss-army knife for debugging, testing, privacy measurements, and penetration testing.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://mitmproxy.org",
+            "location": "/__w/apps/apps/trains/community/mitmproxy",
+            "latest_version": "1.0.12",
+            "latest_app_version": "12.1.2",
+            "latest_human_version": "12.1.2_1.0.12",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "mitmproxy",
+            "recommended": false,
+            "title": "mitmproxy",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "mitmproxy",
+                "network",
+                "proxy"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://hub.docker.com/r/mitmproxy/mitmproxy"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/mitmproxy/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "mitmproxy is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "mitmproxy is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "mitmproxy is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "mitmproxy is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "mitmproxy is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "mitmproxy runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "onlyoffice-document-server": {
+            "app_readme": "<h1>ONLYOFFICE Document Server</h1> <p><a href=\"https://www.onlyoffice.com/\">ONLYOFFICE</a> Document Server is an online office suite comprising viewers and editors for texts, spreadsheets and presentations and enabling collaborative editing in real time.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "ONLYOFFICE Docs is an online office suite comprising viewers and editors for texts, spreadsheets and presentations and enabling collaborative editing in real time",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.onlyoffice.com/",
+            "location": "/__w/apps/apps/trains/community/onlyoffice-document-server",
+            "latest_version": "1.0.27",
+            "latest_app_version": "9.0.4",
+            "latest_human_version": "9.0.4_1.0.27",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "onlyoffice-document-server",
+            "recommended": false,
+            "title": "ONLYOFFICE Document Server",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "document",
+                "server",
+                "office"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/ONLYOFFICE/Docker-DocumentServer",
+                "https://hub.docker.com/r/onlyoffice/documentserver"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/onlyoffice-document-server/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Onlyoffice is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Onlyoffice is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Onlyoffice is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Onlyoffice is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Onlyoffice is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Onlyoffice runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                }
+            ]
+        },
+        "kasm-workspaces": {
+            "app_readme": "<h1>Kasm Workspaces</h1> <p><a href=\"https://www.kasmweb.com\">Kasm Workspaces</a> is a docker container streaming platform for delivering browser-based access to desktops, applications, and web services.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Kasm Workspaces is a docker container streaming platform for delivering browser-based access to desktops, applications, and web services.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.kasmweb.com",
+            "location": "/__w/apps/apps/trains/community/kasm-workspaces",
+            "latest_version": "1.0.14",
+            "latest_app_version": "1.17.0",
+            "latest_human_version": "1.17.0_1.0.14",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "kasm-workspaces",
+            "recommended": false,
+            "title": "Kasm Workspaces",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "kasm",
+                "workspaces"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/linuxserver/docker-kasm"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/kasm-workspaces/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Kasm is able to write records to audit log",
+                    "name": "AUDIT_WRITE"
+                },
+                {
+                    "description": "Kasm is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Kasm is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Kasm is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Kasm is able to preserve set-user-ID and set-group-ID bits",
+                    "name": "FSETID"
+                },
+                {
+                    "description": "Kasm is able to send signals to any process",
+                    "name": "KILL"
+                },
+                {
+                    "description": "Kasm is able to create special files using mknod()",
+                    "name": "MKNOD"
+                },
+                {
+                    "description": "Kasm is able to bind to privileged ports (< 1024)",
+                    "name": "NET_BIND_SERVICE"
+                },
+                {
+                    "description": "Kasm is able to use raw and packet sockets",
+                    "name": "NET_RAW"
+                },
+                {
+                    "description": "Kasm is able to set file capabilities on other files",
+                    "name": "SETFCAP"
+                },
+                {
+                    "description": "Kasm is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Kasm is able to transfer capabilities between processes",
+                    "name": "SETPCAP"
+                },
+                {
+                    "description": "Kasm is able to change user ID of processes",
+                    "name": "SETUID"
+                },
+                {
+                    "description": "Kasm is able to use chroot() system call",
+                    "name": "SYS_CHROOT"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Kasm Workspaces runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "grocy": {
+            "app_readme": "<h1>Grocy</h1> <p><a href=\"https://grocy.info/\">Grocy</a> is a web-based self-hosted groceries &amp; household management solution for your home.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Grocy is a web-based self-hosted groceries & household management solution for your home",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://grocy.info/",
+            "location": "/__w/apps/apps/trains/community/grocy",
+            "latest_version": "1.0.3",
+            "latest_app_version": "4.5.0",
+            "latest_human_version": "4.5.0_1.0.3",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "grocy",
+            "recommended": false,
+            "title": "Grocy",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "groceries"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/linuxserver/docker-grocy",
+                "https://github.com/grocy/grocy",
+                "https://grocy.info/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/grocy/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Grocy is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Grocy is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Grocy is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Grocy is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Grocy is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Grocy runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "firefly-iii": {
+            "app_readme": "<h1>Firefly III</h1> <p><a href=\"https://www.firefly-iii.org/\">Firefly III</a> is a personal finances manager</p>",
+            "categories": [
+                "financial"
+            ],
+            "description": "Firefly III is a personal finances manager",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.firefly-iii.org/",
+            "location": "/__w/apps/apps/trains/community/firefly-iii",
+            "latest_version": "1.6.27",
+            "latest_app_version": "version-6.3.2",
+            "latest_human_version": "version-6.3.2_1.6.27",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "firefly-iii",
+            "recommended": false,
+            "title": "Firefly III",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "finance"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/firefly-iii/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/firefly-iii/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/firefly-iii/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/firefly-iii/screenshots/screenshot4.png"
+            ],
+            "sources": [
+                "https://hub.docker.com/r/fireflyiii/core/",
+                "https://github.com/firefly-iii/firefly-iii"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/firefly-iii/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Importer is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Importer is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Cron, Importer are able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Cron, Importer are able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Firefly III runs as non-root user.",
+                    "gid": 33,
+                    "group_name": "www-data",
+                    "uid": 33,
+                    "user_name": "www-data"
+                },
+                {
+                    "description": "Firefly III Data Importer runs as non-root user.",
+                    "gid": 33,
+                    "group_name": "www-data",
+                    "uid": 33,
+                    "user_name": "www-data"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                }
+            ]
+        },
+        "wordpress": {
+            "app_readme": "<h1>WordPress</h1> <p><a href=\"https://wordpress.org/\">WordPress</a> is a web content management system.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "WordPress is a web content management system",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://wordpress.org",
+            "location": "/__w/apps/apps/trains/community/wordpress",
+            "latest_version": "1.2.10",
+            "latest_app_version": "6.8.2",
+            "latest_human_version": "6.8.2_1.2.10",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "wordpress",
+            "recommended": false,
+            "title": "WordPress",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "cms",
+                "blog"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/wordpress/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/wordpress/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/wordpress/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/wordpress/screenshots/screenshot4.png"
+            ],
+            "sources": [
+                "https://hub.docker.com/_/wordpress"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/wordpress/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "WordPress is able to bind to privileged ports (< 1024)",
+                    "name": "NET_BIND_SERVICE"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "WordPress runs as a non-root user.",
+                    "gid": 33,
+                    "group_name": "www-data",
+                    "uid": 33,
+                    "user_name": "www-data"
+                },
+                {
+                    "description": "MariaDB runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "mariadb",
+                    "uid": 999,
+                    "user_name": "mariadb"
+                }
+            ]
+        },
+        "rsyncd": {
+            "app_readme": "<h1>Rsyncd</h1> <p><a href=\"https://rsync.samba.org/\">Rsyncd</a> is an open source utility that provides fast incremental file transfer.</p>",
+            "categories": [
+                "storage"
+            ],
+            "description": "Rsync is an open source utility that provides fast incremental file transfer.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/truenas/containers/tree/main/apps/rsyncd",
+            "location": "/__w/apps/apps/trains/community/rsyncd",
+            "latest_version": "1.2.7",
+            "latest_app_version": "1.0.2",
+            "latest_human_version": "1.0.2_1.2.7",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "rsyncd",
+            "recommended": false,
+            "title": "Rsync Daemon",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "sync",
+                "rsync",
+                "file transfer"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/truenas/containers/tree/master/apps/rsyncd",
+                "https://hub.docker.com/r/ixsystems/rsyncd"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/rsyncd/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Rsyncd is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Rsyncd is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Rsyncd is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Rsyncd is able to set file capabilities on other files",
+                    "name": "SETFCAP"
+                },
+                {
+                    "description": "Rsyncd is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Rsyncd is able to change user ID of processes",
+                    "name": "SETUID"
+                },
+                {
+                    "description": "Rsyncd is able to use chroot() system call",
+                    "name": "SYS_CHROOT"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Rsync Daemon run as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "codegate": {
+            "app_readme": "<h1>Codegate</h1> <p><a href=\"https://github.com/stacklok/codegate\">Codegate</a>: Security, Workspaces and Muxing for AI Applications, coding assistants, and agentic frameworks.</p>",
+            "categories": [
+                "ai"
+            ],
+            "description": "Security, Workspaces and Muxing for AI Applications, coding assistants, and agentic frameworks.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/stacklok/codegate",
+            "location": "/__w/apps/apps/trains/community/codegate",
+            "latest_version": "1.0.16",
+            "latest_app_version": "v0.1.32",
+            "latest_human_version": "v0.1.32_1.0.16",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "codegate",
+            "recommended": false,
+            "title": "CodeGate (Deprecated)",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "ai",
+                "developer-tool",
+                "security"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/codegate/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/codegate/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/codegate/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/codegate/screenshots/screenshot4.png",
+                "https://media.sys.truenas.net/apps/codegate/screenshots/screenshot5.webp"
+            ],
+            "sources": [
+                "https://github.com/stacklok/codegate",
+                "https://github.com/stacklok/codegate/pkgs/container/codegate"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/codegate/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Codegate runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "codegate",
+                    "uid": 1000,
+                    "user_name": "codegate"
+                }
+            ]
+        },
+        "vikunja": {
+            "app_readme": "<h1>Vikunja</h1> <p><a href=\"https://vikunja.io\">Vikunja</a> is an open-source, self-hostable to-do app.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Vikunja is an open-source, self-hostable to-do app.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://vikunja.io/",
+            "location": "/__w/apps/apps/trains/community/vikunja",
+            "latest_version": "1.5.12",
+            "latest_app_version": "0.24.6",
+            "latest_human_version": "0.24.6_1.5.12",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "vikunja",
+            "recommended": false,
+            "title": "Vikunja",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "todo"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/vikunja/screenshots/screenshot1.webp",
+                "https://media.sys.truenas.net/apps/vikunja/screenshots/screenshot2.webp",
+                "https://media.sys.truenas.net/apps/vikunja/screenshots/screenshot3.webp",
+                "https://media.sys.truenas.net/apps/vikunja/screenshots/screenshot4.webp",
+                "https://media.sys.truenas.net/apps/vikunja/screenshots/screenshot5.webp",
+                "https://media.sys.truenas.net/apps/vikunja/screenshots/screenshot6.webp",
+                "https://media.sys.truenas.net/apps/vikunja/screenshots/screenshot7.webp",
+                "https://media.sys.truenas.net/apps/vikunja/screenshots/screenshot8.webp",
+                "https://media.sys.truenas.net/apps/vikunja/screenshots/screenshot9.webp"
+            ],
+            "sources": [
+                "https://vikunja.io/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/vikunja/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Vikunja runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "vikunja",
+                    "uid": 568,
+                    "user_name": "vikunja"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                }
+            ]
+        },
+        "kimai": {
+            "app_readme": "<h1>Kimai</h1> <p><a href=\"https://www.kimai.org/\">Kimai</a> is a web-based multi-user time-tracking application.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Kimai is a web-based multi-user time-tracking application.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.kimai.org/",
+            "location": "/__w/apps/apps/trains/community/kimai",
+            "latest_version": "1.0.6",
+            "latest_app_version": "apache-2.38.0",
+            "latest_human_version": "apache-2.38.0_1.0.6",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "kimai",
+            "recommended": false,
+            "title": "Kimai",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "time-tracking"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://hub.docker.com/r/kimai/kimai2"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/kimai/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Kimai is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Kimai is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Kimai is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Kimai is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Kimai is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Kimai runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "MariaDB runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "mariadb",
+                    "uid": 999,
+                    "user_name": "mariadb"
+                }
+            ]
+        },
+        "shoko-server": {
+            "app_readme": "<h1>Shoko Server</h1> <p><a href=\"https://shokoanime.com/\">Shoko Server</a> is an All-in-One Cross-Platform Anime Management System Built For You</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "The All-in-One Cross-Platform Anime Management System Built For You",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://shokoanime.com/",
+            "location": "/__w/apps/apps/trains/community/shoko-server",
+            "latest_version": "1.0.3",
+            "latest_app_version": "v5.1.0",
+            "latest_human_version": "v5.1.0_1.0.3",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "shoko-server",
+            "recommended": false,
+            "title": "Shoko Server",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "anime",
+                "management"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/ShokoAnime/ShokoServer"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/shoko-server/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Shoko Server is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Shoko Server is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Shoko Server is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Shoko Server is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Shoko Server is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Shoko Server runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "chia": {
+            "app_readme": "<h1>Chia</h1> <p><a href=\"https://www.chia.net/\">Chia</a> is a modern cryptocurrency built from scratch, designed to be efficient,decentralized, and secure.</p> <p>Key file is stored in <code>/plots/keyfile</code> and is generated automatically, if the file does not exist. If you want to use your own <code>keyfile</code>, you can create a file called <code>keyfile</code> in the <code>/plots</code> directory and it will be used instead.</p> <p>When set on harvester mode keys variable is set to none and no generation is performed.</p>",
+            "categories": [
+                "financial"
+            ],
+            "description": "Chia is a modern cryptocurrency built from scratch, designed to be efficient, decentralized, and secure.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.chia.net/",
+            "location": "/__w/apps/apps/trains/community/chia",
+            "latest_version": "1.2.6",
+            "latest_app_version": "2.5.5",
+            "latest_human_version": "2.5.5_1.2.6",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "chia",
+            "recommended": false,
+            "title": "Chia",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "blockchain",
+                "hard-drive",
+                "chia"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/Chia-Network/chia-docker",
+                "https://www.chia.net/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/chia/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Chia runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "mempool": {
+            "app_readme": "<h1>Mempool</h1> <p><a href=\"https://mempool.space\">Mempool</a> is a fully-featured Bitcoin mempool visualizer, block explorer, and API service.</p>",
+            "categories": [
+                "financial"
+            ],
+            "description": "A fully-featured Bitcoin mempool visualizer, block explorer, and API service.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://mempool.space",
+            "location": "/__w/apps/apps/trains/community/mempool",
+            "latest_version": "1.0.13",
+            "latest_app_version": "v3.2.1",
+            "latest_human_version": "v3.2.1_1.0.13",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "mempool",
+            "recommended": false,
+            "title": "Mempool",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "bitcoin",
+                "cryptocurrency",
+                "blockchain"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/mempool/mempool"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/mempool/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Mempool Backend runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "mempool",
+                    "uid": 1000,
+                    "user_name": "mempool"
+                },
+                {
+                    "description": "Mempool Frontend runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "mempool",
+                    "uid": 1000,
+                    "user_name": "mempool"
+                },
+                {
+                    "description": "MariaDB runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "mariadb",
+                    "uid": 999,
+                    "user_name": "mariadb"
+                }
+            ]
+        },
+        "bazarr": {
+            "app_readme": "<h1>Bazarr</h1> <p><a href=\"https://www.bazarr.media/\">Bazarr</a> is a companion application to Sonarr and Radarr. It manages and downloads subtitles based on your requirements.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Bazarr is a companion application to Sonarr and Radarr. It manages and downloads subtitles based on your requirements.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.bazarr.media",
+            "location": "/__w/apps/apps/trains/community/bazarr",
+            "latest_version": "1.2.6",
+            "latest_app_version": "1.5.2",
+            "latest_human_version": "1.5.2_1.2.6",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "bazarr",
+            "recommended": false,
+            "title": "Bazarr",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "subtitles"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/bazarr/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/bazarr/screenshots/screenshot2.png"
+            ],
+            "sources": [
+                "https://github.com/home-operations/containers/tree/main/apps/bazarr",
+                "https://github.com/morpheus65535/bazarr"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/bazarr/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Bazarr runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "bazarr",
+                    "uid": 568,
+                    "user_name": "bazarr"
+                }
+            ]
+        },
+        "homepage": {
+            "app_readme": "<h1>Homepage</h1> <p><a href=\"https://github.com/benphelps/homepage\">Homepage</a> is a modern, secure, highly customizable application dashboard.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Homepage is a modern, secure, highly customizable application dashboard.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://gethomepage.dev/",
+            "location": "/__w/apps/apps/trains/community/homepage",
+            "latest_version": "1.2.13",
+            "latest_app_version": "v1.4.6",
+            "latest_human_version": "v1.4.6_1.2.13",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "homepage",
+            "recommended": false,
+            "title": "Homepage",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "dashboard"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/homepage/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/homepage/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/homepage/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/homepage/screenshots/screenshot4.png",
+                "https://media.sys.truenas.net/apps/homepage/screenshots/screenshot5.png",
+                "https://media.sys.truenas.net/apps/homepage/screenshots/screenshot6.png"
+            ],
+            "sources": [
+                "https://gethomepage.dev/",
+                "https://github.com/benphelps/homepage"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/homepage/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Homepage runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "homepage",
+                    "uid": 1000,
+                    "user_name": "homepage"
+                }
+            ]
+        },
+        "concourse": {
+            "app_readme": "<h1>Concourse</h1> <p><a href=\"https://concourse-ci.org\">Concourse</a> is a container-based automation system written in Go.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Concourse is a container-based automation system written in Go.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://concourse-ci.org",
+            "location": "/__w/apps/apps/trains/community/concourse",
+            "latest_version": "1.0.16",
+            "latest_app_version": "7.14.1",
+            "latest_human_version": "7.14.1_1.0.16",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "concourse",
+            "recommended": false,
+            "title": "Concourse",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "automation",
+                "ci"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://concourse-ci.org",
+                "https://github.com/concourse/concourse",
+                "https://hub.docker.com/r/concourse/concourse"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/concourse/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Concourse runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "navidrome": {
+            "app_readme": "<h1>Navidrome</h1> <p><a href=\"https://www.navidrome.org/\">Navidrome</a> is a personal streaming service</p> <p>Additional configuration options can be defined via environment variables. See more information on the <a href=\"https://www.navidrome.org/docs/usage/configuration-options\">Navidrome Documentation</a></p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Navidrome is a personal streaming service",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.navidrome.org",
+            "location": "/__w/apps/apps/trains/community/navidrome",
+            "latest_version": "1.2.8",
+            "latest_app_version": "0.58.0",
+            "latest_human_version": "0.58.0_1.2.8",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "navidrome",
+            "recommended": false,
+            "title": "Navidrome",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "music"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/navidrome/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://hub.docker.com/r/deluan/navidrome",
+                "https://github.com/navidrome/navidrome/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/navidrome/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Navidrome runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "navidrome",
+                    "uid": 568,
+                    "user_name": "navidrome"
+                }
+            ]
+        },
+        "stash": {
+            "app_readme": "<h1>Stash</h1> <p><a href=\"https://stashapp.cc\">Stash</a> is an organizer for your NSFW materials, written in Go.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "An organizer for your NSFW materials, written in Go.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://stashapp.cc/",
+            "location": "/__w/apps/apps/trains/community/stash",
+            "latest_version": "1.0.5",
+            "latest_app_version": "v0.28.1",
+            "latest_human_version": "v0.28.1_1.0.5",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "stash",
+            "recommended": false,
+            "title": "Stash",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "series"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://hub.docker.com/r/stashapp/stash",
+                "https://github.com/stashapp/stash"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/stash/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Stash runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "shlink": {
+            "app_readme": "<h1>Shlink</h1> <p><a href=\"https://shlink.io/\">Shlink</a> is the definitive self-hosted URL shortener</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "The definitive self-hosted URL shortener",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://shlink.io/",
+            "location": "/__w/apps/apps/trains/community/shlink",
+            "latest_version": "1.0.12",
+            "latest_app_version": "4.5.2",
+            "latest_human_version": "4.5.2_1.0.12",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "shlink",
+            "recommended": false,
+            "title": "Shlink",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "shortener",
+                "url"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/shlinkio/shlink"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/shlink/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Shlink runs as any root user.",
+                    "gid": 1001,
+                    "group_name": "shlink",
+                    "uid": 1001,
+                    "user_name": "shlink"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                }
+            ]
+        },
+        "garage": {
+            "app_readme": "<h1>Garage</h1> <p><a href=\"https://garagehq.deuxfleurs.fr/\">Garage</a> - An open-source distributed object storage service tailored for self-hosting</p>",
+            "categories": [
+                "storage"
+            ],
+            "description": "An open-source distributed object storage service tailored for self-hosting.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://garagehq.deuxfleurs.fr/",
+            "location": "/__w/apps/apps/trains/community/garage",
+            "latest_version": "1.0.8",
+            "latest_app_version": "v2.0.0",
+            "latest_human_version": "v2.0.0_1.0.8",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "garage",
+            "recommended": false,
+            "title": "Garage",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "garage",
+                "object storage"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://garagehq.deuxfleurs.fr/",
+                "https://git.deuxfleurs.fr/Deuxfleurs/garage",
+                "https://hub.docker.com/r/dxflrs/garage",
+                "https://github.com/khairul169/garage-webui"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/garage/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Garage runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "garage",
+                    "uid": 568,
+                    "user_name": "garage"
+                },
+                {
+                    "description": "Garage Web runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "garage",
+                    "uid": 568,
+                    "user_name": "garage"
+                }
+            ]
+        },
+        "nextpvr": {
+            "app_readme": "<h1>NextPVR</h1> <p><a href=\"https://nextpvr.com\">NextPVR</a> NextPVR is a personal video recorder application, with the goal making it easy to watch or record live TV.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "NextPVR is a personal video recorder application, with the goal making it easy to watch or record live TV",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "http://nextpvr.com/",
+            "location": "/__w/apps/apps/trains/community/nextpvr",
+            "latest_version": "1.0.7",
+            "latest_app_version": "stable",
+            "latest_human_version": "stable_1.0.7",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "nextpvr",
+            "recommended": false,
+            "title": "NextPVR",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "nextpvr",
+                "livetv",
+                "streaming"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://hub.docker.com/r/nextpvr/nextpvr_amd64"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/nextpvr/icons/icon.webp",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "NextPVR runs as non-root user.",
+                    "gid": 568,
+                    "group_name": "nextpvr",
+                    "uid": 568,
+                    "user_name": "nextpvr"
+                }
+            ]
+        },
+        "authelia": {
+            "app_readme": "<h1>Authelia</h1> <p><a href=\"https://www.authelia.com/\">Authelia</a> is a Single Sign-On Multi-Factor portal for web apps.</p>",
+            "categories": [
+                "authentication"
+            ],
+            "description": "The Single Sign-On Multi-Factor portal for web apps",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.authelia.com/",
+            "location": "/__w/apps/apps/trains/community/authelia",
+            "latest_version": "1.0.33",
+            "latest_app_version": "4.39.8",
+            "latest_human_version": "4.39.8_1.0.33",
+            "last_update": "2025-09-02 15:29:51",
+            "name": "authelia",
+            "recommended": false,
+            "title": "Authelia",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "authentication",
+                "sso"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/authelia/authelia"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/authelia/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Authelia runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "authelia",
+                    "uid": 568,
+                    "user_name": "authelia"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                }
+            ]
+        },
+        "handbrake": {
+            "app_readme": "<h1>Handbrake</h1> <p><a href=\"https://github.com/jlesage/docker-handbrake\">Handbrake</a> is a tool for converting video from nearly any format to a selection of modern, widely supported codecs.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "HandBrake is a tool for converting video from nearly any format to a selection of modern, widely supported codecs.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/jlesage/docker-handbrake",
+            "location": "/__w/apps/apps/trains/community/handbrake",
+            "latest_version": "2.2.11",
+            "latest_app_version": "v25.07.2",
+            "latest_human_version": "v25.07.2_2.2.11",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "handbrake",
+            "recommended": false,
+            "title": "Handbrake",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "video",
+                "transcoder"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/jlesage/docker-handbrake",
+                "https://hub.docker.com/r/jlesage/handbrake"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/handbrake/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Handbrake is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Handbrake is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Handbrake is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Handbrake is able to send signals to any process",
+                    "name": "KILL"
+                },
+                {
+                    "description": "Handbrake is able to bind to privileged ports (< 1024)",
+                    "name": "NET_BIND_SERVICE"
+                },
+                {
+                    "description": "Handbrake is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Handbrake is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Handbrake runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "karakeep": {
+            "app_readme": "<h1>Karakeep</h1> <p><a href=\"https://karakeep.app\">Karakeep</a> is a self-hostable bookmark-everything app (links, notes and images) with AI-based automatic tagging and full text search</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Karakeep is a self-hostable bookmark-everything app (links, notes and images) with AI-based automatic tagging and full text search",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://karakeep.app",
+            "location": "/__w/apps/apps/trains/community/karakeep",
+            "latest_version": "1.0.26",
+            "latest_app_version": "0.26.0",
+            "latest_human_version": "0.26.0_1.0.26",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "karakeep",
+            "recommended": false,
+            "title": "Karakeep",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "hoarder",
+                "bookmarks"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/karakeep/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/karakeep-app/karakeep",
+                "https://karakeep.app/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/karakeep/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Karakeep is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Karakeep is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Karakeep is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Karakeep runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Meilisearch runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "meilisearch",
+                    "uid": 568,
+                    "user_name": "meilisearch"
+                },
+                {
+                    "description": "Chrome runs as non-root user.",
+                    "gid": 568,
+                    "group_name": "chrome",
+                    "uid": 568,
+                    "user_name": "chrome"
+                }
+            ]
+        },
+        "omada-controller": {
+            "app_readme": "<h1>Omada Controller</h1> <p><a href=\"https://github.com/mbentley/docker-omada-controller\">Omada Controller</a> is a network management controller for Omada (TP-Link) Equipment.</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "Omada Controller (TP-Link) is a network management controller for TP-Link Omada Equipment",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/mbentley/docker-omada-controller",
+            "location": "/__w/apps/apps/trains/community/omada-controller",
+            "latest_version": "1.3.8",
+            "latest_app_version": "5.15.24.19",
+            "latest_human_version": "5.15.24.19_1.3.8",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "omada-controller",
+            "recommended": false,
+            "title": "Omada Controller",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "network",
+                "controller",
+                "omada",
+                "tp-link"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/mbentley/docker-omada-controller",
+                "https://hub.docker.com/r/mbentley/omada-controller"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/omada-controller/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Omada Controller is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Omada Controller is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Omada Controller is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Omada Controller is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Omada Controller is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Omada Controller runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "memcached": {
+            "app_readme": "<h1>Memcached</h1> <p><a href=\"https://github.com/memcached/memcached\">Memcached</a> is a high-performance, distributed memory object caching system.</p>",
+            "categories": [
+                "database"
+            ],
+            "description": "Memcached is a high-performance, distributed memory object caching system.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://memcached.org/",
+            "location": "/__w/apps/apps/trains/community/memcached",
+            "latest_version": "1.0.4",
+            "latest_app_version": "1.6.39",
+            "latest_human_version": "1.6.39_1.0.4",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "memcached",
+            "recommended": false,
+            "title": "Memcached",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "database",
+                "cache"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://hub.docker.com/_/memcached",
+                "https://github.com/memcached/memcached"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/memcached/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Memcached runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "memcached",
+                    "uid": 568,
+                    "user_name": "memcached"
+                }
+            ]
+        },
+        "qbittorrent": {
+            "app_readme": "<h1>qBittorrent</h1> <p>The <a href=\"https://www.qbittorrent.org/\">qBittorrent</a> project aims to provide an open-source software alternative to \u00b5Torrent.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "The qBittorrent project aims to provide an open-source software alternative to mTorrent.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.qbittorrent.org/",
+            "location": "/__w/apps/apps/trains/community/qbittorrent",
+            "latest_version": "1.2.8",
+            "latest_app_version": "5.1.2",
+            "latest_human_version": "5.1.2_1.2.8",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "qbittorrent",
+            "recommended": false,
+            "title": "qBittorrent",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "torrent",
+                "download"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/qbittorrent/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/home-operations/containers/tree/main/apps/qbittorrent",
+                "https://www.qbittorrent.org/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/qbittorrent/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "qBittorrent runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "qbittorrent",
+                    "uid": 568,
+                    "user_name": "qbittorrent"
+                }
+            ]
+        },
+        "unifi-protect-backup": {
+            "app_readme": "<h1>Unifi Protect Backup</h1> <p><a href=\"https://github.com/ep1cman/unifi-protect-backup\">Unifi Protect Backup</a> is a python based tool for backing up UniFi Protect event clips as they occur.</p>",
+            "categories": [
+                "storage"
+            ],
+            "description": "Unifi Protect Backup is a python based tool for backing up UniFi Protect event clips as they occur.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/ep1cman/unifi-protect-backup",
+            "location": "/__w/apps/apps/trains/community/unifi-protect-backup",
+            "latest_version": "1.2.7",
+            "latest_app_version": "0.14.0",
+            "latest_human_version": "0.14.0_1.2.7",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "unifi-protect-backup",
+            "recommended": false,
+            "title": "Unifi Protect Backup",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "backup",
+                "unifi-protect"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/ep1cman/unifi-protect-backup",
+                "https://github.com/ep1cman/unifi-protect-backup/pkgs/container/unifi-protect-backup"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/unifi-protect-backup/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Unifi Protect Backup is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Unifi Protect Backup is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Unifi Protect Backup is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Unifi Protect Backup is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Unifi Protect Backup runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "twofactor-auth": {
+            "app_readme": "<h1>2FAuth</h1> <p><a href=\"https://docs.2fauth.app/\">2FAuth</a> is a web based self-hosted alternative to One Time Passcode (OTP) generators like Google Authenticator, designed for both mobile and desktop.</p>",
+            "categories": [
+                "security"
+            ],
+            "description": "2FAuth is a web based self-hosted alternative to One Time Passcode (OTP) generators like Google Authenticator, designed for both mobile and desktop.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://docs.2fauth.app/",
+            "location": "/__w/apps/apps/trains/community/twofactor-auth",
+            "latest_version": "1.2.7",
+            "latest_app_version": "5.6.0",
+            "latest_human_version": "5.6.0_1.2.7",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "twofactor-auth",
+            "recommended": false,
+            "title": "2FAuth",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "security",
+                "2fa",
+                "otp"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/twofactor-auth/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/Bubka/2FAuth",
+                "https://hub.docker.com/r/2fauth/2fauth/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/twofactor-auth/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "2FAuth runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "2fauth",
+                    "uid": 1000,
+                    "user_name": "2fauth"
+                }
+            ]
+        },
+        "immich": {
+            "app_readme": "<h1>Immich</h1> <p><a href=\"https://immich.app\">Immich</a> - Self-hosted backup solution for photos and videos on mobile device</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Immich is a self-hosted photo and video backup solution directly from your mobile phone.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://immich.app",
+            "location": "/__w/apps/apps/trains/community/immich",
+            "latest_version": "1.9.23",
+            "latest_app_version": "v1.140.1",
+            "latest_human_version": "v1.140.1_1.9.23",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "immich",
+            "recommended": false,
+            "title": "Immich",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "photos",
+                "backup"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://immich.app",
+                "https://github.com/immich-app/immich"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/immich/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Immich runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "immich",
+                    "uid": 568,
+                    "user_name": "immich"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                }
+            ]
+        },
+        "open-webui": {
+            "app_readme": "<h1>Open WebUI</h1> <p><a href=\"https://github.com/open-webui/open-webui\">Open WebUI</a> - User-friendly AI Interface (Supports Ollama, OpenAI API, ...)</p> <p>Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI designed to operate entirely offline. It supports various LLM runners, including Ollama and OpenAI-compatible APIs.</p>",
+            "categories": [
+                "ai"
+            ],
+            "description": "Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI designed to operate entirely offline. It supports various LLM runners, including Ollama and OpenAI-compatible APIs.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/open-webui/open-webui",
+            "location": "/__w/apps/apps/trains/community/open-webui",
+            "latest_version": "1.1.18",
+            "latest_app_version": "v0.6.26",
+            "latest_human_version": "v0.6.26_1.1.18",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "open-webui",
+            "recommended": false,
+            "title": "Open WebUI",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "ai",
+                "llm",
+                "webui",
+                "open-webui"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/open-webui/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/open-webui/open-webui"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/open-webui/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Open WebUI runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "open-webui",
+                    "uid": 568,
+                    "user_name": "open-webui"
+                }
+            ]
+        },
+        "glances": {
+            "app_readme": "<h1>Glances</h1> <p><a href=\"https://nicolargo.github.io/glances/\">Glances</a> is a cross-platform system monitoring tool.</p>",
+            "categories": [
+                "monitoring"
+            ],
+            "description": "Glances is a cross-platform system monitoring tool.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://nicolargo.github.io/glances",
+            "location": "/__w/apps/apps/trains/community/glances",
+            "latest_version": "1.0.16",
+            "latest_app_version": "4.3.0.8",
+            "latest_human_version": "4.3.0.8_1.0.16",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "glances",
+            "recommended": false,
+            "title": "Glances",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "metric",
+                "monitoring"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/glances/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://hub.docker.com/r/nicolargo/glances"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/glances/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Glances is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Glances is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Glances is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Glances is able to change user ID of processes",
+                    "name": "SETUID"
+                },
+                {
+                    "description": "Glances is able to trace and control other processes",
+                    "name": "SYS_PTRACE"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Glances runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "octoprint": {
+            "app_readme": "<h1>Octoprint</h1> <p><a href=\"https://octoprint.org\">OctoPrint</a> provides a snappy web interface for controlling consumer 3D printers. It is Free Software and released under the GNU Affero General Public License V3.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Octoprint provides a snappy web interface for controlling consumer 3D printers.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://octoprint.org",
+            "location": "/__w/apps/apps/trains/community/octoprint",
+            "latest_version": "1.0.15",
+            "latest_app_version": "1.11.2",
+            "latest_human_version": "1.11.2_1.0.15",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "octoprint",
+            "recommended": false,
+            "title": "Octoprint",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "3D",
+                "printer"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/octopring/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://hub.docker.com/r/octoprint/octoprint",
+                "https://github.com/OctoPrint/OctoPrint"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/octoprint/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Octoprint is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Octoprint is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Octoprint runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "stirling-pdf": {
+            "app_readme": "<h1>Stirling PDF</h1> <p><a href=\"https://www.stirlingpdf.com/\">Stirling PDF</a> - #1 Locally hosted web application that allows you to perform various operations on PDF files</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "#1 Locally hosted web application that allows you to perform various operations on PDF files",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.stirlingpdf.com/",
+            "location": "/__w/apps/apps/trains/community/stirling-pdf",
+            "latest_version": "1.0.32",
+            "latest_app_version": "1.3.1",
+            "latest_human_version": "1.3.1_1.0.32",
+            "last_update": "2025-09-05 20:13:30",
+            "name": "stirling-pdf",
+            "recommended": false,
+            "title": "Stirling PDF",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "pdf",
+                "pdf-editor"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/stirling-pdf/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/stirling-pdf/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/stirling-pdf/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/stirling-pdf/screenshots/screenshot4.png"
+            ],
+            "sources": [
+                "https://github.com/Stirling-Tools/Stirling-PDF",
+                "https://docs.stirlingpdf.com/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/stirling-pdf/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Stirling PDF is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Stirling PDF is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Stirling PDF is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Stirling PDF is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Stirling PDF is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Stirling PDF runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "influxdb": {
+            "app_readme": "<h1>InfluxDB</h1> <p><a href=\"https://www.influxdata.com/\">InfluxDB</a> is an open source time series database for recording metrics, events, and analytics.</p>",
+            "categories": [
+                "database"
+            ],
+            "description": "Scalable datastore for metrics, events, and real-time analytics",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://influxdata.com",
+            "location": "/__w/apps/apps/trains/community/influxdb",
+            "latest_version": "1.0.12",
+            "latest_app_version": "2.7.12",
+            "latest_human_version": "2.7.12_1.0.12",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "influxdb",
+            "recommended": false,
+            "title": "InfluxDB",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "metrics"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/influxdata/influxdb",
+                "https://github.com/influxdata/influxdata-docker"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/influxdb/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "InfluxDB runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "influxdb",
+                    "uid": 568,
+                    "user_name": "influxdb"
+                }
+            ]
+        },
+        "bookstack": {
+            "app_readme": "<h1>BookStack</h1> <p><a href=\"https://www.bookstackapp.com/\">BookStack</a> is a simple, self-hosted, easy-to-use platform for organizing and storing information.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "BookStack is a simple, self-hosted, easy-to-use platform for organizing and storing information",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.bookstackapp.com/",
+            "location": "/__w/apps/apps/trains/community/bookstack",
+            "latest_version": "1.0.11",
+            "latest_app_version": "25.7.2",
+            "latest_human_version": "25.7.2_1.0.11",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "bookstack",
+            "recommended": false,
+            "title": "BookStack",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "bookstack",
+                "documentation",
+                "wiki"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/solidnerd/docker-bookstack",
+                "https://hub.docker.com/r/solidnerd/bookstack"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/bookstack/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "BookStack runs as non-root user.",
+                    "gid": 33,
+                    "group_name": "www-data",
+                    "uid": 33,
+                    "user_name": "www-data"
+                },
+                {
+                    "description": "MariaDB runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "mariadb",
+                    "uid": 999,
+                    "user_name": "mariadb"
+                }
+            ]
+        },
+        "postgres": {
+            "app_readme": "<h1>Postgres</h1> <p><a href=\"https://www.postgresql.org\">Postgres</a> - Object-relational database system provides reliability and data integrity.</p> <p>The PostgreSQL object-relational database system provides reliability and data integrity.</p>",
+            "categories": [
+                "database"
+            ],
+            "description": "The PostgreSQL object-relational database system provides reliability and data integrity.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.postgresql.org",
+            "location": "/__w/apps/apps/trains/community/postgres",
+            "latest_version": "1.1.8",
+            "latest_app_version": "17.6",
+            "latest_human_version": "17.6_1.1.8",
+            "last_update": "2025-09-05 14:25:56",
+            "name": "postgres",
+            "recommended": false,
+            "title": "Postgres",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "database"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://hub.docker.com/_/postgres"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/postgres/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "invoice-ninja": {
+            "app_readme": "<h1>Invoice Ninja</h1> <p><a href=\"https://invoiceninja.com/\">Invoice Ninja</a> is a source-available invoice, quote, project and time-tracking app built with Laravel</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Invoices, Expenses and Tasks built with Laravel, Flutter and React",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://invoiceninja.com/",
+            "location": "/__w/apps/apps/trains/community/invoice-ninja",
+            "latest_version": "1.0.93",
+            "latest_app_version": "5.12.26",
+            "latest_human_version": "5.12.26_1.0.93",
+            "last_update": "2025-09-04 12:07:56",
+            "name": "invoice-ninja",
+            "recommended": false,
+            "title": "Invoice Ninja",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "finance"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/invoiceninja/dockerfiles",
+                "https://hub.docker.com/r/invoiceninja/invoiceninja-octane"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/invoice-ninja/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Invoice Ninja, Scheduler, Worker are able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Invoice Ninja, Scheduler, Worker are able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Invoice Ninja, Scheduler, Worker are able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Invoice Ninja, Scheduler, Worker are able to bind to privileged ports (< 1024)",
+                    "name": "NET_BIND_SERVICE"
+                },
+                {
+                    "description": "Invoice Ninja, Scheduler, Worker are able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Invoice Ninja, Scheduler, Worker are able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Invoice Ninja runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "invoice-ninja",
+                    "uid": 999,
+                    "user_name": "invoice-ninja"
+                },
+                {
+                    "description": "Invoice Ninja Worker runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "invoice-ninja",
+                    "uid": 999,
+                    "user_name": "invoice-ninja"
+                },
+                {
+                    "description": "Invoice Ninja Scheduler runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "invoice-ninja",
+                    "uid": 999,
+                    "user_name": "invoice-ninja"
+                },
+                {
+                    "description": "MariaDB runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "mariadb",
+                    "uid": 999,
+                    "user_name": "mariadb"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                }
+            ]
+        },
+        "vaultwarden": {
+            "app_readme": "<h1>Vaultwarden</h1> <p><a href=\"https://github.com/dani-garcia/vaultwarden\">Vaultwarden</a> Alternative implementation of the <code>Bitwarden</code> server API written in Rust and compatible with upstream Bitwarden clients</p> <p>While the option to use <code>Rocket</code> for TLS is there, it is not <a href=\"https://github.com/dani-garcia/vaultwarden/wiki/Enabling-HTTPS#via-rocket\">recommended</a>. Instead, use a reverse proxy to handle TLS termination.</p> <p>Using <code>HTTPS</code> is <strong>required</strong> for the most of the features to work (correctly).</p>",
+            "categories": [
+                "security"
+            ],
+            "description": "Alternative implementation of the Bitwarden server API written in Rust and compatible with upstream Bitwarden clients.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/dani-garcia/vaultwarden",
+            "location": "/__w/apps/apps/trains/community/vaultwarden",
+            "latest_version": "1.3.11",
+            "latest_app_version": "1.34.3",
+            "latest_human_version": "1.34.3_1.3.11",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "vaultwarden",
+            "recommended": false,
+            "title": "Vaultwarden",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "password",
+                "manager"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/vaultwarden/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/dani-garcia/vaultwarden"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/vaultwarden/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Vaultwarden runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "vaultwarden",
+                    "uid": 568,
+                    "user_name": "vaultwarden"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "zwave-js-ui": {
+            "app_readme": "<h1>Z-Wave JS UI</h1> <p><a href=\"https://zwave-js.github.io/zwave-js-ui\">Z-Wave JS UI</a> is a full featured Z-Wave Control Panel UI and MQTT gateway. Built using Nodejs, and Vue/Vuetify</p>",
+            "categories": [
+                "home-automation"
+            ],
+            "description": "Full featured Z-Wave Control Panel UI and MQTT gateway. Built using Nodejs, and Vue/Vuetify",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://zwave-js.github.io/zwave-js-ui",
+            "location": "/__w/apps/apps/trains/community/zwave-js-ui",
+            "latest_version": "1.0.13",
+            "latest_app_version": "11.2.1",
+            "latest_human_version": "11.2.1_1.0.13",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "zwave-js-ui",
+            "recommended": false,
+            "title": "Z-Wave JS UI",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "zwave",
+                "mqtt",
+                "bridge"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/zwave-js/zwave-js-ui"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/zwave-js-ui/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Z-Wave JS UI runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "zwave-js-ui",
+                    "uid": 568,
+                    "user_name": "zwave-js-ui"
+                }
+            ]
+        },
+        "enclosed": {
+            "app_readme": "<h1>Enclosed</h1> <p><a href=\"https://enclosed.cc/\">Enclosed</a> is a minimalistic web app designed for sending private and secure notes.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Enclosed is a minimalistic web app designed for sending private and secure notes.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://enclosed.cc/",
+            "location": "/__w/apps/apps/trains/community/enclosed",
+            "latest_version": "1.0.4",
+            "latest_app_version": "1.16.0",
+            "latest_human_version": "1.16.0_1.0.4",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "enclosed",
+            "recommended": false,
+            "title": "Enclosed",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "series"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/CorentinTh/enclosed"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/enclosed/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Enclosed runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "enclosed",
+                    "uid": 568,
+                    "user_name": "enclosed"
+                }
+            ]
+        },
+        "chatwoot": {
+            "app_readme": "<h1>Chatwoot</h1> <p><a href=\"https://www.chatwoot.com/\">Chatwoot</a> is an open-source live-chat, email support, omni-channel desk. An alternative to Intercom, Zendesk, Salesforce Service Cloud etc.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Open-source live-chat, email support, omni-channel desk. An alternative to Intercom, Zendesk, Salesforce Service Cloud etc.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.chatwoot.com/",
+            "location": "/__w/apps/apps/trains/community/chatwoot",
+            "latest_version": "1.0.12",
+            "latest_app_version": "v4.5.2",
+            "latest_human_version": "v4.5.2_1.0.12",
+            "last_update": "2025-09-05 14:25:56",
+            "name": "chatwoot",
+            "recommended": false,
+            "title": "Chatwoot",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "support",
+                "live chat"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/chatwoot/chatwoot",
+                "https://hub.docker.com/r/chatwoot/chatwoot"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/chatwoot/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Chatwoot runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                }
+            ]
+        },
+        "element-web": {
+            "app_readme": "<h1>Element-Web</h1> <p><a href=\"https://element.io/\">Element-Web</a> is a glossy Matrix collaboration client for the web.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Element-Web is a glossy Matrix collaboration client for the web.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://element.io/",
+            "location": "/__w/apps/apps/trains/community/element-web",
+            "latest_version": "1.0.5",
+            "latest_app_version": "v1.11.110",
+            "latest_human_version": "v1.11.110_1.0.5",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "element-web",
+            "recommended": false,
+            "title": "Element Web",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "chat",
+                "element"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/element-hq/element-web"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/element-web/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Element-Web runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "element-web",
+                    "uid": 568,
+                    "user_name": "element-web"
+                }
+            ]
+        },
+        "unifi-controller": {
+            "app_readme": "<h1>Unifi Controller</h1> <p><a href=\"https://github.com/goofball222/unifi\">Unifi Controller</a> is a network management controller for Unifi Equipment.</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "Unifi Controller is a network management controller for Unifi Equipment.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/goofball222/unifi",
+            "location": "/__w/apps/apps/trains/community/unifi-controller",
+            "latest_version": "1.4.10",
+            "latest_app_version": "9.4.19",
+            "latest_human_version": "9.4.19_1.4.10",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "unifi-controller",
+            "recommended": false,
+            "title": "Unifi Controller",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "controller",
+                "unifi",
+                "network"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/goofball222/unifi",
+                "https://hub.docker.com/r/goofball222/unifi"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/unifi-controller/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Unifi Controller runs as a non-root user.",
+                    "gid": 999,
+                    "group_name": "unifi",
+                    "uid": 999,
+                    "user_name": "unifi"
+                }
+            ]
+        },
+        "steam-headless": {
+            "app_readme": "<h1>Steam Headless</h1> <p><a href=\"https://github.com/Steam-Headless/docker-steam-headless\">Steam Headless</a> is a self-hosted Steam client that runs in a Docker container.</p>",
+            "categories": [
+                "games"
+            ],
+            "description": "A Headless Steam Docker image supporting NVIDIA GPU and accessible via Web UI",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/Steam-Headless/docker-steam-headless",
+            "location": "/__w/apps/apps/trains/community/steam-headless",
+            "latest_version": "1.0.17",
+            "latest_app_version": "debian",
+            "latest_human_version": "debian_1.0.17",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "steam-headless",
+            "recommended": false,
+            "title": "Steam Headless",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "games",
+                "steam"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/Steam-Headless/docker-steam-headless"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/steam-headless/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Steam Headless is able to write records to audit log",
+                    "name": "AUDIT_WRITE"
+                },
+                {
+                    "description": "Steam Headless is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Steam Headless is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Steam Headless is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Steam Headless is able to preserve set-user-ID and set-group-ID bits",
+                    "name": "FSETID"
+                },
+                {
+                    "description": "Steam Headless is able to send signals to any process",
+                    "name": "KILL"
+                },
+                {
+                    "description": "Steam Headless is able to create special files using mknod()",
+                    "name": "MKNOD"
+                },
+                {
+                    "description": "Steam Headless is able to perform network administration tasks",
+                    "name": "NET_ADMIN"
+                },
+                {
+                    "description": "Steam Headless is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Steam Headless is able to change user ID of processes",
+                    "name": "SETUID"
+                },
+                {
+                    "description": "Steam Headless is able to perform system administration operations",
+                    "name": "SYS_ADMIN"
+                },
+                {
+                    "description": "Steam Headless is able to modify process scheduling priority",
+                    "name": "SYS_NICE"
+                },
+                {
+                    "description": "Steam Headless is able to override resource limits",
+                    "name": "SYS_RESOURCE"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Steam Headless runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "audiobookshelf": {
+            "app_readme": "<h1>Audiobookshelf</h1> <p><a href=\"https://www.audiobookshelf.org/\">Audiobookshelf</a> is a self-hosted audiobook and podcast server.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Audiobookshelf is a self-hosted audiobook and podcast server",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.audiobookshelf.org/",
+            "location": "/__w/apps/apps/trains/community/audiobookshelf",
+            "latest_version": "1.4.12",
+            "latest_app_version": "2.29.0",
+            "latest_human_version": "2.29.0_1.4.12",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "audiobookshelf",
+            "recommended": false,
+            "title": "Audiobookshelf",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "audiobook"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/audiobookshelf/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/audiobookshelf/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/audiobookshelf/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/audiobookshelf/screenshots/screenshot4.jpg"
+            ],
+            "sources": [
+                "https://ghcr.io/advplyr/audiobookshelf",
+                "https://github.com/advplyr/audiobookshelf"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/audiobookshelf/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Audiobookshelf runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "audiobookshelf",
+                    "uid": 568,
+                    "user_name": "audiobookshelf"
+                }
+            ]
+        },
+        "cloudbeaver": {
+            "app_readme": "<h1>CloudBeaver</h1> <p><a href=\"https://github.com/dbeaver/cloudbeaver\">CloudBeaver</a> is a Cloud Database Manager.</p>",
+            "categories": [
+                "database"
+            ],
+            "description": "CloudBeaver is a Cloud Database Manager.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://dbeaver.com/",
+            "location": "/__w/apps/apps/trains/community/cloudbeaver",
+            "latest_version": "1.0.8",
+            "latest_app_version": "25.2.0",
+            "latest_human_version": "25.2.0_1.0.8",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "cloudbeaver",
+            "recommended": false,
+            "title": "CloudBeaver",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "cloudbeaver",
+                "database",
+                "dbeaver"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://hub.docker.com/r/dbeaver/cloudbeaver",
+                "https://github.com/dbeaver/cloudbeaver"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/cloudbeaver/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "CloudBeaver runs as non-root user.",
+                    "gid": 8978,
+                    "group_name": "cloudbeaver",
+                    "uid": 8978,
+                    "user_name": "cloudbeaver"
+                }
+            ]
+        },
+        "makemkv": {
+            "app_readme": "<h1>MakeMKV</h1> <p><a href=\"https://www.makemkv.com/\">MakeMKV</a> - MakeMKV is your one-click solution to convert video that you own into free and patents-unencumbered format that can be played everywhere</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "MakeMKV is your one-click solution to convert video that you own into free and patents-unencumbered format that can be played everywhere",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.makemkv.com/",
+            "location": "/__w/apps/apps/trains/community/makemkv",
+            "latest_version": "1.0.5",
+            "latest_app_version": "v25.08.1",
+            "latest_human_version": "v25.08.1_1.0.5",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "makemkv",
+            "recommended": false,
+            "title": "MakeMKV",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "video",
+                "ripping"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/jlesage/docker-makemkv"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/makemkv/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "MakeMKV is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "MakeMKV is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "MakeMKV is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "MakeMKV is able to send signals to any process",
+                    "name": "KILL"
+                },
+                {
+                    "description": "MakeMKV is able to bind to privileged ports (< 1024)",
+                    "name": "NET_BIND_SERVICE"
+                },
+                {
+                    "description": "MakeMKV is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "MakeMKV is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "MakeMKV runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "komga": {
+            "app_readme": "<h1>Komga</h1> <p><a href=\"https://github.com/gotson/komga\">Komga</a> is a free and open source comics/mangas server.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Komga is a free and open source comics/mangas server.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://komga.org",
+            "location": "/__w/apps/apps/trains/community/komga",
+            "latest_version": "1.3.12",
+            "latest_app_version": "1.23.3",
+            "latest_human_version": "1.23.3_1.3.12",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "komga",
+            "recommended": false,
+            "title": "Komga",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "comics",
+                "mangas"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/komga/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/gotson/komga",
+                "https://hub.docker.com/r/gotson/komga"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/komga/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Komga runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "komga",
+                    "uid": 568,
+                    "user_name": "komga"
+                }
+            ]
+        },
+        "komodo": {
+            "app_readme": "<h1>Komodo</h1> <p><a href=\"https://komo.do/\">Komodo</a> is a tool to build and deploy software across many servers.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Komodo is a tool to build and deploy software across many servers.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://komo.do",
+            "location": "/__w/apps/apps/trains/community/komodo",
+            "latest_version": "1.0.25",
+            "latest_app_version": "1.19.2",
+            "latest_human_version": "1.19.2_1.0.25",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "komodo",
+            "recommended": false,
+            "title": "Komodo",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "deployment"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/komodo/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/komodo/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/komodo/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/komodo/screenshots/screenshot4.png",
+                "https://media.sys.truenas.net/apps/komodo/screenshots/screenshot5.png",
+                "https://media.sys.truenas.net/apps/komodo/screenshots/screenshot6.png",
+                "https://media.sys.truenas.net/apps/komodo/screenshots/screenshot7.png"
+            ],
+            "sources": [
+                "https://github.com/moghtech/komodo"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/komodo/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Komodo Core runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Komodo Periphery runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "MongoDB runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "mongodb",
+                    "uid": 999,
+                    "user_name": "mongodb"
+                }
+            ]
+        },
+        "jellyfin": {
+            "app_readme": "<h1>Jellyfin</h1> <p><a href=\"https://jellyfin.org/\">Jellyfin</a> is a Free Software Media System that puts you in control of managing and streaming your media.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://jellyfin.org/",
+            "location": "/__w/apps/apps/trains/community/jellyfin",
+            "latest_version": "1.2.8",
+            "latest_app_version": "10.10.7",
+            "latest_human_version": "10.10.7_1.2.8",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "jellyfin",
+            "recommended": false,
+            "title": "Jellyfin",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "entertainment",
+                "movies",
+                "series",
+                "tv",
+                "media",
+                "streaming"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/jellyfin/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/jellyfin/screenshots/screenshot2.png"
+            ],
+            "sources": [
+                "https://hub.docker.com/r/jellyfin/jellyfin",
+                "https://jellyfin.org/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/jellyfin/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Jellyfin runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "jellyfin",
+                    "uid": 568,
+                    "user_name": "jellyfin"
+                }
+            ]
+        },
+        "logseq": {
+            "app_readme": "<h1>Logseq</h1> <p><a href=\"https://logseq.com/\">Logseq</a> is a privacy-first, open-source platform for knowledge management and collaboration.</p> <p>HTTPS is <strong>required</strong> in order to use Logseq.</p> <p>Either by using the Certificate selection or with an external reverse proxy.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Logseq is a privacy-first, open-source platform for knowledge management and collaboration.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://logseq.com",
+            "location": "/__w/apps/apps/trains/community/logseq",
+            "latest_version": "1.2.6",
+            "latest_app_version": "latest",
+            "latest_human_version": "latest_1.2.6",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "logseq",
+            "recommended": false,
+            "title": "Logseq",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "knowledge",
+                "management"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/logseq/logseq"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/logseq/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Logseq runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "logseq",
+                    "uid": 568,
+                    "user_name": "logseq"
+                }
+            ]
+        },
+        "wiki-js": {
+            "app_readme": "<h1>Wiki.js</h1> <p><a href=\"https://docs.requarks.io/\">Wiki.js</a> is a modern and powerful wiki app built on Node.js</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Wiki.js is a modern and powerful wiki app built on Node.js.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://js.wiki/",
+            "location": "/__w/apps/apps/trains/community/wiki-js",
+            "latest_version": "1.0.6",
+            "latest_app_version": "2.5.308",
+            "latest_human_version": "2.5.308_1.0.6",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "wiki-js",
+            "recommended": false,
+            "title": "Wiki.js",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "wiki"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/requarks/wiki",
+                "https://docs.requarks.io/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/wiki-js/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Wiki.js runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "wiki-js",
+                    "uid": 1000,
+                    "user_name": "wiki-js"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "it-tools": {
+            "app_readme": "<h1>IT Tools</h1> <p><a href=\"https://it-tools.tech\">IT Tools</a> is a collection of handy online tools for developers, with great UX.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Collection of handy online tools for developers, with great UX.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://it-tools.tech",
+            "location": "/__w/apps/apps/trains/community/it-tools",
+            "latest_version": "1.0.13",
+            "latest_app_version": "2024.10.22-7ca5933",
+            "latest_human_version": "2024.10.22-7ca5933_1.0.13",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "it-tools",
+            "recommended": false,
+            "title": "IT Tools",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "tools"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/CorentinTh/it-tools/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/it-tools/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "IT Tools is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "IT Tools is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "IT Tools is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "IT Tools is able to bind to privileged ports (< 1024)",
+                    "name": "NET_BIND_SERVICE"
+                },
+                {
+                    "description": "IT Tools is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "IT Tools is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "IT Tools runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "roundcube": {
+            "app_readme": "<h1>Roundcube</h1> <p><a href=\"https://roundcube.net/\">Roundcube</a> is a browser-based multilingual IMAP client with an application-like user interface.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Roundcube is a browser-based multilingual IMAP client with an application-like user interface.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://roundcube.net/",
+            "location": "/__w/apps/apps/trains/community/roundcube",
+            "latest_version": "1.3.9",
+            "latest_app_version": "1.6.11-apache",
+            "latest_human_version": "1.6.11-apache_1.3.9",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "roundcube",
+            "recommended": false,
+            "title": "Roundcube",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "webmail",
+                "email"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/roundcube/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/roundcube/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/roundcube/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/roundcube/screenshots/screenshot4.png",
+                "https://media.sys.truenas.net/apps/roundcube/screenshots/screenshot5.png"
+            ],
+            "sources": [
+                "https://roundcube.net/",
+                "https://hub.docker.com/r/roundcube/roundcubemail/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/roundcube/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Roundcube is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Roundcube is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Roundcube is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Roundcube is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Roundcube is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Roundcube runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "satisfactory-server": {
+            "app_readme": "<h1>Satisfactory Server</h1> <p><a href=\"https://github.com/wolveix/satisfactory-server\">Satisfactory Server</a> is a Dockerized version of the Satisfactory dedicated server.</p>",
+            "categories": [
+                "games"
+            ],
+            "description": "A Dockerized version of the Satisfactory dedicated server",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/wolveix/satisfactory-server",
+            "location": "/__w/apps/apps/trains/community/satisfactory-server",
+            "latest_version": "1.0.20",
+            "latest_app_version": "v1.9.9",
+            "latest_human_version": "v1.9.9_1.0.20",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "satisfactory-server",
+            "recommended": false,
+            "title": "Satisfactory Server",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "games",
+                "server",
+                "satisfactory"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/wolveix/satisfactory-server"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/satisfactory-server/icons/icon.webp",
+            "capabilities": [
+                {
+                    "description": "Satisfactory is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Satisfactory is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Satisfactory is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Satisfactory is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Satisfactory is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Satisfactory Server runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "hoppscotch": {
+            "app_readme": "<h1>Hoppscotch</h1> <p><a href=\"https://hoppscotch.io\">Hoppscotch</a> is a lightweight, web-based API development suite.</p>",
+            "categories": [
+                "development"
+            ],
+            "description": "Hoppscotch is a lightweight, web-based API development suite.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://hoppscotch.io/",
+            "location": "/__w/apps/apps/trains/community/hoppscotch",
+            "latest_version": "1.0.6",
+            "latest_app_version": "2025.8.1",
+            "latest_human_version": "2025.8.1_1.0.6",
+            "last_update": "2025-09-03 12:56:43",
+            "name": "hoppscotch",
+            "recommended": false,
+            "title": "Hoppscotch",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "api",
+                "development",
+                "postman"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://hoppscotch.com",
+                "https://hoppscotch.io",
+                "https://github.com/hoppscotch/hoppscotch",
+                "https://hub.docker.com/r/hoppscotch/hoppscotch-frontend",
+                "https://hub.docker.com/r/hoppscotch/hoppscotch-backend",
+                "https://hub.docker.com/r/hoppscotch/hoppscotch-admin"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/hoppscotch/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Hoppscotch Frontend runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Hoppscotch Backend runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Hoppscotch Admin runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Postgres runs as a non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "profilarr": {
+            "app_readme": "<h1>Profilarr</h1> <p><a href=\"https://github.com/Dictionarry-Hub/profilarr\">Profilarr</a> is a Configuration Management Platform for Radarr/Sonarr.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Profilarr is a Configuration Management Platform for Radarr/Sonarr",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://dictionarry.dev/",
+            "location": "/__w/apps/apps/trains/community/profilarr",
+            "latest_version": "1.0.5",
+            "latest_app_version": "v1.1.3",
+            "latest_human_version": "v1.1.3_1.0.5",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "profilarr",
+            "recommended": false,
+            "title": "Profilarr",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "management"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/Dictionarry-Hub/profilarr",
+                "https://hub.docker.com/r/santiagosayshey/profilarr"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/profilarr/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Profilarr is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Profilarr is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Profilarr is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Profilarr is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Profilarr is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Profilarr runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "node-red": {
+            "app_readme": "<h1>Node-RED</h1> <p><a href=\"https://nodered.org\">Node-RED</a> is a programming tool for wiring together hardware devices, APIs and online services in new and interesting ways.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Node-RED is a programming tool for wiring together hardware devices, APIs and online services in new and interesting ways.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://nodered.org",
+            "location": "/__w/apps/apps/trains/community/node-red",
+            "latest_version": "1.2.8",
+            "latest_app_version": "4.1.0",
+            "latest_human_version": "4.1.0_1.2.8",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "node-red",
+            "recommended": false,
+            "title": "Node-RED",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "automation"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/node-red/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://nodered.org",
+                "https://github.com/node-red/node-red-docker"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/node-red/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Node-RED runs as a non-root user.",
+                    "gid": 1000,
+                    "group_name": "node-red",
+                    "uid": 1000,
+                    "user_name": "node-red"
+                }
+            ]
+        },
+        "nocodb": {
+            "app_readme": "<h1>NocoDB</h1> <p><a href=\"https://nocodb.com/\">NocoDB</a> is an open source NoCode platform that turns any database into a smart spreadsheet.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "NocoDB is an open source NoCode platform that turns any database into a smart spreadsheet.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://nocodb.com/",
+            "location": "/__w/apps/apps/trains/community/nocodb",
+            "latest_version": "1.0.23",
+            "latest_app_version": "0.264.6",
+            "latest_human_version": "0.264.6_1.0.23",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "nocodb",
+            "recommended": false,
+            "title": "NocoDB",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "database",
+                "nocode",
+                "spreadsheet",
+                "api"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://hub.docker.com/r/nocodb/nocodb",
+                "https://github.com/nocodb/nocodb",
+                "https://nocodb.com/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/nocodb/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "NocoDB runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "nocodb",
+                    "uid": 568,
+                    "user_name": "nocodb"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                }
+            ]
+        },
+        "wyze-bridge": {
+            "app_readme": "<h1>Wyze-Bridge</h1> <p><a href=\"https://github.com/mrlt8/docker-wyze-bridge\">Wyze-Bridge</a> Create a local WebRTC, RTSP, RTMP, or HLS/Low-Latency HLS stream for most of your Wyze cameras</p>",
+            "categories": [
+                "security"
+            ],
+            "description": "Create a local WebRTC, RTSP, RTMP, or HLS/Low-Latency HLS stream for most of your Wyze cameras",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/mrlt8/docker-wyze-bridge",
+            "location": "/__w/apps/apps/trains/community/wyze-bridge",
+            "latest_version": "1.0.12",
+            "latest_app_version": "2.10.3",
+            "latest_human_version": "2.10.3_1.0.12",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "wyze-bridge",
+            "recommended": false,
+            "title": "Wyze Bridge",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "camera"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/wyze-bridge/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/mrlt8/docker-wyze-bridge"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/wyze-bridge/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Wyze Bridge is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Wyze Bridge is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Wyze Bridge is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Wyze Bridge is able to send signals to any process",
+                    "name": "KILL"
+                },
+                {
+                    "description": "Wyze Bridge is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Wyze Bridge is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Wyze Bridge runs as the root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "homarr": {
+            "app_readme": "<h1>Homarr</h1> <p><a href=\"https://github.com/ajnart/homarr\">Homarr</a> is a sleek, modern dashboard that puts all of your apps and services at your fingertips.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Homarr a modern and easy to use dashboard. 14+ integrations. 10K+ icons built in. Authentication out of the box. No YAML, drag and drop configuration.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://homarr.dev/",
+            "location": "/__w/apps/apps/trains/community/homarr",
+            "latest_version": "2.1.18",
+            "latest_app_version": "v1.35.1",
+            "latest_human_version": "v1.35.1_2.1.18",
+            "last_update": "2025-09-04 12:07:56",
+            "name": "homarr",
+            "recommended": false,
+            "title": "Homarr",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "dashboard"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/homarr/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/homarr/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/homarr/screenshots/screenshot3.png"
+            ],
+            "sources": [
+                "https://homarr.dev/",
+                "https://github.com/homarr-labs/homarr"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/homarr/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Homarr is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Homarr is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Homarr is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Homarr is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Homarr is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Homarr runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "ollama": {
+            "app_readme": "<h1>Ollama</h1> <p><a href=\"https://github.com/ollama/ollama\">Ollama</a> - Get up and running with large language models.</p> <p>Get up and running with Llama 3.2, Mistral, Gemma 2, and other large language models.</p>",
+            "categories": [
+                "ai"
+            ],
+            "description": "Get up and running with Llama 3.2, Mistral, Gemma 2, and other large language models.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/ollama/ollama",
+            "location": "/__w/apps/apps/trains/community/ollama",
+            "latest_version": "1.1.21",
+            "latest_app_version": "0.11.10",
+            "latest_human_version": "0.11.10_1.1.21",
+            "last_update": "2025-09-05 14:25:56",
+            "name": "ollama",
+            "recommended": false,
+            "title": "Ollama",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "ai",
+                "llm"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/ollama/ollama"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/ollama/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Ollama runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "teamspeak": {
+            "app_readme": "<h1>Teamspeak</h1> <p><a href=\"https://teamspeak.com\">Teamspeak</a> is software for quality voice communication via the Internet.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "TeamSpeak is software for quality voice communication via the Internet.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://teamspeak.com",
+            "location": "/__w/apps/apps/trains/community/teamspeak",
+            "latest_version": "1.0.5",
+            "latest_app_version": "3.13.7",
+            "latest_human_version": "3.13.7_1.0.5",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "teamspeak",
+            "recommended": false,
+            "title": "Teamspeak",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "voice",
+                "audio"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://hub.docker.com/_/teamspeak"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/teamspeak/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Teamspeak runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "teamspeak",
+                    "uid": 568,
+                    "user_name": "teamspeak"
+                }
+            ]
+        },
+        "headscale": {
+            "app_readme": "<h1>Headscale</h1> <p><a href=\"https://github.com/juanfont/headscale\">Headscale</a> is an open source, self-hosted implementation of the Tailscale control server.</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "An open source, self-hosted implementation of the Tailscale control server",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/juanfont/headscale/releases",
+            "location": "/__w/apps/apps/trains/community/headscale",
+            "latest_version": "1.0.5",
+            "latest_app_version": "v0.26.1",
+            "latest_human_version": "v0.26.1_1.0.5",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "headscale",
+            "recommended": false,
+            "title": "Headscale",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "networking",
+                "tailscale",
+                "vpn"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/juanfont/headscale"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/headscale/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Headscale runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "headscale",
+                    "uid": 568,
+                    "user_name": "headscale"
+                }
+            ]
+        },
+        "mineos": {
+            "app_readme": "<h1>MineOS</h1> <p><a href=\"https://github.com/hexparrot/mineos-node\">MineOS</a> is a server front-end to ease managing Minecraft administrative tasks.</p>",
+            "categories": [
+                "games"
+            ],
+            "description": "MineOS is a server front-end to ease managing Minecraft administrative tasks.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/hexparrot/mineos-node",
+            "location": "/__w/apps/apps/trains/community/mineos",
+            "latest_version": "1.2.6",
+            "latest_app_version": "latest",
+            "latest_human_version": "latest_1.2.6",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "mineos",
+            "recommended": false,
+            "title": "MineOS",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "minecraft"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/mineos/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://hub.docker.com/r/hexparrot/mineos/",
+                "https://github.com/hexparrot/mineos-node"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/mineos/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "MineOS is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "MineOS is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "MineOS is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "MineOS is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "MineOS is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "MineOS runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "plex-auto-languages": {
+            "app_readme": "<h1>Plex Auto Languages</h1> <p><a href=\"https://github.com/RemiRigal/Plex-Auto-Languages\">Plex Auto Languages</a> offer automated language selection for Plex TV Shows</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Plex Auto Languages offer automated language selection for Plex TV Shows",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/JourneyDocker/Plex-Auto-Languages",
+            "location": "/__w/apps/apps/trains/community/plex-auto-languages",
+            "latest_version": "1.3.6",
+            "latest_app_version": "1.3.10",
+            "latest_human_version": "1.3.10_1.3.6",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "plex-auto-languages",
+            "recommended": false,
+            "title": "Plex Auto Languages",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "plex",
+                "languages"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/JourneyDocker/Plex-Auto-Languages"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/plex-auto-languages/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Plex Auto Languages runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "plex-auto-languages",
+                    "uid": 568,
+                    "user_name": "plex-auto-languages"
+                }
+            ]
+        },
+        "invidious": {
+            "app_readme": "<h1>Invidious</h1> <p><a href=\"https://invidious.io/\">Invidious</a> is an alternative front-end to YouTube.</p> <p>Additional configuration can be specified</p> <ul> <li>Via <a href=\"https://github.com/iv-org/invidious/pull/1702\">environment variables</a></li> <li>By editing the file <code>/config/config.yaml</code> (see <a href=\"https://github.com/iv-org/invidious/blob/master/config/config.example.yml\">example</a>)</li> </ul>",
+            "categories": [
+                "media"
+            ],
+            "description": "Invidious is an alternative front-end to YouTube",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://invidious.io/",
+            "location": "/__w/apps/apps/trains/community/invidious",
+            "latest_version": "1.3.12",
+            "latest_app_version": "2.20250517.0",
+            "latest_human_version": "2.20250517.0_1.3.12",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "invidious",
+            "recommended": false,
+            "title": "Invidious",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "youtube"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/invidious/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/invidious/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/invidious/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/invidious/screenshots/screenshot4.png",
+                "https://media.sys.truenas.net/apps/invidious/screenshots/screenshot5.png",
+                "https://media.sys.truenas.net/apps/invidious/screenshots/screenshot6.png"
+            ],
+            "sources": [
+                "https://invidious.io/",
+                "https://quay.io/repository/invidious"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/invidious/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Invidious runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "invidious",
+                    "uid": 1000,
+                    "user_name": "invidious"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "code-server": {
+            "app_readme": "<h1>Code Server</h1> <p><a href=\"https://coder.com\">Code Server</a> is VS Code in the browser</p>",
+            "categories": [
+                "development"
+            ],
+            "description": "Code Server is VS Code in the browser",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://coder.com",
+            "location": "/__w/apps/apps/trains/community/code-server",
+            "latest_version": "1.0.23",
+            "latest_app_version": "4.103.2",
+            "latest_human_version": "4.103.2_1.0.23",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "code-server",
+            "recommended": false,
+            "title": "Code Server",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "code",
+                "editor"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/coder/code-server"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/code-server/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Code Server is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Code Server is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Code Server runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "code-server",
+                    "uid": 568,
+                    "user_name": "code-server"
+                }
+            ]
+        },
+        "unmanic": {
+            "app_readme": "<h1>Unmanic</h1> <p><a href=\"https://unmanic.app/\">Unmanic</a> is a library optimizer.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Unmanic is a library optimizer",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/Unmanic/unmanic",
+            "location": "/__w/apps/apps/trains/community/unmanic",
+            "latest_version": "1.0.13",
+            "latest_app_version": "0.3.0",
+            "latest_human_version": "0.3.0_1.0.13",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "unmanic",
+            "recommended": false,
+            "title": "Unmanic",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "optimize",
+                "media"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/unmanic/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/Unmanic/unmanic"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/unmanic/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Unmanic is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Unmanic is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Unmanic is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Unmanic is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Unmanic is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Unmanic runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "affine": {
+            "app_readme": "<h1>AFFiNE</h1> <p><a href=\"https://affine.pro/\">AFFiNE</a> is a next-gen knowledge base that brings planning, sorting and creating all together. Privacy first, open-source, customizable and ready to use.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "AFFiNE is a next-gen knowledge base that brings planning, sorting and creating all together. Privacy first, open-source, customizable and ready to use.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://affine.pro",
+            "location": "/__w/apps/apps/trains/community/affine",
+            "latest_version": "1.0.18",
+            "latest_app_version": "0.24.1",
+            "latest_human_version": "0.24.1_1.0.18",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "affine",
+            "recommended": false,
+            "title": "AFFiNE",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "planning",
+                "knowledge base"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/toeverything/AFFiNE"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/affine/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "AFFiNE runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                }
+            ]
+        },
+        "rdt-client": {
+            "app_readme": "<h1>RDT Client</h1> <p><a href=\"https://github.com/rogerfar/rdt-client\">RDT Client</a> is a Real-Debrid Client Proxy</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Real-Debrid Client Proxy",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/rogerfar/rdt-client",
+            "location": "/__w/apps/apps/trains/community/rdt-client",
+            "latest_version": "1.0.2",
+            "latest_app_version": "2.0.116",
+            "latest_human_version": "2.0.116_1.0.2",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "rdt-client",
+            "recommended": false,
+            "title": "RDT Client",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "ebooks"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/rogerfar/rdt-client"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/rdt-client/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "RDT Client is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "RDT Client is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "RDT Client is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "RDT Client is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "RDT Client is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "RDT Client runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "flood": {
+            "app_readme": "<h1>Flood</h1> <p><a href=\"https://flood.js.org\">Flood</a> is a modern web UI for various torrent clients with a Node.js backend and React frontend.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "A modern web UI for various torrent clients with a Node.js backend and React frontend.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://flood.js.org",
+            "location": "/__w/apps/apps/trains/community/flood",
+            "latest_version": "1.1.7",
+            "latest_app_version": "4.9.5",
+            "latest_human_version": "4.9.5_1.1.7",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "flood",
+            "recommended": false,
+            "title": "Flood",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "torrent"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/flood/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/jesec/flood",
+                "https://hub.docker.com/r/jesec/flood"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/flood/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Flood runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "flood",
+                    "uid": 568,
+                    "user_name": "flood"
+                }
+            ]
+        },
+        "baserow": {
+            "app_readme": "<h1>Baserow</h1> <p><a href=\"https://baserow.io\">Baserow</a> is an open source no-code database and Airtable alternative. Create your own database without technical experience. Our user friendly no-code tool gives you the powers of a developer without leaving your browser.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Baserow is an open source no-code database and Airtable alternative. Create your own database without technical experience. Our user friendly no-code tool gives you the powers of a developer without leaving your browser.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://baserow.io",
+            "location": "/__w/apps/apps/trains/community/baserow",
+            "latest_version": "1.0.24",
+            "latest_app_version": "1.35.1",
+            "latest_human_version": "1.35.1_1.0.24",
+            "last_update": "2025-09-03 12:56:43",
+            "name": "baserow",
+            "recommended": false,
+            "title": "Baserow",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "database",
+                "airtable",
+                "webui",
+                "no-code"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://gitlab.com/baserow/baserow"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/baserow/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Baserow is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Baserow is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Baserow is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Baserow is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Baserow is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Baserow runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Redis runs as a any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                }
+            ]
+        },
+        "sonarr": {
+            "app_readme": "<h1>Sonarr</h1> <p><a href=\"https://github.com/Sonarr/Sonarr\">Sonarr</a> is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new episodes of your favorite shows and will grab, sort and rename them.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Sonarr is a PVR for Usenet and BitTorrent users.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/Sonarr/Sonarr",
+            "location": "/__w/apps/apps/trains/community/sonarr",
+            "latest_version": "1.2.7",
+            "latest_app_version": "4.0.15.2940",
+            "latest_human_version": "4.0.15.2940_1.2.7",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "sonarr",
+            "recommended": false,
+            "title": "Sonarr",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "series"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/sonarr/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/sonarr/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/sonarr/screenshots/screenshot3.png"
+            ],
+            "sources": [
+                "https://github.com/home-operations/containers/tree/main/apps/sonarr",
+                "https://github.com/Sonarr/Sonarr"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/sonarr/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Sonarr runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "sonarr",
+                    "uid": 568,
+                    "user_name": "sonarr"
+                }
+            ]
+        },
+        "lancache-monolithic": {
+            "app_readme": "<h1>LanCache Monolithic</h1> <p><a href=\"https://github.com/lancachenet/monolithic\">LanCache Monolithic</a> is a monolithic lancache service capable of caching all CDNs in a single instance</p>",
+            "categories": [
+                "games"
+            ],
+            "description": "LanCache Monolithic is a monolithic lancache service capable of caching all CDNs in a single instance",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://lancache.net/",
+            "location": "/__w/apps/apps/trains/community/lancache-monolithic",
+            "latest_version": "1.0.7",
+            "latest_app_version": "latest",
+            "latest_human_version": "latest_1.0.7",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "lancache-monolithic",
+            "recommended": false,
+            "title": "LanCache Monolithic",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "lancache",
+                "games"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://hub.docker.com/r/lancachenet/monolithic",
+                "https://lancache.net/docs/containers/monolithic/",
+                "https://github.com/lancachenet/monolithic"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/lancache-monolithic/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "LanCache Monolithic is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "LanCache Monolithic is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "LanCache Monolithic is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "LanCache Monolithic runs as any non-root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "recyclarr": {
+            "app_readme": "<h1>Recyclarr</h1> <p><a href=\"https://github.com/recyclarr/recyclarr\">Recyclarr</a> synchronizes recommended settings from the TRaSH guides to your Sonarr/Radarr instances.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Recyclarr synchronizes recommended settings from the TRaSH guides to your Sonarr/Radarr instances.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://recyclarr.dev",
+            "location": "/__w/apps/apps/trains/community/recyclarr",
+            "latest_version": "1.2.5",
+            "latest_app_version": "7.4.1",
+            "latest_human_version": "7.4.1_1.2.5",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "recyclarr",
+            "recommended": false,
+            "title": "Recyclarr",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "sync",
+                "sonarr",
+                "radarr"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://recyclarr.dev",
+                "https://github.com/recyclarr/recyclarr/tree/recyclarr"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/recyclarr/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Recyclarr runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "recyclarr",
+                    "uid": 568,
+                    "user_name": "recyclarr"
+                }
+            ]
+        },
+        "automatic-ripping-machine": {
+            "app_readme": "<h1>Automatic Ripping Machine</h1> <p><a href=\"https://github.com/automatic-ripping-machine/automatic-ripping-machine\">Automatic Ripping Machine</a> Insert an optical disc (Blu-ray, DVD, CD) and checks to see if it's audio, video (Movie or TV), or data, then rips it.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Insert an optical disc (Blu-ray, DVD, CD) and checks to see if it's audio, video (Movie or TV), or data, then rips it.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/automatic-ripping-machine/automatic-ripping-machine",
+            "location": "/__w/apps/apps/trains/community/automatic-ripping-machine",
+            "latest_version": "1.0.25",
+            "latest_app_version": "2.18.4",
+            "latest_human_version": "2.18.4_1.0.25",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "automatic-ripping-machine",
+            "recommended": false,
+            "title": "Automatic Ripping Machine",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "video",
+                "ripping"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/automatic-ripping-machine/automatic-ripping-machine",
+                "https://hub.docker.com/r/automaticrippingmachine/automatic-ripping-machine"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/automatic-ripping-machine/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Automatic Ripping Machine is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Automatic Ripping Machine is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Automatic Ripping Machine is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Automatic Ripping Machine is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Automatic Ripping Machine is able to change user ID of processes",
+                    "name": "SETUID"
+                },
+                {
+                    "description": "Automatic Ripping Machine is able to perform system administration operations",
+                    "name": "SYS_ADMIN"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Automatic Ripping Machine runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "clamav": {
+            "app_readme": "<h1>ClamAV</h1> <p><a href=\"https://www.clamav.net/\">ClamAV</a> - ClamAV\u00ae is an open-source antivirus engine for detecting trojans, viruses, malware &amp; other malicious threats.</p>",
+            "categories": [
+                "security"
+            ],
+            "description": "ClamAV is an open source (GPLv2) anti-virus toolkit.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.clamav.net/",
+            "location": "/__w/apps/apps/trains/community/clamav",
+            "latest_version": "1.3.5",
+            "latest_app_version": "1.1.2-2",
+            "latest_human_version": "1.1.2-2_1.3.5",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "clamav",
+            "recommended": false,
+            "title": "ClamAV",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "anti-virus",
+                "clamav"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://docs.clamav.net/",
+                "https://www.clamav.net/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/clamav/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "ClamAV is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "ClamAV is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "ClamAV is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "ClamAV is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "ClamAV is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "ClamAV runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "versitygw": {
+            "app_readme": "<h1>Versity S3 Gateway</h1> <p>The <a href=\"https://github.com/versity/versitygw\">Versity S3 Gateway</a> is a simple to use tool for seamless inline translation between AWS S3 object commands and storage systems.</p>",
+            "categories": [
+                "storage"
+            ],
+            "description": "The Versity Gateway serves as a bridge between file based, POSIX, storage systems and applications that rely on S3 object interfaces. It enables applications to interact with file storage using familiar S3 operations like put and get, allowing for easy integration and compatibility.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/versity/versitygw",
+            "location": "/__w/apps/apps/trains/community/versitygw",
+            "latest_version": "1.0.9",
+            "latest_app_version": "v1.0.17",
+            "latest_human_version": "v1.0.17_1.0.9",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "versitygw",
+            "recommended": false,
+            "title": "Versity Gateway",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "storage",
+                "object-storage",
+                "S3",
+                "versity"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/versity/versitygw"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/versitygw/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Versity Gateway runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "versity",
+                    "uid": 568,
+                    "user_name": "versity"
+                }
+            ]
+        },
+        "overseerr": {
+            "app_readme": "<h1>Overseerr</h1> <p><a href=\"https://github.com/sct/overseerr\">Overseerr</a> is a free and open source software application for managing requests for your media library. It integrates with your existing services, such as Sonarr, Radarr, and Plex!</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Overseerr is a free and open source software application for managing requests for your media library.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/sct/overseerr",
+            "location": "/__w/apps/apps/trains/community/overseerr",
+            "latest_version": "1.2.6",
+            "latest_app_version": "1.34.0",
+            "latest_human_version": "1.34.0_1.2.6",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "overseerr",
+            "recommended": false,
+            "title": "Overseerr",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://hub.docker.com/r/sctx/overseerr",
+                "https://github.com/sct/overseerr"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/overseerr/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Overseerr runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "overseerr",
+                    "uid": 568,
+                    "user_name": "overseerr"
+                }
+            ]
+        },
+        "searxng": {
+            "app_readme": "<h1>SearXNG</h1> <p><a href=\"https://github.com/searxng/searxng\">SearXNG</a> is a privacy-respecting, hackable metasearch engine</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "SearXNG is a privacy-respecting, hackable metasearch engine",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/searxng/searxng",
+            "location": "/__w/apps/apps/trains/community/searxng",
+            "latest_version": "1.2.50",
+            "latest_app_version": "2025.9.5-e7501ea",
+            "latest_human_version": "2025.9.5-e7501ea_1.2.50",
+            "last_update": "2025-09-05 14:25:56",
+            "name": "searxng",
+            "recommended": false,
+            "title": "SearXNG",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "search"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://hub.docker.com/r/searxng/searxng",
+                "https://github.com/searxng/searxng"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/searxng/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "SearXNG is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "SearXNG is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "SearXNG runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                }
+            ]
+        },
+        "lidarr": {
+            "app_readme": "<h1>Lidarr</h1> <p><a href=\"https://github.com/Lidarr/Lidarr\">Lidarr</a> is a music collection manager for Usenet and BitTorrent users.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Lidarr is a music collection manager for Usenet and BitTorrent users.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/Lidarr/Lidarr",
+            "location": "/__w/apps/apps/trains/community/lidarr",
+            "latest_version": "1.3.14",
+            "latest_app_version": "2.14.1.4714",
+            "latest_human_version": "2.14.1.4714_1.3.14",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "lidarr",
+            "recommended": false,
+            "title": "Lidarr",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "music"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/lidarr/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/lidarr/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/lidarr/screenshots/screenshot3.png"
+            ],
+            "sources": [
+                "https://github.com/Lidarr/Lidarr",
+                "https://github.com/home-operations/containers/tree/main/apps/lidarr"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/lidarr/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Lidarr runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "lidarr",
+                    "uid": 568,
+                    "user_name": "lidarr"
+                }
+            ]
+        },
+        "nzbget": {
+            "app_readme": "<h1>NZBGet</h1> <p><a href=\"https://github.com/nzbgetcom/nzbget\">NZBGet</a> is a binary downloader, which downloads files from Usenet based-on information given in nzb files.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "NZBGet is a binary downloader, which downloads files from Usenet based-on information given in nzb files.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://nzbget.com/",
+            "location": "/__w/apps/apps/trains/community/nzbget",
+            "latest_version": "1.0.13",
+            "latest_app_version": "v25.3",
+            "latest_human_version": "v25.3_1.0.13",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "nzbget",
+            "recommended": false,
+            "title": "NZBGet",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "usenet",
+                "newsreader"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/nzbget/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/nzbget/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/nzbget/screenshots/screenshot3.png"
+            ],
+            "sources": [
+                "https://github.com/nzbgetcom/nzbget"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/nzbget/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "NZBGet is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "NZBGet is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "NZBGet is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "NZBGet is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "NZBGet runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "restic-rest-server": {
+            "app_readme": "<h1>Restic REST Server</h1> <p><a href=\"https://github.com/restic/rest-server\">Restic REST Server</a> Rest Server is a high performance HTTP server that implements restic's REST backend API.</p>",
+            "categories": [
+                "backup"
+            ],
+            "description": "Rest Server is a high performance HTTP server that implements restic's REST backend API.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/restic/rest-server",
+            "location": "/__w/apps/apps/trains/community/restic-rest-server",
+            "latest_version": "1.0.2",
+            "latest_app_version": "0.14.0",
+            "latest_human_version": "0.14.0_1.0.2",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "restic-rest-server",
+            "recommended": false,
+            "title": "Restic REST Server",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "backup",
+                "restic"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/restic/rest-server",
+                "https://hub.docker.com/r/restic/rest-server"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/restic-rest-server/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Restic rest-server runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "rest-server",
+                    "uid": 568,
+                    "user_name": "rest-server"
+                }
+            ]
+        },
+        "fscrawler": {
+            "app_readme": "<h1>FSCrawler</h1> <p><a href=\"https://fscrawler.readthedocs.io/\">FSCrawler</a> is a crawler that helps to index binary documents such as PDF, Open Office, MS Office.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "FSCrawler is a crawler that helps to index binary documents such as PDF, Open Office, MS Office.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://fscrawler.readthedocs.io",
+            "location": "/__w/apps/apps/trains/community/fscrawler",
+            "latest_version": "1.2.5",
+            "latest_app_version": "2.10-SNAPSHOT-ocr-es7",
+            "latest_human_version": "2.10-SNAPSHOT-ocr-es7_1.2.5",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "fscrawler",
+            "recommended": false,
+            "title": "FSCrawler",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "index",
+                "crawler"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/dadoonet/fscrawler",
+                "https://hub.docker.com/r/dadoonet/fscrawler",
+                "https://fscrawler.readthedocs.io/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/fscrawler/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "FSCrawler runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "actual-budget": {
+            "app_readme": "<h1>Actual Budget</h1> <p><a href=\"https://actualbudget.org/\">Actual Budget</a> is a super fast and privacy-focused app for managing your finances.</p>",
+            "categories": [
+                "financial"
+            ],
+            "description": "Actual Budget is a super fast and privacy-focused app for managing your finances.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://actualbudget.org",
+            "location": "/__w/apps/apps/trains/community/actual-budget",
+            "latest_version": "1.3.12",
+            "latest_app_version": "25.9.0",
+            "latest_human_version": "25.9.0_1.3.12",
+            "last_update": "2025-09-04 12:07:56",
+            "name": "actual-budget",
+            "recommended": false,
+            "title": "Actual Budget",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "finance",
+                "budget"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/actual-budget/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/actual-budget/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/actual-budget/screenshots/screenshot3.png"
+            ],
+            "sources": [
+                "https://github.com/actualbudget/actual-server",
+                "https://hub.docker.com/r/actualbudget/actual-server"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/actual-budget/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Actual Budget runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "actual",
+                    "uid": 568,
+                    "user_name": "actual"
+                }
+            ]
+        },
+        "warracker": {
+            "app_readme": "<h1>Warracker</h1> <p><a href=\"https://github.com/sassanix/Warracker\">Warracker</a> is an open-source web application to manage product warranties, track expiration dates, and store related documents.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Warracker is an open-source web application to manage product warranties, track expiration dates, and store related documents.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/sassanix/Warracker",
+            "location": "/__w/apps/apps/trains/community/warracker",
+            "latest_version": "1.0.25",
+            "latest_app_version": "0.10.1.10",
+            "latest_human_version": "0.10.1.10_1.0.25",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "warracker",
+            "recommended": false,
+            "title": "Warracker",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "warranty",
+                "expiration"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/warracker/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/sassanix/Warracker"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/warracker/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Warracker is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Warracker is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Warracker is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Warracker is able to bind to privileged ports (< 1024)",
+                    "name": "NET_BIND_SERVICE"
+                },
+                {
+                    "description": "Warracker is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Warracker is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Warracker runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "warracker",
+                    "uid": 568,
+                    "user_name": "warracker"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "spottarr": {
+            "app_readme": "<h1>Spottarr</h1> <p><a href=\"https://github.com/Spottarr/Spottarr\">Spottarr</a> is a small application that can index the spotnet messages (spots) and exposes them as a newznab indexer.</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "Spottarr is a small application that can index the spotnet messages (spots) and exposes them as a newznab indexer.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/Spottarr/Spottarr",
+            "location": "/__w/apps/apps/trains/community/spottarr",
+            "latest_version": "1.0.20",
+            "latest_app_version": "1.9.0",
+            "latest_human_version": "1.9.0_1.0.20",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "spottarr",
+            "recommended": false,
+            "title": "Spottarr",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "newsserver",
+                "download",
+                "newznab"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/Spottarr/Spottarr"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/spottarr/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Spottarr runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "spottarr",
+                    "uid": 568,
+                    "user_name": "spottarr"
+                }
+            ]
+        },
+        "gitea-act-runner": {
+            "app_readme": "<h1>Gitea Act Runner</h1> <p><a href=\"https://gitea.com/gitea/act_runner\">Gitea Act Runner</a> is a runner for Gitea based on <a href=\"https://github.com/nektos/act\">act</a>.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "A runner for Gitea based on act.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://gitea.com",
+            "location": "/__w/apps/apps/trains/community/gitea-act-runner",
+            "latest_version": "1.0.16",
+            "latest_app_version": "0.2.13",
+            "latest_human_version": "0.2.13_1.0.16",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "gitea-act-runner",
+            "recommended": false,
+            "title": "Gitea Act Runner",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "gitea",
+                "actions",
+                "runner"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://docs.gitea.com/usage/actions/act-runner",
+                "https://hub.docker.com/r/gitea/act_runner",
+                "https://gitea.com/gitea/act_runner"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/gitea/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Gitea Act Runner runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "gitea-act-runner",
+                    "uid": 568,
+                    "user_name": "gitea-act-runner"
+                }
+            ]
+        },
+        "uptime-kuma": {
+            "app_readme": "<h1>Uptime Kuma</h1> <p><a href=\"https://github.com/louislam/uptime-kuma\">Uptime Kuma</a> - A fancy self-hosted monitoring tool</p> <p>Uptime Kuma is an easy-to-use self-hosted monitoring tool.</p>",
+            "categories": [
+                "monitoring"
+            ],
+            "description": "A fancy self-hosted monitoring tool",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/louislam/uptime-kuma",
+            "location": "/__w/apps/apps/trains/community/uptime-kuma",
+            "latest_version": "1.1.9",
+            "latest_app_version": "2.0.0-beta.4",
+            "latest_human_version": "2.0.0-beta.4_1.1.9",
+            "last_update": "2025-09-05 14:25:56",
+            "name": "uptime-kuma",
+            "recommended": false,
+            "title": "Uptime Kuma",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "uptime",
+                "monitor"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/uptime-kuma/screenshots/screenshot1.jpg",
+                "https://media.sys.truenas.net/apps/uptime-kuma/screenshots/screenshot2.jpg"
+            ],
+            "sources": [
+                "https://github.com/louislam/uptime-kuma"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/uptime-kuma/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Uptime Kuma is able to use raw and packet sockets",
+                    "name": "NET_RAW"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Uptime Kuma runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "traccar": {
+            "app_readme": "<h1>Traccar</h1> <p><a href=\"https://www.traccar.org/\">Traccar</a> GPS Tracking System</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Traccar GPS Tracking System",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.traccar.org/",
+            "location": "/__w/apps/apps/trains/community/traccar",
+            "latest_version": "1.0.10",
+            "latest_app_version": "6.9.1",
+            "latest_human_version": "6.9.1_1.0.10",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "traccar",
+            "recommended": false,
+            "title": "Traccar",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "gps",
+                "tracking"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/traccar/traccar"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/traccar/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Traccar runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "traccar",
+                    "uid": 568,
+                    "user_name": "traccar"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "zipline": {
+            "app_readme": "<h1>Zipline</h1> <p><a href=\"https://zipline.diced.sh/\">Zipline</a> The next generation ShareX / File upload server.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Zipline The next generation ShareX / File upload server.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://zipline.diced.sh/",
+            "location": "/__w/apps/apps/trains/community/zipline",
+            "latest_version": "1.0.17",
+            "latest_app_version": "4.2.3",
+            "latest_human_version": "4.2.3_1.0.17",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "zipline",
+            "recommended": false,
+            "title": "Zipline",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "zipline",
+                "media"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/diced/zipline"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/zipline/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Zipline runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "zipline",
+                    "uid": 568,
+                    "user_name": "zipline"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "mongodb": {
+            "app_readme": "<h1>MongoDB</h1> <p><a href=\"https://www.mongodb.com/\">MongoDB</a> is a document database designed for ease of application development and scaling.</p>",
+            "categories": [
+                "database"
+            ],
+            "description": "MongoDB is a document database designed for ease of application development and scaling.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.mongodb.com/",
+            "location": "/__w/apps/apps/trains/community/mongodb",
+            "latest_version": "1.0.11",
+            "latest_app_version": "8.0.13",
+            "latest_human_version": "8.0.13_1.0.11",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "mongodb",
+            "recommended": false,
+            "title": "MongoDB",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "database"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://hub.docker.com/_/mongo"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/mongodb/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "MongoDB runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "mongodb",
+                    "uid": 568,
+                    "user_name": "mongodb"
+                }
+            ]
+        },
+        "briefkasten": {
+            "app_readme": "<h1>Briefkasten</h1> <p><a href=\"https://github.com/ndom91/briefkasten\">Briefkasten</a> is a self hosted bookmarking app</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Briefkasten is a self hosted bookmarking app",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/ndom91/briefkasten",
+            "location": "/__w/apps/apps/trains/community/briefkasten",
+            "latest_version": "1.3.10",
+            "latest_app_version": "latest",
+            "latest_human_version": "latest_1.3.10",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "briefkasten",
+            "recommended": false,
+            "title": "Briefkasten",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "bookmark"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/briefkasten/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/briefkasten/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/briefkasten/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/briefkasten/screenshots/screenshot4.png"
+            ],
+            "sources": [
+                "https://github.com/ndom91/briefkasten",
+                "https://docs.briefkastenhq.com/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/briefkasten/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Briefkasten runs as non-root user.",
+                    "gid": 1001,
+                    "group_name": "briefkasten",
+                    "uid": 1001,
+                    "user_name": "briefkasten"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "photoview": {
+            "app_readme": "<h1>Photoview</h1> <p><a href=\"https://photoview.github.io/\">Photoview</a> is a photo gallery for self-hosted personal servers</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Photo gallery for self-hosted personal servers",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://photoview.github.io/",
+            "location": "/__w/apps/apps/trains/community/photoview",
+            "latest_version": "1.0.7",
+            "latest_app_version": "2.4.0",
+            "latest_human_version": "2.4.0_1.0.7",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "photoview",
+            "recommended": false,
+            "title": "Photoview",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "photos"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/photoview/photoview",
+                "https://hub.docker.com/r/photoview/photoview"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/photoview/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Photoview runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "photoview",
+                    "uid": 568,
+                    "user_name": "photoview"
+                }
+            ]
+        },
+        "gitea": {
+            "app_readme": "<h1>Gitea</h1> <p><a href=\"https://gitea.io/en-us\">Gitea</a> - Git with a cup of tea</p> <p>On initial startup a setup wizard will be launched with settings for <code>database</code>, <code>ports</code>, <code>path</code>, and <code>domain</code> prefilled. Keep them as they are, fill anything you want in the optional settings section and click on <code>Install Gitea</code>.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Gitea - Git with a cup of tea",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://gitea.io/en-us",
+            "location": "/__w/apps/apps/trains/community/gitea",
+            "latest_version": "1.3.15",
+            "latest_app_version": "1.24.5",
+            "latest_human_version": "1.24.5_1.3.15",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "gitea",
+            "recommended": false,
+            "title": "Gitea",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "git",
+                "gitea",
+                "source control"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/gitea/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/gitea/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/gitea/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/gitea/screenshots/screenshot4.png"
+            ],
+            "sources": [
+                "https://gitea.io/en-us",
+                "https://docs.gitea.io/en-us/install-with-docker-rootless"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/gitea/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Gitea runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "gitea",
+                    "uid": 1000,
+                    "user_name": "gitea"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "jenkins": {
+            "app_readme": "<h1>Jenkins</h1> <p><a href=\"https://www.jenkins.io/\">Jenkins</a>. The leading open source automation server, Jenkins provides hundreds of plugins to support building, deploying and automating any project.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Jenkins is a leading open source automation server,",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.jenkins.io/",
+            "location": "/__w/apps/apps/trains/community/jenkins",
+            "latest_version": "1.2.9",
+            "latest_app_version": "2.516.2-jdk17",
+            "latest_human_version": "2.516.2-jdk17_1.2.9",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "jenkins",
+            "recommended": false,
+            "title": "Jenkins",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "automation",
+                "ci/cd"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/jenkins/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/jenkins/screenshots/screenshot2.png"
+            ],
+            "sources": [
+                "https://hub.docker.com/r/jenkins/jenkins",
+                "https://github.com/jenkinsci/jenkins",
+                "https://www.jenkins.io/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/jenkins/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Jenkins runs as any non-root user.",
+                    "gid": 1000,
+                    "group_name": "jenkins",
+                    "uid": 1000,
+                    "user_name": "jenkins"
+                }
+            ]
+        },
+        "pgadmin": {
+            "app_readme": "<h1>pgAdmin</h1> <p><a href=\"https://github.com/pgadmin-org/pgadmin4\">pgAdmin</a> is the most popular and feature rich Open Source administration and development platform for PostgreSQL</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "pgAdmin is the most popular and feature rich Open Source administration and development platform for PostgreSQL",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.pgadmin.org/",
+            "location": "/__w/apps/apps/trains/community/pgadmin",
+            "latest_version": "1.2.10",
+            "latest_app_version": "9.8",
+            "latest_human_version": "9.8_1.2.10",
+            "last_update": "2025-09-04 12:07:56",
+            "name": "pgadmin",
+            "recommended": false,
+            "title": "pgAdmin",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "database",
+                "management"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/pgadmin/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/pgadmin/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/pgadmin/screenshots/screenshot3.png"
+            ],
+            "sources": [
+                "https://hub.docker.com/r/dpage/pgadmin4",
+                "https://www.pgadmin.org/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/pgadmin/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "pgAdmin is able to bind to privileged ports (< 1024)",
+                    "name": "NET_BIND_SERVICE"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "pgAdmin runs as non-root user.",
+                    "gid": 5050,
+                    "group_name": "pgadmin",
+                    "uid": 5050,
+                    "user_name": "pgadmin"
+                }
+            ]
+        },
+        "mattermost": {
+            "app_readme": "<h1>Mattermost</h1> <p><a href=\"https://mattermost.com/\">Mattermost</a> is an open source platform for secure collaboration across the entire software development lifecycle.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Mattermost is an open source platform for secure collaboration across the entire software development lifecycle..",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://mattermost.com/",
+            "location": "/__w/apps/apps/trains/community/mattermost",
+            "latest_version": "1.0.0",
+            "latest_app_version": "10.12.0",
+            "latest_human_version": "10.12.0_1.0.0",
+            "last_update": "2025-09-04 11:40:29",
+            "name": "mattermost",
+            "recommended": false,
+            "title": "Mattermost",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "collaboration",
+                "communication",
+                "team"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/mattermost/docker",
+                "https://hub.docker.com/r/mattermost/mattermost-team-edition",
+                "https://docs.mattermost.com/administration-guide/configure/environment-configuration-settings.html"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/mattermost/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Mattermost runs as non-root user.",
+                    "gid": 2000,
+                    "group_name": "mattermost",
+                    "uid": 2000,
+                    "user_name": "mattermost"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "kitchenowl": {
+            "app_readme": "<h1>KitchenOwl</h1> <p><a href=\"https://kitchenowl.org/\">KitchenOwl</a> is a self-hosted grocery list and recipe manager. The backend is made with Flask and the frontend with Flutter. Easily add items to your shopping list before you go shopping. You can also create recipes and add items based on what you want to cook.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "KitchenOwl is a self-hosted grocery list and recipe manager. The backend is made with Flask and the frontend with Flutter. Easily add items to your shopping list before you go shopping. You can also create recipes and add items based on what you want to cook.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://kitchenowl.org/",
+            "location": "/__w/apps/apps/trains/community/kitchenowl",
+            "latest_version": "1.0.10",
+            "latest_app_version": "v0.7.3",
+            "latest_human_version": "v0.7.3_1.0.10",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "kitchenowl",
+            "recommended": false,
+            "title": "KitchenOwl",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "grocery list",
+                "recipe manager",
+                "shopping list"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/TomBursch/kitchenowl",
+                "https://hub.docker.com/r/tombursch/kitchenowl"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/kitchenowl/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "KitchenOwl runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "kitchenowl",
+                    "uid": 568,
+                    "user_name": "kitchenowl"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "directus": {
+            "app_readme": "<h1>Directus</h1> <p><a href=\"https://github.com/directus/directus\">Directus</a> is a real-time API and App dashboard for managing SQL database content.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Directus is a real-time API and App dashboard for managing SQL database content.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://directus.io/",
+            "location": "/__w/apps/apps/trains/community/directus",
+            "latest_version": "1.0.18",
+            "latest_app_version": "11.11.0",
+            "latest_human_version": "11.11.0_1.0.18",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "directus",
+            "recommended": false,
+            "title": "Directus",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "directus"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/directus/directus",
+                "https://directus.io/",
+                "https://directus.io/docs/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/directus/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Directus runs as a non-root user.",
+                    "gid": 1000,
+                    "group_name": "directus",
+                    "uid": 1000,
+                    "user_name": "directus"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                }
+            ]
+        },
+        "tailscale": {
+            "app_readme": "<h1>Tailscale</h1> <p><a href=\"https://tailscale.com\">Tailscale</a> Secure remote access to shared resources</p> <ul> <li>When <code>Userspace</code> is <strong>disabled</strong>, <code>Tailscale</code> will run with <code>/dev/net/tun</code> device mounted from the host.</li> </ul>",
+            "categories": [
+                "networking"
+            ],
+            "description": "Secure remote access to shared resources",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://tailscale.com/",
+            "location": "/__w/apps/apps/trains/community/tailscale",
+            "latest_version": "1.3.8",
+            "latest_app_version": "v1.86.5",
+            "latest_human_version": "v1.86.5_1.3.8",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "tailscale",
+            "recommended": false,
+            "title": "Tailscale",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "vpn",
+                "tailscale"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://tailscale.com/",
+                "https://hub.docker.com/r/tailscale/tailscale"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/tailscale/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Tailscale is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Tailscale is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Tailscale is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Tailscale is able to perform network administration tasks",
+                    "name": "NET_ADMIN"
+                },
+                {
+                    "description": "Tailscale is able to use raw and packet sockets",
+                    "name": "NET_RAW"
+                },
+                {
+                    "description": "Tailscale is able to load and unload kernel modules",
+                    "name": "SYS_MODULE"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Tailscale runs as a root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "netbootxyz": {
+            "app_readme": "<h1>Netboot.xyz</h1> <p><a href=\"https://netboot.xyz\">netboot.xyz</a> lets you PXE boot various operating system installers or utilities from a single tool over the network.</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "netboot.xyz lets you PXE boot various operating system installers or utilities from a single tool over the network.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://netboot.xyz",
+            "location": "/__w/apps/apps/trains/community/netbootxyz",
+            "latest_version": "1.2.7",
+            "latest_app_version": "0.7.6-nbxyz4",
+            "latest_human_version": "0.7.6-nbxyz4_1.2.7",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "netbootxyz",
+            "recommended": false,
+            "title": "Netboot.xyz",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "tftp",
+                "network",
+                "pxe",
+                "netboot",
+                "netbootxyz",
+                "netboot.xyz"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/netbootxyz/screenshots/screenshot1.jpg"
+            ],
+            "sources": [
+                "https://github.com/netbootxyz/docker-netbootxyz",
+                "https://netboot.xyz"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/netbootxyz/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Netboot is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Netboot is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Netboot is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Netboot is able to send signals to any process",
+                    "name": "KILL"
+                },
+                {
+                    "description": "Netboot is able to bind to privileged ports (< 1024)",
+                    "name": "NET_BIND_SERVICE"
+                },
+                {
+                    "description": "Netboot is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Netboot is able to change user ID of processes",
+                    "name": "SETUID"
+                },
+                {
+                    "description": "Netboot is able to use chroot() system call",
+                    "name": "SYS_CHROOT"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Netboot.xyz runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "grafana": {
+            "app_readme": "<h1>Grafana</h1> <p><a href=\"https://grafana.com/\">Grafana</a> is the open source analytics &amp; monitoring solution for every database.</p> <p>Additional configuration can be made by adding additional environment variables Here is the available <a href=\"https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/\">configuration documentation</a></p> <p>Use the following syntax: <code>GF_[SECTION-NAME]_[KEY-NAME]</code></p> <p>Example: <code>GF_SMTP_ENABLED</code></p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Grafana is the open source analytics & monitoring solution for every database.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://grafana.com",
+            "location": "/__w/apps/apps/trains/community/grafana",
+            "latest_version": "1.3.9",
+            "latest_app_version": "12.1.1",
+            "latest_human_version": "12.1.1_1.3.9",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "grafana",
+            "recommended": false,
+            "title": "Grafana",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "analytics",
+                "monitoring",
+                "metrics",
+                "dashboards"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/grafana/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/grafana/screenshots/screenshot2.png"
+            ],
+            "sources": [
+                "https://hub.docker.com/r/grafana/grafana",
+                "https://github.com/grafana"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/grafana/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Grafana runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "grafana runs as any non-root user.",
+                    "uid": 568,
+                    "user_name": "grafana runs as any non-root user."
+                }
+            ]
+        },
+        "cloudflared": {
+            "app_readme": "<h1>Cloudflared</h1> <p><a href=\"https://github.com/cloudflare/cloudflared\">Cloudflared</a> is a client for Cloudflare Tunnel, a daemon that exposes private services through the Cloudflare edge.</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "Cloudflared is a client for Cloudflare Tunnel, a daemon that exposes private services through the Cloudflare edge.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/cloudflare/cloudflared",
+            "location": "/__w/apps/apps/trains/community/cloudflared",
+            "latest_version": "1.3.11",
+            "latest_app_version": "2025.8.1",
+            "latest_human_version": "2025.8.1_1.3.11",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "cloudflared",
+            "recommended": false,
+            "title": "Cloudflared",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "network",
+                "cloudflare",
+                "tunnel"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/cloudflare/cloudflared",
+                "https://hub.docker.com/r/cloudflare/cloudflared"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/cloudflared/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Cloudflared runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "cloudflared",
+                    "uid": 568,
+                    "user_name": "cloudflared"
+                }
+            ]
+        },
+        "arti": {
+            "app_readme": "<h1>Arti</h1> <p><a href=\"https://tpo.pages.torproject.net/core/arti/\">Arti</a> is an experimental Tor implementation written in Rust, and it is designed to be modular, reusable, and easy to audit.</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "Arti is an experimental Tor implementation written in Rust, and it is designed to be modular, reusable, and easy to audit.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://tpo.pages.torproject.net/core/arti/",
+            "location": "/__w/apps/apps/trains/community/arti",
+            "latest_version": "1.1.12",
+            "latest_app_version": "1.4.6",
+            "latest_human_version": "1.4.6_1.1.12",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "arti",
+            "recommended": false,
+            "title": "Arti",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "tor",
+                "privacy"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://tpo.pages.torproject.net/core/arti/",
+                "https://github.com/MAGICGrants/arti-docker"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/arti/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Arti runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "arti",
+                    "uid": 1000,
+                    "user_name": "arti"
+                }
+            ]
+        },
+        "crafty-4": {
+            "app_readme": "<h1>Crafty 4</h1> <p><a href=\"https://craftycontrol.com/\">Crafty 4</a> is the next iteration of our Minecraft Server Wrapper/Controller/Launcher</p>",
+            "categories": [
+                "games"
+            ],
+            "description": "Crafty 4 is the next iteration of our Minecraft Server Wrapper/Controller/Launcher",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://craftycontrol.com/",
+            "location": "/__w/apps/apps/trains/community/crafty-4",
+            "latest_version": "1.0.15",
+            "latest_app_version": "4.5.3",
+            "latest_human_version": "4.5.3_1.0.15",
+            "last_update": "2025-09-03 12:56:43",
+            "name": "crafty-4",
+            "recommended": false,
+            "title": "Crafty 4",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "minecraft",
+                "crafty"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://gitlab.com/crafty-controller/crafty-4"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/crafty-4/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Crafty 4 runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "crafty",
+                    "uid": 1000,
+                    "user_name": "crafty"
+                }
+            ]
+        },
+        "seaweedfs": {
+            "app_readme": "<h1>SeaweedFS</h1> <p><a href=\"https://seaweedfs.com/\">SeaweedFS</a> is a fast distributed storage system for blobs, objects, files, and data lake, for billions of files!</p>",
+            "categories": [
+                "storage"
+            ],
+            "description": "SeaweedFS is a fast distributed storage system for blobs, objects, files, and data lake, for billions of files!",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://seaweedfs.com/",
+            "location": "/__w/apps/apps/trains/community/seaweedfs",
+            "latest_version": "1.0.15",
+            "latest_app_version": "3.97",
+            "latest_human_version": "3.97_1.0.15",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "seaweedfs",
+            "recommended": false,
+            "title": "SeaweedFS",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "s3",
+                "webdav",
+                "blob",
+                "object",
+                "bucket"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/seaweedfs/seaweedfs",
+                "https://hub.docker.com/r/chrislusf/seaweedfs"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/seaweedfs/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "SeaweedFS runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "seaweedfs",
+                    "uid": 568,
+                    "user_name": "seaweedfs"
+                }
+            ]
+        },
+        "heimdall": {
+            "app_readme": "<h1>Heimdall</h1> <p><a href=\"https://heimdall.site\">Heimdall</a> is an Application dashboard and launcher.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "An Application dashboard and launcher",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://heimdall.site/",
+            "location": "/__w/apps/apps/trains/community/heimdall",
+            "latest_version": "1.0.13",
+            "latest_app_version": "2.7.4",
+            "latest_human_version": "2.7.4_1.0.13",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "heimdall",
+            "recommended": false,
+            "title": "Heimdall",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "dashboard"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/linuxserver/Heimdall",
+                "https://github.com/linuxserver/docker-heimdall"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/heimdall/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Heimdall is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Heimdall is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Heimdall is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Heimdall is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Heimdall is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Heimdall runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "deluge": {
+            "app_readme": "<h1>Deluge</h1> <p><a href=\"https://deluge-torrent.org/\">Deluge</a> is a lightweight, Free Software, cross-platform BitTorrent client.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Deluge is a lightweight, Free Software, cross-platform BitTorrent client.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://deluge-torrent.org",
+            "location": "/__w/apps/apps/trains/community/deluge",
+            "latest_version": "1.2.6",
+            "latest_app_version": "2.2.0",
+            "latest_human_version": "2.2.0_1.2.6",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "deluge",
+            "recommended": false,
+            "title": "Deluge",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "torrent",
+                "download"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/linuxserver/docker-deluge",
+                "https://deluge-torrent.org/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/deluge/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Deluge is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Deluge is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Deluge is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Deluge is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Deluge is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Deluge runs as a root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "linkding": {
+            "app_readme": "<h1>Linkding</h1> <p><a href=\"https://github.com/sissbruecker/linkding\">Linkding</a> is a bookmark manager that you can host yourself.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Linkding is a bookmark manager that you can host yourself.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/sissbruecker/linkding",
+            "location": "/__w/apps/apps/trains/community/linkding",
+            "latest_version": "1.3.11",
+            "latest_app_version": "1.42.0",
+            "latest_human_version": "1.42.0_1.3.11",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "linkding",
+            "recommended": false,
+            "title": "Linkding",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "bookmark"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/linkding/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/sissbruecker/linkding",
+                "https://hub.docker.com/r/sissbruecker/linkding/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/linkding/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Linkding runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "linkding",
+                    "uid": 568,
+                    "user_name": "linkding"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "anything-llm": {
+            "app_readme": "<h1>Anything-LLM</h1> <p><a href=\"https://anythingllm.com/\">Anything-LLM</a> is the all-in-one Desktop &amp; Docker AI application with built-in RAG, AI agents, No-code agent builder, MCP compatibility, and more.</p>",
+            "categories": [
+                "ai"
+            ],
+            "description": "Anything-LLM is the all-in-one Desktop & Docker AI application with built-in RAG, AI agents, No-code agent builder, MCP compatibility, and more.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://anythingllm.com/",
+            "location": "/__w/apps/apps/trains/community/anything-llm",
+            "latest_version": "1.0.4",
+            "latest_app_version": "1.8.5",
+            "latest_human_version": "1.8.5_1.0.4",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "anything-llm",
+            "recommended": false,
+            "title": "Anything LLM",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "ai",
+                "llm",
+                "webui",
+                "anything-llm"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/Mintplex-Labs/anything-llm"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/anything-llm/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Anything LLM is able to perform system administration operations",
+                    "name": "SYS_ADMIN"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Anything LLM runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "anything-llm",
+                    "uid": 1000,
+                    "user_name": "anything-llm"
+                }
+            ]
+        },
+        "sabnzbd": {
+            "app_readme": "<h1>SABnzbd</h1> <p><a href=\"https://github.com/Sabnzbd/Sabnzbd\">SABnzbd</a> is an Open Source Binary Newsreader written in Python.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "SABnzbd is an Open Source Binary Newsreader written in Python.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://sabnzbd.org/",
+            "location": "/__w/apps/apps/trains/community/sabnzbd",
+            "latest_version": "1.2.8",
+            "latest_app_version": "4.5.3",
+            "latest_human_version": "4.5.3_1.2.8",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "sabnzbd",
+            "recommended": false,
+            "title": "SABnzbd",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "usenet",
+                "newsreader"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/sabnzbd/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/sabnzbd/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/sabnzbd/screenshots/screenshot3.png"
+            ],
+            "sources": [
+                "https://github.com/home-operations/containers/tree/main/apps/sabnzbd",
+                "https://sabnzbd.org/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/sabnzbd/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "SABnzbd runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "sabnzbd",
+                    "uid": 568,
+                    "user_name": "sabnzbd"
+                }
+            ]
+        },
+        "byparr": {
+            "app_readme": "<h1>Byparr</h1> <p><a href=\"https://github.com/ThePhaseless/Byparr\">Byparr</a> is a FlareSolverr drop-in replacement with FastAPI and nodriver</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "Byparr is a FlareSolverr drop-in replacement with FastAPI and nodriver",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/ThePhaseless/Byparr",
+            "location": "/__w/apps/apps/trains/community/byparr",
+            "latest_version": "1.0.3",
+            "latest_app_version": "1.2.1",
+            "latest_human_version": "1.2.1_1.0.3",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "byparr",
+            "recommended": false,
+            "title": "Byparr",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "networking",
+                "captcha"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/ThePhaseless/Byparr"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/byparr/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Byparr runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "ntfy": {
+            "app_readme": "<h1>ntfy</h1> <p><a href=\"https://ntfy.sh/\">ntfy</a> (pronounced \"notify\") is a simple HTTP-based pub-sub notification service.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "ntfy (pronounced \"notify\") is a simple HTTP-based pub-sub notification service.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://ntfy.sh/",
+            "location": "/__w/apps/apps/trains/community/ntfy",
+            "latest_version": "1.0.10",
+            "latest_app_version": "v2.14.0",
+            "latest_human_version": "v2.14.0_1.0.10",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "ntfy",
+            "recommended": false,
+            "title": "ntfy",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "notification"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/binwiederhier/ntfy",
+                "https://hub.docker.com/r/binwiederhier/ntfy"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/ntfy/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "ntfy runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "ntfy",
+                    "uid": 568,
+                    "user_name": "ntfy"
+                }
+            ]
+        },
+        "zoraxy": {
+            "app_readme": "<h1>Zoraxy</h1> <p><a href=\"https://zoraxy.aroz.org/\">Zoraxy</a> A general purpose HTTP reverse proxy and forwarding tool.</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "A general purpose HTTP reverse proxy and forwarding tool.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://zoraxy.aroz.org/",
+            "location": "/__w/apps/apps/trains/community/zoraxy",
+            "latest_version": "1.0.0",
+            "latest_app_version": "v3.2.5",
+            "latest_human_version": "v3.2.5_1.0.0",
+            "last_update": "2025-09-05 14:09:56",
+            "name": "zoraxy",
+            "recommended": false,
+            "title": "Zoraxy",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "proxy",
+                "reverse-proxy"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/tobychui/zoraxy",
+                "https://github.com/tobychui/zoraxy/blob/main/docker/README.md",
+                "https://hub.docker.com/r/zoraxydocker/zoraxy"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/zoraxy/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Zoraxy is able to perform network administration tasks",
+                    "name": "NET_ADMIN"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Zoraxy runs as a root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "zerotier": {
+            "app_readme": "<h1>Zerotier</h1> <p><a href=\"https://www.zerotier.com\">Zerotier</a> Securely connect any device, anywhere.</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "Securely connect any device, anywhere.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.zerotier.com",
+            "location": "/__w/apps/apps/trains/community/zerotier",
+            "latest_version": "1.2.5",
+            "latest_app_version": "1.14.2",
+            "latest_human_version": "1.14.2_1.2.5",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "zerotier",
+            "recommended": false,
+            "title": "Zerotier",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "vpn",
+                "zerotier"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://www.zerotier.com",
+                "https://hub.docker.com/r/zerotier/zerotier"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/zerotier/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Zerotier is able to write records to audit log",
+                    "name": "AUDIT_WRITE"
+                },
+                {
+                    "description": "Zerotier is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Zerotier is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Zerotier is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Zerotier is able to perform network administration tasks",
+                    "name": "NET_ADMIN"
+                },
+                {
+                    "description": "Zerotier is able to bind to privileged ports (< 1024)",
+                    "name": "NET_BIND_SERVICE"
+                },
+                {
+                    "description": "Zerotier is able to use raw and packet sockets",
+                    "name": "NET_RAW"
+                },
+                {
+                    "description": "Zerotier is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Zerotier is able to transfer capabilities between processes",
+                    "name": "SETPCAP"
+                },
+                {
+                    "description": "Zerotier is able to change user ID of processes",
+                    "name": "SETUID"
+                },
+                {
+                    "description": "Zerotier is able to perform system administration operations",
+                    "name": "SYS_ADMIN"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Zerotier runs as a root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "bitcoind-knots": {
+            "app_readme": "<h1>Bitcoin Knots</h1> <p>Run your personal node powered by <a href=\"https://bitcoinknots.org/\">Bitcoin Knots</a>.</p>",
+            "categories": [
+                "financial"
+            ],
+            "description": "Run your personal node powered by Bitcoin Knots.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://bitcoinknots.org",
+            "location": "/__w/apps/apps/trains/community/bitcoind-knots",
+            "latest_version": "1.0.7",
+            "latest_app_version": "v29.1",
+            "latest_human_version": "v29.1_1.0.7",
+            "last_update": "2025-09-05 20:13:30",
+            "name": "bitcoind-knots",
+            "recommended": false,
+            "title": "Bitcoin Knots",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "bitcoin",
+                "cryptocurrency",
+                "blockchain"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://bitcoinknots.org",
+                "https://github.com/Retropex/docker-bitcoind-truenas"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/bitcoind-knots/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Bitcoin Node runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "bitcoind",
+                    "uid": 568,
+                    "user_name": "bitcoind"
+                }
+            ]
+        },
+        "mariadb": {
+            "app_readme": "<h1>MariaDB</h1> <p><a href=\"https://mariadb.org/\">MariaDB</a> - MariaDB server is a community developed fork of MySQL server.</p> <p>MariaDB server is a community developed fork of MySQL server. Started by core members of the original MySQL team, MariaDB actively works with outside developers to deliver the most featureful, stable, and sanely licensed open SQL server in the industry.</p>",
+            "categories": [
+                "database"
+            ],
+            "description": "MariaDB server is a community developed fork of MySQL server.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://mariadb.org/",
+            "location": "/__w/apps/apps/trains/community/mariadb",
+            "latest_version": "1.0.12",
+            "latest_app_version": "12.0.2",
+            "latest_human_version": "12.0.2_1.0.12",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "mariadb",
+            "recommended": false,
+            "title": "Mariadb",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "database"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://hub.docker.com/_/mariadb"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/mariadb/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "MariaDB runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "mysql",
+                    "uid": 999,
+                    "user_name": "mysql"
+                }
+            ]
+        },
+        "outline": {
+            "app_readme": "<h1>Outline</h1> <p><a href=\"https://www.getoutline.com/\">Outline</a> is the fastest knowledge base for growing teams.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Outline is the fastest knowledge base for growing teams. Beautiful, realtime collaborative, feature packed, and markdown compatible.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.getoutline.com",
+            "location": "/__w/apps/apps/trains/community/outline",
+            "latest_version": "1.0.31",
+            "latest_app_version": "0.87.3",
+            "latest_human_version": "0.87.3_1.0.31",
+            "last_update": "2025-09-02 15:29:51",
+            "name": "outline",
+            "recommended": false,
+            "title": "Outline",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "documentation",
+                "knowledgebase"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://docs.getoutline.com/s/hosting",
+                "https://github.com/outline/outline"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/outline/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Outline runs as non-root user.",
+                    "gid": 1001,
+                    "group_name": "outline",
+                    "uid": 1001,
+                    "user_name": "outline"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                }
+            ]
+        },
+        "rust-desk": {
+            "app_readme": "<h1>Rust Desk</h1> <p><a href=\"https://rustdesk.com\">Rust Desk</a> is an open-source remote desktop, and alternative to TeamViewer.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Rust Desk is an open-source remote desktop, and alternative to TeamViewer.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://rustdesk.com",
+            "location": "/__w/apps/apps/trains/community/rust-desk",
+            "latest_version": "1.2.6",
+            "latest_app_version": "1.1.14",
+            "latest_human_version": "1.1.14_1.2.6",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "rust-desk",
+            "recommended": false,
+            "title": "Rust Desk",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "remote",
+                "desktop"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://hub.docker.com/r/rustdesk/rustdesk-server",
+                "https://github.com/rustdesk/rustdesk-server"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/rust-desk/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Rust Desk runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "rust-desk",
+                    "uid": 568,
+                    "user_name": "rust-desk"
+                }
+            ]
+        },
+        "i2p": {
+            "app_readme": "<h1>I2P</h1> <p><a href=\"https://i2pd.website/\">I2P</a> (Invisible Internet Protocol) is a universal anonymous network layer. All communications over I2P are anonymous and end-to-end encrypted, participants don't reveal their real IP addresses.</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "I2P (Invisible Internet Protocol) is a universal anonymous network layer. All communications over I2P are anonymous and end-to-end encrypted, participants don't reveal their real IP addresses.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://geti2p.net",
+            "location": "/__w/apps/apps/trains/community/i2p",
+            "latest_version": "1.0.13",
+            "latest_app_version": "i2p-2.9.0",
+            "latest_human_version": "i2p-2.9.0_1.0.13",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "i2p",
+            "recommended": false,
+            "title": "I2P",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "i2p",
+                "privacy"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://geti2p.net",
+                "https://github.com/i2p/i2p.i2p"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/i2p/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "I2P runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "monero-lws": {
+            "app_readme": "<h1>Monero LWS</h1> <p><a href=\"https://github.com/vtnerd/monero-lws\">Monero LWS</a> is a Monero light-wallet server, currently compatible with MyMonero and Edge.</p>",
+            "categories": [
+                "financial"
+            ],
+            "description": "Monero LWS is a Monero light-wallet server, currently compatible with MyMonero and Edge.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/vtnerd/monero-lws",
+            "location": "/__w/apps/apps/trains/community/monero-lws",
+            "latest_version": "1.0.9",
+            "latest_app_version": "0.3_0.18",
+            "latest_human_version": "0.3_0.18_1.0.9",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "monero-lws",
+            "recommended": false,
+            "title": "Monero LWS",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "monero",
+                "cryptocurrency",
+                "wallet",
+                "rpc",
+                "blockchain",
+                "privacy"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/vtnerd/monero-lws",
+                "https://github.com/MAGICGrants/monero-lws-docker"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/monero-lws/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Monero LWS runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "monero-lws",
+                    "uid": 1000,
+                    "user_name": "monero-lws"
+                }
+            ]
+        },
+        "pigallery2": {
+            "app_readme": "<h1>PiGallery2</h1> <p><a href=\"https://bpatrik.github.io/pigallery2\">PiGallery2</a> is a fast directory-first photo gallery website, with rich UI, optimized for running on low resource servers</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "PiGallery2 is a fast directory-first photo gallery website, with rich UI, optimized for running on low resource servers",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://bpatrik.github.io/pigallery2",
+            "location": "/__w/apps/apps/trains/community/pigallery2",
+            "latest_version": "1.2.6",
+            "latest_app_version": "2.0.0",
+            "latest_human_version": "2.0.0_1.2.6",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "pigallery2",
+            "recommended": false,
+            "title": "PiGallery2",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "photo",
+                "media"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/pigallery2/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/pigallery2/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/pigallery2/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/pigallery2/screenshots/screenshot4.png",
+                "https://media.sys.truenas.net/apps/pigallery2/screenshots/screenshot5.png",
+                "https://media.sys.truenas.net/apps/pigallery2/screenshots/screenshot6.png",
+                "https://media.sys.truenas.net/apps/pigallery2/screenshots/screenshot7.png",
+                "https://media.sys.truenas.net/apps/pigallery2/screenshots/screenshot8.png",
+                "https://media.sys.truenas.net/apps/pigallery2/screenshots/screenshot9.png",
+                "https://media.sys.truenas.net/apps/pigallery2/screenshots/screenshot10.png"
+            ],
+            "sources": [
+                "https://hub.docker.com/r/bpatrik/pigallery2",
+                "https://github.com/bpatrik/pigallery2"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/pigallery2/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "PiGallery2 runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "pigallery2",
+                    "uid": 568,
+                    "user_name": "pigallery2"
+                }
+            ]
+        },
+        "peanut": {
+            "app_readme": "<h1>PeaNUT</h1> <p><a href=\"https://github.com/Brandawg93/PeaNUT\">PeaNUT</a> is a tiny Dashboard for Network UPS Tools.</p>",
+            "categories": [
+                "monitoring"
+            ],
+            "description": "A tiny Dashboard for Network UPS Tools",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/Brandawg93/PeaNUT",
+            "location": "/__w/apps/apps/trains/community/peanut",
+            "latest_version": "1.0.9",
+            "latest_app_version": "5.14.1",
+            "latest_human_version": "5.14.1_1.0.9",
+            "last_update": "2025-09-05 14:25:56",
+            "name": "peanut",
+            "recommended": false,
+            "title": "PeaNUT",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "ups",
+                "nut"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://hub.docker.com/r/brandawg93/peanut",
+                "https://github.com/Brandawg93/PeaNUT"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/peanut/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Peanut runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "peanut",
+                    "uid": 568,
+                    "user_name": "peanut"
+                }
+            ]
+        },
+        "tubearchivist": {
+            "app_readme": "<h1>Tube Archivist</h1> <p><a href=\"https://tubearchivist.com/\">Tube Archivist</a> is a self hosted YouTube media server.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Your self hosted YouTube media server",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://tubearchivist.com/",
+            "location": "/__w/apps/apps/trains/community/tubearchivist",
+            "latest_version": "1.0.6",
+            "latest_app_version": "v0.5.7",
+            "latest_human_version": "v0.5.7_1.0.6",
+            "last_update": "2025-09-03 12:56:43",
+            "name": "tubearchivist",
+            "recommended": false,
+            "title": "Tube Archivist",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media-server",
+                "youtube",
+                "download",
+                "videos"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/tubearchivist/tubearchivist",
+                "https://hub.docker.com/r/bbilly1/tubearchivist"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/tubearchivist/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Tube Archivist is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Tube Archivist is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Tube Archivist is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Tube Archivist runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                },
+                {
+                    "description": "Elastic Search runs as a non-root user.",
+                    "gid": 1000,
+                    "group_name": "elastic-search",
+                    "uid": 1000,
+                    "user_name": "elastic-search"
+                }
+            ]
+        },
+        "palworld": {
+            "app_readme": "<h1>Palworld</h1> <p><a href=\"https://www.pocketpair.jp/palworld\">Palworld</a> is a multiplayer, open-world survival crafting game where you befriend and collect mysterious creatures called \"Pals\".</p>",
+            "categories": [
+                "games"
+            ],
+            "description": "Palworld is a multiplayer, open-world survival crafting game where you befriend and collect mysterious creatures called \"Pals\".",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.pocketpair.jp/palworld",
+            "location": "/__w/apps/apps/trains/community/palworld",
+            "latest_version": "1.2.5",
+            "latest_app_version": "palworld",
+            "latest_human_version": "palworld_1.2.5",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "palworld",
+            "recommended": false,
+            "title": "Palworld",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "game",
+                "palworld"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://www.pocketpair.jp/palworld",
+                "https://github.com/ich777/docker-steamcmd-server/tree/palworld"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/palworld/icons/icon.webp",
+            "capabilities": [
+                {
+                    "description": "Palworld is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Palworld is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Palworld is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Palworld is able to send signals to any process",
+                    "name": "KILL"
+                },
+                {
+                    "description": "Palworld is able to bind to privileged ports (< 1024)",
+                    "name": "NET_BIND_SERVICE"
+                },
+                {
+                    "description": "Palworld is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Palworld is able to change user ID of processes",
+                    "name": "SETUID"
+                },
+                {
+                    "description": "Palworld is able to override resource limits",
+                    "name": "SYS_RESOURCE"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Palworld runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "mealie": {
+            "app_readme": "<h1>Mealie</h1> <p><a href=\"https://mealie.io\">Mealie</a> is a self-hosted recipe manager and meal planner</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Mealie is a self-hosted recipe manager and meal planner",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://docs.mealie.io/",
+            "location": "/__w/apps/apps/trains/community/mealie",
+            "latest_version": "1.5.13",
+            "latest_app_version": "v3.1.2",
+            "latest_human_version": "v3.1.2_1.5.13",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "mealie",
+            "recommended": false,
+            "title": "Mealie",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "recipes",
+                "meal planner"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/mealie/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://docs.mealie.io/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/mealie/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Mealie runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "mealie",
+                    "uid": 568,
+                    "user_name": "mealie"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "flaresolverr": {
+            "app_readme": "<h1>FlareSolverr</h1> <p><a href=\"https://github.com/FlareSolverr/FlareSolverr\">FlareSolverr</a> - Proxy server to bypass Cloudflare protection</p> <p>FlareSolverr is a proxy server to bypass Cloudflare and DDoS-GUARD protection.</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "FlareSolverr is a proxy server to bypass Cloudflare and DDoS-GUARD protection.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/FlareSolverr/FlareSolverr",
+            "location": "/__w/apps/apps/trains/community/flaresolverr",
+            "latest_version": "1.1.9",
+            "latest_app_version": "v3.4.0",
+            "latest_human_version": "v3.4.0_1.1.9",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "flaresolverr",
+            "recommended": false,
+            "title": "FlareSolverr",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/FlareSolverr/FlareSolverr"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/flaresolverr/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "FlareSolverr runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "flaresolverr",
+                    "uid": 1000,
+                    "user_name": "flaresolverr"
+                }
+            ]
+        },
+        "nginx-proxy-manager": {
+            "app_readme": "<h1>Nginx Proxy Manager</h1> <p><a href=\"https://nginxproxymanager.com\">Nginx Proxy Manager</a> Expose your services easily and securely</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "Expose your services easily and securely",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://nginxproxymanager.com/",
+            "location": "/__w/apps/apps/trains/community/nginx-proxy-manager",
+            "latest_version": "1.2.9",
+            "latest_app_version": "2.12.6",
+            "latest_human_version": "2.12.6_1.2.9",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "nginx-proxy-manager",
+            "recommended": false,
+            "title": "Nginx Proxy Manager",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "reverse",
+                "nginx",
+                "proxy"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/nginx-proxy-manager/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/nginx-proxy-manager/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/nginx-proxy-manager/screenshots/screenshot3.png"
+            ],
+            "sources": [
+                "https://nginxproxymanager.com/",
+                "https://hub.docker.com/r/jc21/nginx-proxy-manager"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/nginx-proxy-manager/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Nginx Proxy Manager is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Nginx Proxy Manager is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Nginx Proxy Manager is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Nginx Proxy Manager is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Nginx Proxy Manager is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Nginx Proxy Manager runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "umami": {
+            "app_readme": "<h1>Umami</h1> <p><a href=\"https://umami.is/\">Umami</a> is a simple, fast, privacy-focused alternative to Google Analytics.</p>",
+            "categories": [
+                "monitoring"
+            ],
+            "description": "Umami is a simple, fast, privacy-focused alternative to Google Analytics.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://umami.is/",
+            "location": "/__w/apps/apps/trains/community/umami",
+            "latest_version": "1.0.26",
+            "latest_app_version": "postgresql-v2.19.0",
+            "latest_human_version": "postgresql-v2.19.0_1.0.26",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "umami",
+            "recommended": false,
+            "title": "Umami",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "analytics",
+                "monitoring"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/umami/screenshots/screenshot1.jpg"
+            ],
+            "sources": [
+                "https://github.com/umami-software/umami"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/umami/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Umami runs as non-root user.",
+                    "gid": 1001,
+                    "group_name": "umami",
+                    "uid": 1001,
+                    "user_name": "umami"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "bitmagnet": {
+            "app_readme": "<h1>Bitmagnet</h1> <p><a href=\"https://bitmagnet.io/\">Bitmagnet</a> is a self-hosted BitTorrent indexer, DHT crawler, content classifier and torrent search engine with web UI, GraphQL API and Servarr stack integration.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Bitmagnet is a self-hosted BitTorrent indexer, DHT crawler, content classifier and torrent search engine with web UI, GraphQL API and Servarr stack integration.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://bitmagnet.io/",
+            "location": "/__w/apps/apps/trains/community/bitmagnet",
+            "latest_version": "1.0.9",
+            "latest_app_version": "v0.10.0",
+            "latest_human_version": "v0.10.0_1.0.9",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "bitmagnet",
+            "recommended": false,
+            "title": "Bitmagnet",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "torrent",
+                "bittorrent",
+                "indexer",
+                "dht"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/bitmagnet/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/bitmagnet-io/bitmagnet"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/bitmagnet/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Bitmagnet runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "bitmagnet",
+                    "uid": 568,
+                    "user_name": "bitmagnet"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "iconik-storage-gateway": {
+            "app_readme": "<h1>Iconik Storage Gateway</h1> <p><a href=\"https://iconik.io\">Iconik Storage Gateway</a> is a cloud-native storage gateway for your on-premise storage.</p> <p>Sync your folder structures from your local SAN, NAS or personal computer to the cloud, using fast parallel original file uploads, or using local transcoding to just upload proxies and keyframes.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "The iconik Storage Gateway (ISG) allows you to use your on-premise storage with iconik in the cloud.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://iconik.io",
+            "location": "/__w/apps/apps/trains/community/iconik-storage-gateway",
+            "latest_version": "1.0.24",
+            "latest_app_version": "3.13.5",
+            "latest_human_version": "3.13.5_1.0.24",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "iconik-storage-gateway",
+            "recommended": false,
+            "title": "Iconik Storage Gateway",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "iconik"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/truenas/iconik-storage-gateway-docker",
+                "https://app.iconik.io/help/pages/isg"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/iconik-storage-gateway/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Iconik Storage Gateway runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "iconik-storage-gateway",
+                    "uid": 568,
+                    "user_name": "iconik-storage-gateway"
+                }
+            ]
+        },
+        "jackett": {
+            "app_readme": "<h1>Jackett</h1> <p><a href=\"https://github.com/Jackett/Jackett\">Jackett</a> is a free and open source API supported by your favorite torrent trackers.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "API Support for your favorite torrent trackers",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/Jackett/Jackett",
+            "location": "/__w/apps/apps/trains/community/jackett",
+            "latest_version": "1.0.183",
+            "latest_app_version": "0.22.2429",
+            "latest_human_version": "0.22.2429_1.0.183",
+            "last_update": "2025-09-05 14:25:56",
+            "name": "jackett",
+            "recommended": false,
+            "title": "Jackett",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "indexer"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/Jackett/Jackett",
+                "https://github.com/home-operations/containers/tree/main/apps/jackett"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/jackett/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Jackett runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "jackett",
+                    "uid": 568,
+                    "user_name": "jackett"
+                }
+            ]
+        },
+        "passbolt": {
+            "app_readme": "<h1>Passbolt</h1> <p><a href=\"https://www.passbolt.com\">Passbolt</a> is a security-first, open source password manager</p> <h2>Register admin user</h2> <p>Connect to the container's shell and run the following command replacing the values (<code>user@example.com</code>, <code>first_name</code>, <code>last_name</code>) with your own values.</p> <p><code>shell /usr/share/php/passbolt/bin/cake passbolt register_user -r admin -u user@example.com -f first_name -l last_name</code></p>",
+            "categories": [
+                "security"
+            ],
+            "description": "Passbolt is a security-first, open source password manager",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.passbolt.com",
+            "location": "/__w/apps/apps/trains/community/passbolt",
+            "latest_version": "1.2.14",
+            "latest_app_version": "5.4.1",
+            "latest_human_version": "5.4.1_1.2.14",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "passbolt",
+            "recommended": false,
+            "title": "Passbolt",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "password",
+                "manager"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/passbolt/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/passbolt/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/passbolt/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/passbolt/screenshots/screenshot4.png"
+            ],
+            "sources": [
+                "https://hub.docker.com/r/passbolt/passbolt",
+                "https://www.passbolt.com"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/passbolt/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Passbolt runs as a non-root user.",
+                    "gid": 33,
+                    "group_name": "www-data",
+                    "uid": 33,
+                    "user_name": "www-data"
+                },
+                {
+                    "description": "MariaDB runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "mariadb",
+                    "uid": 999,
+                    "user_name": "mariadb"
+                }
+            ]
+        },
+        "romm": {
+            "app_readme": "<h1>RomM</h1> <h2>Your beautiful, powerful, self-hosted ROM manager</h2> <p><a href=\"https://romm.app\">RomM</a> allows you to scan, enrich, and browse your game collection with a clean and responsive interface. With support for multiple platforms, various naming schemes, and custom tags, RomM is a must-have for anyone who plays on emulators.</p>",
+            "categories": [
+                "games"
+            ],
+            "description": "RomM allows you to scan, enrich, browse and play your game collection with a clean and responsive interface. With support for multiple platforms, various naming schemes, and custom tags, RomM is a must-have for anyone who plays on emulators.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://romm.app",
+            "location": "/__w/apps/apps/trains/community/romm",
+            "latest_version": "1.0.43",
+            "latest_app_version": "4.1.6",
+            "latest_human_version": "4.1.6_1.0.43",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "romm",
+            "recommended": false,
+            "title": "Romm",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "romm",
+                "rom",
+                "manager",
+                "emulator"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/romm/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/romm/screenshots/screenshot2.png"
+            ],
+            "sources": [
+                "https://github.com/rommapp/romm"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/romm/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Romm runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "romm",
+                    "uid": 568,
+                    "user_name": "romm"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                }
+            ]
+        },
+        "jellyseerr": {
+            "app_readme": "<h1>Jellyseerr</h1> <p><a href=\"https://github.com/Fallenbagel/jellyseerr\">Jellyseerr</a> is a free and open source software application for managing requests for your media library.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Jellyseerr is a free and open source software application for managing requests for your media library.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/Fallenbagel/jellyseerr",
+            "location": "/__w/apps/apps/trains/community/jellyseerr",
+            "latest_version": "1.2.11",
+            "latest_app_version": "2.7.3",
+            "latest_human_version": "2.7.3_1.2.11",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "jellyseerr",
+            "recommended": false,
+            "title": "Jellyseerr",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/jellyseerr/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/Fallenbagel/jellyseerr",
+                "https://hub.docker.com/r/fallenbagel/jellyseerr"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/jellyseerr/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Jellyseerr runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "jellyseerr",
+                    "uid": 568,
+                    "user_name": "jellyseerr"
+                }
+            ]
+        },
+        "omni-tools": {
+            "app_readme": "<h1>Omni Tools</h1> <p><a href=\"https://omnitools.app/\">Omni Tools</a> is a self-hosted collection of powerful web-based tools for everyday tasks. No ads, no tracking, just fast, accessible utilities right from your browser!</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Self-hosted collection of powerful web-based tools for everyday tasks. No ads, no tracking, just fast, accessible utilities right from your browser!",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://omnitools.app/",
+            "location": "/__w/apps/apps/trains/community/omni-tools",
+            "latest_version": "1.0.3",
+            "latest_app_version": "0.5.0",
+            "latest_human_version": "0.5.0_1.0.3",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "omni-tools",
+            "recommended": false,
+            "title": "Omni Tools",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "utilities",
+                "tools"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/iib0011/omni-tools",
+                "https://hub.docker.com/r/iib0011/omni-tools"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/omni-tools/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Omni Tools is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Omni Tools is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Omni Tools is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Omni Tools is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Omni Tools is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Omni Tools runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "kavita": {
+            "app_readme": "<h1>Kavita</h1> <p><a href=\"https://www.kavitareader.com\">Kavita</a> is a fast, feature rich, cross platform reading server.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Kavita is a fast, feature rich, cross platform reading server.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.kavitareader.com/",
+            "location": "/__w/apps/apps/trains/community/kavita",
+            "latest_version": "1.2.7",
+            "latest_app_version": "0.8.7",
+            "latest_human_version": "0.8.7_1.2.7",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "kavita",
+            "recommended": false,
+            "title": "Kavita",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "ebook",
+                "manga"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/kavita/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/kavita/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/kavita/screenshots/screenshot3.png",
+                "https://media.sys.truenas.net/apps/kavita/screenshots/screenshot4.png",
+                "https://media.sys.truenas.net/apps/kavita/screenshots/screenshot5.png"
+            ],
+            "sources": [
+                "https://github.com/Kareadita/Kavita",
+                "https://www.kavitareader.com"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/kavita/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Kavita is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Kavita is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Kavita is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Kavita is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Kavita is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Kavita runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "adventurelog": {
+            "app_readme": "<h1>AdventureLog</h1> <p><a href=\"https://adventurelog.app/\">AdventureLog</a> is a self-hostable travel tracker and trip planner.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Self-hostable travel tracker and trip planner.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://adventurelog.app/",
+            "location": "/__w/apps/apps/trains/community/adventurelog",
+            "latest_version": "1.0.0",
+            "latest_app_version": "0.11.0",
+            "latest_human_version": "0.11.0_1.0.0",
+            "last_update": "2025-09-03 10:24:37",
+            "name": "adventurelog",
+            "recommended": false,
+            "title": "AdventureLog",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "adventure",
+                "location",
+                "tracker",
+                "planner"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/seanmorley15/AdventureLog"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/adventurelog/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Backend is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Backend is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Backend is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Backend is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Backend is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "AdventureLog Backend runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                },
+                {
+                    "description": "AdventureLog Frontend runs as root user.",
+                    "gid": 1000,
+                    "group_name": "adventurelog",
+                    "uid": 1000,
+                    "user_name": "adventurelog"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "metube": {
+            "app_readme": "<h1>MeTube</h1> <p><a href=\"https://github.com/alexta69/metube\">MeTube</a> is a web GUI for youtube-dl (using the yt-dlp fork) with playlist support.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "MeTube is a web GUI for youtube-dl (using the yt-dlp fork) with playlist support.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/alexta69/metube",
+            "location": "/__w/apps/apps/trains/community/metube",
+            "latest_version": "1.3.15",
+            "latest_app_version": "2025-07-30",
+            "latest_human_version": "2025-07-30_1.3.15",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "metube",
+            "recommended": false,
+            "title": "MeTube",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "youtube-dl",
+                "yt-dlp"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/metube/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/alexta69/metube"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/metube/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "MeTube runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "metube",
+                    "uid": 568,
+                    "user_name": "metube"
+                }
+            ]
+        },
+        "drawio": {
+            "app_readme": "<h1>Draw.io</h1> <p><a href=\"https://www.drawio.com\">Draw.io</a> is a whiteboarding / diagramming software application.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Draw.io is a whiteboarding / diagramming software application.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.drawio.com",
+            "location": "/__w/apps/apps/trains/community/drawio",
+            "latest_version": "1.3.12",
+            "latest_app_version": "28.1.2",
+            "latest_human_version": "28.1.2_1.3.12",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "drawio",
+            "recommended": false,
+            "title": "Draw.io",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "diagram",
+                "whiteboard"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/drawio/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/drawio/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/drawio/screenshots/screenshot3.png"
+            ],
+            "sources": [
+                "https://hub.docker.com/r/jgraph/drawio",
+                "https://github.com/jgraph/drawio"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/drawio/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Draw.io runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "drawio",
+                    "uid": 999,
+                    "user_name": "drawio"
+                }
+            ]
+        },
+        "jelu": {
+            "app_readme": "<h1>Jelu</h1> <p><a href=\"https://github.com/bayang/jelu\">Jelu</a> is a self hosted read and to-read list book tracker.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Jelu is a self hosted read and to-read list book tracker",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/bayang/jelu",
+            "location": "/__w/apps/apps/trains/community/jelu",
+            "latest_version": "1.0.34",
+            "latest_app_version": "0.72.2",
+            "latest_human_version": "0.72.2_1.0.34",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "jelu",
+            "recommended": false,
+            "title": "Jelu",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "book"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/jelu/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/jelu/screenshots/screenshot2.png"
+            ],
+            "sources": [
+                "https://github.com/bayang/jelu",
+                "https://hub.docker.com/repository/docker/wabayang/jelu"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/jelu/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Jelu runs as root user.",
+                    "gid": 0,
+                    "group_name": "jelu",
+                    "uid": 0,
+                    "user_name": "jelu"
+                }
+            ]
+        },
+        "forgejo": {
+            "app_readme": "<h1>Forgejo</h1> <p><a href=\"https://forgejo.org/\">Forgejo</a> is a self-hosted lightweight software forge.</p> <p>On initial startup a setup wizard will be launched with settings for <code>database</code>, <code>ports</code>, <code>path</code>, and <code>domain</code> prefilled. Keep them as they are, fill anything you want in the optional settings section and click on <code>Install Forgejo</code>.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "Forgejo is a self-hosted lightweight software forge. Easy to install and low maintenance, it just does the job.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://forgejo.org/",
+            "location": "/__w/apps/apps/trains/community/forgejo",
+            "latest_version": "1.0.20",
+            "latest_app_version": "12.0.2",
+            "latest_human_version": "12.0.2_1.0.20",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "forgejo",
+            "recommended": false,
+            "title": "Forgejo",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "git",
+                "forgejo",
+                "source control"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://forgejo.org/",
+                "https://codeberg.org/forgejo/forgejo"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/forgejo/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Forgejo runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "forgejo",
+                    "uid": 568,
+                    "user_name": "forgejo"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                }
+            ]
+        },
+        "organizr": {
+            "app_readme": "<h1>Organizr</h1> <p><a href=\"https://docs.organizr.app/\">Organizr</a> is a HTPC/Homelab Services Organizer - Written in PHP</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Organizr is a HTPC/Homelab Services Organizer - Written in PHP",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/causefx/Organizr",
+            "location": "/__w/apps/apps/trains/community/organizr",
+            "latest_version": "1.2.6",
+            "latest_app_version": "latest",
+            "latest_human_version": "latest_1.2.6",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "organizr",
+            "recommended": false,
+            "title": "Organizr",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "dashboard",
+                "organizr"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/organizr/screenshots/screenshot1.webp",
+                "https://media.sys.truenas.net/apps/organizr/screenshots/screenshot2.webp"
+            ],
+            "sources": [
+                "https://hub.docker.com/r/organizr/organizr",
+                "https://github.com/causefx/Organizr"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/organizr/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Organizr is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Organizr is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Organizr is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Organizr is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Organizr is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Organizr runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "pairdrop": {
+            "app_readme": "<h1>PairDrop</h1> <p><a href=\"https://github.com/schlagmichdoch/PairDrop\">PairDrop</a> transfer Files Cross-Platform. No Setup, No Signup.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "Transfer Files Cross-Platform. No Setup, No Signup.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://pairdrop.net/",
+            "location": "/__w/apps/apps/trains/community/pairdrop",
+            "latest_version": "1.0.3",
+            "latest_app_version": "v1.11.2",
+            "latest_human_version": "v1.11.2_1.0.3",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "pairdrop",
+            "recommended": false,
+            "title": "PairDrop",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "file transfer"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/schlagmichdoch/PairDrop"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/pairdrop/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "PairDrop runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "pairdrop",
+                    "uid": 568,
+                    "user_name": "pairdrop"
+                }
+            ]
+        },
+        "zigbee2mqtt": {
+            "app_readme": "<h1>Zigbee2mqtt</h1> <p><a href=\"www.zigbee2mqtt.io\">Zigbee \ud83d\udc1d to MQTT</a> bridge \ud83c\udf09, get rid of your proprietary Zigbee bridges \ud83d\udd28</p>",
+            "categories": [
+                "home-automation"
+            ],
+            "description": "Zigbee to MQTT bridge get rid of your proprietary Zigbee bridges",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.zigbee2mqtt.io",
+            "location": "/__w/apps/apps/trains/community/zigbee2mqtt",
+            "latest_version": "1.0.38",
+            "latest_app_version": "2.6.1",
+            "latest_human_version": "2.6.1_1.0.38",
+            "last_update": "2025-09-02 15:29:51",
+            "name": "zigbee2mqtt",
+            "recommended": false,
+            "title": "Zigbee2MQTT",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "zigbee",
+                "mqtt",
+                "bridge"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/zigbee2mqtt/screenshots/screenshot1.png"
+            ],
+            "sources": [
+                "https://github.com/Koenkk/zigbee2mqtt"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/zigbee2mqtt/icons/icon.svg",
+            "capabilities": [
+                {
+                    "description": "Zigbee2MQTT is able to bind to privileged ports (< 1024)",
+                    "name": "NET_BIND_SERVICE"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Zigbee2MQTT runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "zigbee2mqtt",
+                    "uid": 568,
+                    "user_name": "zigbee2mqtt"
+                }
+            ]
+        },
+        "factorio": {
+            "app_readme": "<h1>Factorio</h1> <p><a href=\"https://factorio.com/\">Factorio</a> headless server in a Docker container</p>",
+            "categories": [
+                "games"
+            ],
+            "description": "Factorio headless server in a Docker container",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://factorio.com/",
+            "location": "/__w/apps/apps/trains/community/factorio",
+            "latest_version": "1.0.6",
+            "latest_app_version": "stable-2.0.66",
+            "latest_human_version": "stable-2.0.66_1.0.6",
+            "last_update": "2025-09-05 14:25:56",
+            "name": "factorio",
+            "recommended": false,
+            "title": "Factorio Server",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "games",
+                "server",
+                "factorio"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/factoriotools/factorio-docker",
+                "https://hub.docker.com/r/factoriotools/factorio/"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/factorio/icons/icon.png",
+            "capabilities": [
+                {
+                    "description": "Factorio is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "Factorio is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "Factorio is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "Factorio is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "Factorio is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Factorio runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "jdownloader2": {
+            "app_readme": "<h1>JDownloader2</h1> <p><a href=\"https://jdownloader.org/\">JDownloader2</a> is a free, open-source download management tool with a huge community that makes downloading as easy and fast as it should be.</p>",
+            "categories": [
+                "media"
+            ],
+            "description": "JDownloader is a free, open-source download management tool with a huge community that makes downloading as easy and fast as it should be.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/jaymoulin/docker-jdownloader",
+            "location": "/__w/apps/apps/trains/community/jdownloader2",
+            "latest_version": "1.0.9",
+            "latest_app_version": "2.1.2",
+            "latest_human_version": "2.1.2_1.0.9",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "jdownloader2",
+            "recommended": false,
+            "title": "JDownloader2",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "media",
+                "download",
+                "files"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/jaymoulin/docker-jdownloader"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/jdownloader2/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "JDownloader2 runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "jdownloader2",
+                    "uid": 568,
+                    "user_name": "jdownloader2"
+                }
+            ]
+        },
+        "newt": {
+            "app_readme": "<h1>Newt</h1> <p><a href=\"https://github.com/fosrl/newt\">Newt</a> - Newt is a fully user space WireGuard tunnel client and TCP/UDP proxy, designed to securely expose private resources controlled by Pangolin. By using Newt, you don't need to manage complex WireGuard tunnels and NATing.</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "Newt is a fully user space WireGuard tunnel client and TCP/UDP proxy, designed to securely expose private resources controlled by Pangolin. By using Newt, you don't need to manage complex WireGuard tunnels and NATing.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/fosrl/newt",
+            "location": "/__w/apps/apps/trains/community/newt",
+            "latest_version": "1.0.17",
+            "latest_app_version": "1.4.4",
+            "latest_human_version": "1.4.4_1.0.17",
+            "last_update": "2025-09-03 12:56:43",
+            "name": "newt",
+            "recommended": false,
+            "title": "Newt",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "tunneling",
+                "proxy",
+                "wireguard",
+                "pangolin"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/fosrl/newt",
+                "https://hub.docker.com/r/fosrl/newt"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/newt/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Newt runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "newt",
+                    "uid": 568,
+                    "user_name": "newt"
+                }
+            ]
+        },
+        "monerod": {
+            "app_readme": "<h1>Monero</h1> <p><a href=\"https://www.getmonero.org/\">Monero</a> is a private, decentralized cryptocurrency that keeps your finances confidential and secure.</p>",
+            "categories": [
+                "financial"
+            ],
+            "description": "Monero is a private, decentralized cryptocurrency that keeps your finances confidential and secure.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.getmonero.org",
+            "location": "/__w/apps/apps/trains/community/monerod",
+            "latest_version": "1.1.11",
+            "latest_app_version": "v0.18.4.2",
+            "latest_human_version": "v0.18.4.2_1.1.11",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "monerod",
+            "recommended": false,
+            "title": "Monero Node",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "monero",
+                "cryptocurrency",
+                "blockchain",
+                "privacy"
+            ],
+            "screenshots": [],
+            "sources": [
+                "https://www.getmonero.org",
+                "https://github.com/sethforprivacy/simple-monerod-docker"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/monerod/icons/icon.png",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Monero runs as non-root user.",
+                    "gid": 1000,
+                    "group_name": "monero",
+                    "uid": 1000,
+                    "user_name": "monero"
+                }
+            ]
+        }
+    },
+    "test": {
+        "nginx": {
+            "app_readme": "<h1>Nginx</h1> <p>It is a test app</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "Nginx description",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/AdguardTeam/AdGuardHome",
+            "location": "/__w/apps/apps/trains/test/nginx",
+            "latest_version": "1.0.8",
+            "latest_app_version": "v1",
+            "latest_human_version": "v1_1.0.8",
+            "last_update": "2025-04-09 14:26:39",
+            "name": "nginx",
+            "recommended": false,
+            "title": "Nginx",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/AdguardTeam/AdGuardHome",
+                "https://hub.docker.com/r/adguard/adguardhome"
+            ],
+            "icon_url": "https://seeklogo.com/images/N/nginx-logo-B38DADE410-seeklogo.com.png",
+            "capabilities": [
+                {
+                    "description": "Just here as an example",
+                    "name": "NET_RAW"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Test app.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "nextcloud": {
+            "app_readme": "<h1>Nextcloud</h1> <p><a href=\"https://nextcloud.com/\">Nextcloud</a> is a file sharing server that puts the control and security of your own data back into your hands.</p>",
+            "categories": [
+                "productivity"
+            ],
+            "description": "A file sharing server that puts the control and security of your own data back into your hands.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://nextcloud.com/",
+            "location": "/__w/apps/apps/trains/test/nextcloud",
+            "latest_version": "1.0.56",
+            "latest_app_version": "31.0.8-fpm-954edb5c",
+            "latest_human_version": "31.0.8-fpm-954edb5c_1.0.56",
+            "last_update": "2025-09-05 14:25:56",
+            "name": "nextcloud",
+            "recommended": false,
+            "title": "Nextcloud",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [
+                "nextcloud",
+                "storage",
+                "sync",
+                "http",
+                "web",
+                "php"
+            ],
+            "screenshots": [
+                "https://media.sys.truenas.net/apps/nextcloud/screenshots/screenshot1.png",
+                "https://media.sys.truenas.net/apps/nextcloud/screenshots/screenshot2.png",
+                "https://media.sys.truenas.net/apps/nextcloud/screenshots/screenshot3.png"
+            ],
+            "sources": [
+                "https://github.com/nextcloud/docker",
+                "https://github.com/truenas/containers/tree/master/apps/nextcloud-fpm",
+                "https://hub.docker.com/r/ixsystems/nextcloud-fpm",
+                "https://github.com/truenas/containers/tree/master/apps/nextcloud-notify-push",
+                "https://hub.docker.com/r/ixsystems/nextcloud-notify-push"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/nextcloud/icons/icon.svg",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Nextcloud runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "nextcloud",
+                    "uid": 568,
+                    "user_name": "nextcloud"
+                },
+                {
+                    "description": "Nextcloud Cron runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "nextcloud",
+                    "uid": 568,
+                    "user_name": "nextcloud"
+                },
+                {
+                    "description": "Nextcloud Notify Push runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "nextcloud",
+                    "uid": 568,
+                    "user_name": "nextcloud"
+                },
+                {
+                    "description": "Postgres runs as non-root user.",
+                    "gid": 999,
+                    "group_name": "postgres",
+                    "uid": 999,
+                    "user_name": "postgres"
+                },
+                {
+                    "description": "Redis runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "redis",
+                    "uid": 568,
+                    "user_name": "redis"
+                },
+                {
+                    "description": "Nginx runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "nginx",
+                    "uid": 568,
+                    "user_name": "nginx"
+                },
+                {
+                    "description": "Imaginary runs as any non-root user.",
+                    "gid": 568,
+                    "group_name": "imaginary",
+                    "uid": 568,
+                    "user_name": "imaginary"
+                }
+            ]
+        },
+        "other-nginx": {
+            "app_readme": "<h1>Nginx</h1> <p>It is a test app</p>",
+            "categories": [
+                "networking"
+            ],
+            "description": "Nginx description",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/AdguardTeam/AdGuardHome",
+            "location": "/__w/apps/apps/trains/test/other-nginx",
+            "latest_version": "1.0.3",
+            "latest_app_version": "v1",
+            "latest_human_version": "v1_1.0.3",
+            "last_update": "2025-04-09 14:26:39",
+            "name": "other-nginx",
+            "recommended": false,
+            "title": "Other Nginx",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/AdguardTeam/AdGuardHome",
+                "https://hub.docker.com/r/adguard/adguardhome"
+            ],
+            "icon_url": "https://seeklogo.com/images/N/nginx-logo-B38DADE410-seeklogo.com.png",
+            "capabilities": [
+                {
+                    "description": "Just here as an example",
+                    "name": "NET_RAW"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Test app.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        }
+    },
+    "dev": {
+        "truenas-webui": {
+            "app_readme": "<h1>Truenas WebUI</h1>",
+            "categories": [
+                "development"
+            ],
+            "description": "TrueNAS WebUI DEVELOPMENT ONLY",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://www.truenas.com/",
+            "location": "/__w/apps/apps/trains/dev/truenas-webui",
+            "latest_version": "1.0.14",
+            "latest_app_version": "latest",
+            "latest_human_version": "latest_1.0.14",
+            "last_update": "2025-09-02 11:33:24",
+            "name": "truenas-webui",
+            "recommended": false,
+            "title": "TrueNAS WebUI",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/truenas/webui"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/ix-chart/icons/icon.webp",
+            "capabilities": [
+                {
+                    "description": "TrueNAS WebUI is able to change file ownership arbitrarily",
+                    "name": "CHOWN"
+                },
+                {
+                    "description": "TrueNAS WebUI is able to bypass file permission checks",
+                    "name": "DAC_OVERRIDE"
+                },
+                {
+                    "description": "TrueNAS WebUI is able to bypass permission checks for file operations",
+                    "name": "FOWNER"
+                },
+                {
+                    "description": "TrueNAS WebUI is able to bind to privileged ports (< 1024)",
+                    "name": "NET_BIND_SERVICE"
+                },
+                {
+                    "description": "TrueNAS WebUI is able to change group ID of processes",
+                    "name": "SETGID"
+                },
+                {
+                    "description": "TrueNAS WebUI is able to change user ID of processes",
+                    "name": "SETUID"
+                }
+            ],
+            "run_as_context": [
+                {
+                    "description": "Truenas WebUI runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        },
+        "docker-socket-proxy": {
+            "app_readme": "<h1>Docker Socket Proxy</h1> <p><a href=\"https://github.com/Tecnativa/docker-socket-proxy\">Docker Socket Proxy</a> is a security-enhanced proxy for the Docker Socket.</p>",
+            "categories": [
+                "development"
+            ],
+            "description": "Docker Socket Proxy is a security-enhanced proxy for the Docker Socket.",
+            "healthy": true,
+            "healthy_error": null,
+            "home": "https://github.com/Tecnativa/docker-socket-proxy",
+            "location": "/__w/apps/apps/trains/dev/docker-socket-proxy",
+            "latest_version": "1.0.6",
+            "latest_app_version": "v0.4.1",
+            "latest_human_version": "v0.4.1_1.0.6",
+            "last_update": "2025-09-05 14:25:56",
+            "name": "docker-socket-proxy",
+            "recommended": false,
+            "title": "Docker Socket Proxy",
+            "maintainers": [
+                {
+                    "email": "dev@ixsystems.com",
+                    "name": "truenas",
+                    "url": "https://www.truenas.com/"
+                }
+            ],
+            "tags": [],
+            "screenshots": [],
+            "sources": [
+                "https://github.com/Tecnativa/docker-socket-proxy"
+            ],
+            "icon_url": "https://media.sys.truenas.net/apps/ix-chart/icons/icon.webp",
+            "capabilities": [],
+            "run_as_context": [
+                {
+                    "description": "Docker Socket Proxy runs as root user.",
+                    "gid": 0,
+                    "group_name": "root",
+                    "uid": 0,
+                    "user_name": "root"
+                }
+            ]
+        }
+    }
+}

+ 456 - 0
cspell.config.yaml

@@ -0,0 +1,456 @@
+words:
+  - 2fauth
+  - adguard
+  - adventurelog
+  - airtable
+  - aliyun
+  - allinkl
+  - altran
+  - anythingllm
+  - apikey
+  - arcadiatechnology
+  - archisteamfarm
+  - arti
+  - asigra
+  - audiobook
+  - audiobookshelf
+  - authelia
+  - authentik
+  - autobrr
+  - automaticrippingmachine
+  - autoupdate
+  - backblaze
+  - baserow
+  - bazarr
+  - bindir
+  - binwiederhier
+  - bitcoind
+  - bitmagnet
+  - bitmonero
+  - bitnami
+  - blakeblackshear
+  - blocklist
+  - briefkasten
+  - bwlimit
+  - byparr
+  - c4illin
+  - caddyfile
+  - castopod
+  - changedetection
+  - changeip
+  - chatwoot
+  - chrislusf
+  - chunksize
+  - cifs
+  - clamav
+  - clamd
+  - cloudbeaver
+  - cloudflared
+  - codegate
+  - collabora
+  - concourse
+  - consts
+  - containerd
+  - convertx
+  - cooldown
+  - cpus
+  - creds
+  - crontasks
+  - cuda
+  - cyfershepard
+  - datasource
+  - datasources
+  - dawarich
+  - dbeaver
+  - dbus
+  - ddns
+  - ddnss
+  - desec
+  - dgtlmoon
+  - dialout
+  - directus
+  - diskover
+  - diskoverdata
+  - dnsmasq
+  - dnsomatic
+  - dnspod
+  - dockge
+  - documentserver
+  - domeneshop
+  - dominio
+  - dondominio
+  - dozzle
+  - drawio
+  - dreamhost
+  - duckdns
+  - duplicati
+  - dxflrs
+  - dynu
+  - dynv
+  - easydns
+  - electrs
+  - elfhosted
+  - elif
+  - emby
+  - endfor
+  - endmacro
+  - endswith
+  - esphome
+  - factorio
+  - familytree
+  - fancybits
+  - fangtooth
+  - filebrowser
+  - filesize
+  - filestash
+  - fireflyiii
+  - fireshare
+  - flaresolverr
+  - forgejo
+  - fowner
+  - freedns
+  - freikin
+  - freshclamd
+  - freshrss
+  - fscrawler
+  - ftlconf
+  - ftpd
+  - funcs
+  - gandi
+  - gaseousgames
+  - gdrive
+  - gensalt
+  - getmeili
+  - gitea
+  - godaddy
+  - goip
+  - goipde
+  - gotenberg
+  - gotify
+  - graalvm
+  - grampsdb
+  - grampsweb
+  - grocy
+  - gunicorn
+  - harverster
+  - hashpw
+  - headscale
+  - healthcheck
+  - healthchecks
+  - healthz
+  - hetzner
+  - hexparrot
+  - hmac
+  - homarr
+  - homebox
+  - hoppscotch
+  - hostable
+  - htauth
+  - htpasswd
+  - htsp
+  - icanhazip
+  - iconik
+  - igdb
+  - immich
+  - initdb
+  - inkl
+  - invoiceninja
+  - inwx
+  - ionos
+  - ipaddr
+  - ipam
+  - ipfs
+  - ipify
+  - ipinfo
+  - ipleak
+  - isconfigured
+  - isready
+  - itzg
+  - ixsystems
+  - jackett
+  - jaymoulin
+  - jdownloader
+  - jellyfin
+  - jellyseerr
+  - jellystat
+  - jelu
+  - jenkins
+  - jesec
+  - jlesage
+  - jorenn
+  - kapowarr
+  - karakeep
+  - kasm
+  - kavita
+  - keepalive
+  - kemal
+  - keyfile
+  - keyframes
+  - kimai
+  - kitchenowl
+  - kiwix
+  - knowledgebase
+  - komga
+  - komodo
+  - kuma
+  - lancache
+  - lancachenet
+  - laravel
+  - lazylibrarian
+  - libsmbclient
+  - lidarr
+  - linkding
+  - linkwarden
+  - listmonk
+  - livetv
+  - loglevel
+  - logseq
+  - loopia
+  - louislam
+  - luadns
+  - luanti
+  - lyrion
+  - magicgrants
+  - maintainerr
+  - makemkv
+  - mangas
+  - manynotes
+  - matplotlib
+  - mebibytes
+  - meili
+  - meilisearch
+  - metasearch
+  - metube
+  - microcontrollers
+  - milterd
+  - mineos
+  - minio
+  - mjpg
+  - monero
+  - monerod
+  - monitee
+  - mosquitto
+  - mostafawahied
+  - mqtt
+  - mqueue
+  - msgbyte
+  - msgmax
+  - msgmnb
+  - msgmni
+  - myaddr
+  - namecheap
+  - namecom
+  - nameserver
+  - namesilo
+  - navidrome
+  - netboot
+  - netbootxyz
+  - netcat
+  - netcup
+  - netdata
+  - nextauth
+  - nextcloud
+  - nextpvr
+  - njalla
+  - nnev
+  - nocodb
+  - nocopy
+  - noip
+  - notifiarr
+  - nowdns
+  - ntfy
+  - nzbget
+  - ocrmypdf
+  - octoprint
+  - odoo
+  - ollama
+  - omada
+  - onlyoffice
+  - opcache
+  - openai
+  - opendns
+  - openj
+  - openspeedtest
+  - openvino
+  - organizr
+  - overseerr
+  - pairdrop
+  - palworld
+  - passbolt
+  - passcode
+  - pecl
+  - penpot
+  - perfmon
+  - pgadmin
+  - pgdata
+  - pgrep
+  - pgvecto
+  - pgvector
+  - pgvectors
+  - phantomjs
+  - photoprism
+  - photoview
+  - pigallery
+  - pihole
+  - pipefail
+  - piwigo
+  - planka
+  - plankanban
+  - plexinc
+  - plexpass
+  - podcasters
+  - porkbun
+  - portracker
+  - postgis
+  - predeploy
+  - profilarr
+  - prowlarr
+  - proxied
+  - proxying
+  - publicip
+  - publicipv
+  - pwuser
+  - qbittorrent
+  - quic
+  - radarr
+  - rawio
+  - rcat
+  - rclone
+  - rcon
+  - rdtclient
+  - readarr
+  - realpath
+  - recyclarr
+  - requestrr
+  - restic
+  - rmid
+  - rocm
+  - romm
+  - rommapp
+  - roundcube
+  - rprivate
+  - rshared
+  - rslave
+  - rstrip
+  - rsyncd
+  - rtmp
+  - rtsp
+  - rtty
+  - rustdesk
+  - ryshe
+  - sabnzbd
+  - scandir
+  - scrollbars
+  - scrypted
+  - searxng
+  - seaweedfs
+  - seccomp
+  - seeip
+  - selfhost
+  - selfhost.de
+  - selfhostde
+  - servercow
+  - setfcap
+  - setgid
+  - sethforprivacy
+  - setpcap
+  - setuid
+  - sftpd
+  - sftpgo
+  - shaneisrael
+  - shlink
+  - shmall
+  - shmmax
+  - shmmni
+  - shoko
+  - shokoanime
+  - shoutrrr
+  - sickgear
+  - sidekiq
+  - sigdb
+  - signup
+  - smbclient
+  - snappdf
+  - socketdir
+  - sonarr
+  - spdyn
+  - spotnet
+  - spottar
+  - spottarr
+  - stacklok
+  - startpage
+  - startswith
+  - stashapp
+  - storagenode
+  - storjlabs
+  - strato
+  - subquestions
+  - syncthing
+  - sysadminsmedia
+  - sysctls
+  - tailscale
+  - tailscaled
+  - tautulli
+  - tdarr
+  - teamspeak
+  - templating
+  - tensorchord
+  - tensorrt
+  - tesseract
+  - tftpboot
+  - tftpd
+  - tianji
+  - tika
+  - tmdb
+  - tmpfs
+  - tojson
+  - traccar
+  - tracebacklimit
+  - truenas
+  - tshock
+  - tubearchivist
+  - tvheadend
+  - twofactor
+  - twofauth
+  - typesense
+  - umami
+  - unifi
+  - unmanic
+  - unpackerr
+  - urbackup
+  - userspace
+  - utmp
+  - valkey
+  - variomedia
+  - vaultwarden
+  - vectorchord
+  - versity
+  - versitygw
+  - vhost
+  - vikunja
+  - wabayang
+  - warracker
+  - webapi
+  - webauthn
+  - webdav
+  - webdavd
+  - webrtc
+  - webui
+  - wger
+  - whisparr
+  - whiteboarding
+  - whoogle
+  - wiki-js
+  - wizarr
+  - woodpeckerci
+  - wtfismyip
+  - wyze
+  - xattr
+  - zenika
+  - zerotier
+  - zigbee
+  - zipline
+  - zksync
+  - zoneedit
+  - zoraxy
+  - zwave

+ 23 - 0
devbox.json

@@ -0,0 +1,23 @@
+{
+  "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.13.7/.schema/devbox.schema.json",
+  "packages": [
+    "python@3.11",
+    "python3Packages.pyyaml",
+    "python3Packages.pytest",
+    "python3Packages.pytest-cov",
+    "python3Packages.bcrypt",
+    "python3Packages.pydantic",
+    "git@latest"
+  ],
+  "shell": {
+    "init_hook": [],
+    "scripts": {
+      "ports": ["python3 ./.github/scripts/port_validation.py"],
+      "lib-test": [
+        "pytest library/ -vvv",
+        "rm -r library/**/__pycache__",
+        "rm -r library/**/tests/__pycache__"
+      ]
+    }
+  }
+}

+ 208 - 0
devbox.lock

@@ -0,0 +1,208 @@
+{
+  "lockfile_version": "1",
+  "packages": {
+    "git@latest": {
+      "last_modified": "2025-07-28T17:09:23Z",
+      "resolved": "github:NixOS/nixpkgs/648f70160c03151bc2121d179291337ad6bc564b#git",
+      "source": "devbox-search",
+      "version": "2.50.1",
+      "systems": {
+        "aarch64-darwin": {
+          "outputs": [
+            {
+              "name": "out",
+              "path": "/nix/store/jn9byxgdjndngf0d2by0djg8gcdll7xc-git-2.50.1",
+              "default": true
+            },
+            {
+              "name": "doc",
+              "path": "/nix/store/j8djmq64ckbah7bl6jv1y6arrjr0shmv-git-2.50.1-doc"
+            }
+          ],
+          "store_path": "/nix/store/jn9byxgdjndngf0d2by0djg8gcdll7xc-git-2.50.1"
+        },
+        "aarch64-linux": {
+          "outputs": [
+            {
+              "name": "out",
+              "path": "/nix/store/h4pvvix6pvnvys9a6y1xj2442r1ajdhl-git-2.50.1",
+              "default": true
+            },
+            {
+              "name": "doc",
+              "path": "/nix/store/q8sicpx16gyzxnp3345a46lj4cz9wd09-git-2.50.1-doc"
+            },
+            {
+              "name": "debug",
+              "path": "/nix/store/rpxnrnsn4nbx8wm9d2vrgj0fr5xzz5lg-git-2.50.1-debug"
+            }
+          ],
+          "store_path": "/nix/store/h4pvvix6pvnvys9a6y1xj2442r1ajdhl-git-2.50.1"
+        },
+        "x86_64-darwin": {
+          "outputs": [
+            {
+              "name": "out",
+              "path": "/nix/store/8d1n8cvi5x1j0v61459lvhqs26vmcqbl-git-2.50.1",
+              "default": true
+            },
+            {
+              "name": "doc",
+              "path": "/nix/store/yn9cvbs7jz4dfdb17qralgr0ybi5rmjf-git-2.50.1-doc"
+            }
+          ],
+          "store_path": "/nix/store/8d1n8cvi5x1j0v61459lvhqs26vmcqbl-git-2.50.1"
+        },
+        "x86_64-linux": {
+          "outputs": [
+            {
+              "name": "out",
+              "path": "/nix/store/5i8zvall945kypmwgqd0y47f02pldwp4-git-2.50.1",
+              "default": true
+            },
+            {
+              "name": "debug",
+              "path": "/nix/store/l46kpjpcwwp8l7kzzr1s2dlk646r73z2-git-2.50.1-debug"
+            },
+            {
+              "name": "doc",
+              "path": "/nix/store/d2lhlzkdziwmijik8nszfwp8srbkskb9-git-2.50.1-doc"
+            }
+          ],
+          "store_path": "/nix/store/5i8zvall945kypmwgqd0y47f02pldwp4-git-2.50.1"
+        }
+      }
+    },
+    "github:NixOS/nixpkgs/nixpkgs-unstable": {
+      "last_modified": "2025-08-19T04:17:39Z",
+      "resolved": "github:NixOS/nixpkgs/97eb7ee0da337d385ab015a23e15022c865be75c?lastModified=1755577059&narHash=sha256-5hYhxIpco8xR%2BIpP3uU56%2B4%2BBw7mf7EMyxS%2FHqUYHQY%3D"
+    },
+    "python3Packages.bcrypt": {
+      "resolved": "github:NixOS/nixpkgs/75a52265bda7fd25e06e3a67dee3f0354e73243c#python3Packages.bcrypt",
+      "source": "nixpkg",
+      "systems": {
+        "aarch64-darwin": {
+          "outputs": [
+            {
+              "path": "/nix/store/0bialsv3mzc3hfkynkky2h4g0fkhl27b-python3.11-bcrypt-4.0.1",
+              "default": true
+            }
+          ]
+        }
+      }
+    },
+    "python3Packages.pydantic": {
+      "resolved": "github:NixOS/nixpkgs/75a52265bda7fd25e06e3a67dee3f0354e73243c#python3Packages.pydantic",
+      "source": "nixpkg",
+      "systems": {
+        "aarch64-darwin": {
+          "outputs": [
+            {
+              "path": "/nix/store/lmhr8skgkpbf8y9frfcfhyd1cwbp7d35-python3.11-pydantic-1.10.12",
+              "default": true
+            }
+          ]
+        }
+      }
+    },
+    "python3Packages.pytest": {
+      "resolved": "github:NixOS/nixpkgs/75a52265bda7fd25e06e3a67dee3f0354e73243c#python3Packages.pytest",
+      "source": "nixpkg",
+      "systems": {
+        "aarch64-darwin": {
+          "outputs": [
+            {
+              "path": "/nix/store/97pny9bx1msg7nlj5zgvj7abfyyx76rg-python3.11-pytest-7.4.2",
+              "default": true
+            }
+          ]
+        }
+      }
+    },
+    "python3Packages.pytest-cov": {
+      "resolved": "github:NixOS/nixpkgs/75a52265bda7fd25e06e3a67dee3f0354e73243c#python3Packages.pytest-cov",
+      "source": "nixpkg",
+      "systems": {
+        "aarch64-darwin": {
+          "outputs": [
+            {
+              "path": "/nix/store/a2j6csrpmnzfp2shblk00z3b4b6jdgax-python3.11-pytest-cov-4.1.0",
+              "default": true
+            }
+          ]
+        }
+      }
+    },
+    "python3Packages.pyyaml": {
+      "resolved": "github:NixOS/nixpkgs/75a52265bda7fd25e06e3a67dee3f0354e73243c#python3Packages.pyyaml",
+      "source": "nixpkg",
+      "systems": {
+        "aarch64-darwin": {
+          "outputs": [
+            {
+              "path": "/nix/store/wr951m2xcgnndqjdh7i51dknsmnbxa4f-python3.11-pyyaml-6.0.1",
+              "default": true
+            }
+          ]
+        }
+      }
+    },
+    "python@3.11": {
+      "last_modified": "2025-07-28T17:09:23Z",
+      "plugin_version": "0.0.4",
+      "resolved": "github:NixOS/nixpkgs/648f70160c03151bc2121d179291337ad6bc564b#python311",
+      "source": "devbox-search",
+      "version": "3.11.13",
+      "systems": {
+        "aarch64-darwin": {
+          "outputs": [
+            {
+              "name": "out",
+              "path": "/nix/store/ifxz6hniba9h7p1lcfi8zd4xpslgsy6x-python3-3.11.13",
+              "default": true
+            }
+          ],
+          "store_path": "/nix/store/ifxz6hniba9h7p1lcfi8zd4xpslgsy6x-python3-3.11.13"
+        },
+        "aarch64-linux": {
+          "outputs": [
+            {
+              "name": "out",
+              "path": "/nix/store/9gj7a7mjy47l19xx1sh8zrgh687ilqbs-python3-3.11.13",
+              "default": true
+            },
+            {
+              "name": "debug",
+              "path": "/nix/store/ixsm2j4hkbwbdvx35qyy4lb0hycg0cix-python3-3.11.13-debug"
+            }
+          ],
+          "store_path": "/nix/store/9gj7a7mjy47l19xx1sh8zrgh687ilqbs-python3-3.11.13"
+        },
+        "x86_64-darwin": {
+          "outputs": [
+            {
+              "name": "out",
+              "path": "/nix/store/bg2fr8g676s6cvf6afh7md21bz86j4sv-python3-3.11.13",
+              "default": true
+            }
+          ],
+          "store_path": "/nix/store/bg2fr8g676s6cvf6afh7md21bz86j4sv-python3-3.11.13"
+        },
+        "x86_64-linux": {
+          "outputs": [
+            {
+              "name": "out",
+              "path": "/nix/store/xldyfac0kbcl5c1yp9ygsag5y23irwxs-python3-3.11.13",
+              "default": true
+            },
+            {
+              "name": "debug",
+              "path": "/nix/store/5ay9f62pyshj15dxfpvw40wa8w6pzwbc-python3-3.11.13-debug"
+            }
+          ],
+          "store_path": "/nix/store/xldyfac0kbcl5c1yp9ygsag5y23irwxs-python3-3.11.13"
+        }
+      }
+    }
+  }
+}

+ 30 - 0
features_capability.json

@@ -0,0 +1,30 @@
+{
+  "normalize/acl": {
+    "stable": {"min": "24.10-ALPHA"},
+    "nightlies": {"min": "24.10-MASTER-somever"}
+  },
+  "normalize/ix_volume": {
+    "stable": {"min": "24.10-ALPHA"},
+    "nightlies": {"min": "24.10-MASTER-somever"}
+  },
+  "definitions/node_bind_ip": {
+    "stable": {"min": "24.10-ALPHA"},
+    "nightlies": {"min": "24.10-MASTER-somever"}
+  },
+  "definitions/timezone": {
+    "stable": {"min": "24.10-ALPHA"},
+    "nightlies": {"min": "24.10-MASTER"}
+  },
+  "definitions/certificate": {
+    "stable": {"min": "24.10-ALPHA"},
+    "nightlies": {"min": "24.10-MASTER"}
+  },
+  "definitions/certificate_authority": {
+    "stable": {"min": "24.10-ALPHA", "max": "25.04"},
+    "nightlies": {"min": "24.10-MASTER", "max": "25.04-MASTER"}
+  },
+  "definitions/port": {
+    "stable": {"min": "24.10-ALPHA"},
+    "nightlies": {"min": "24.10-MASTER"}
+  }
+}

+ 3 - 0
ix-dev/community/actual-budget/README.md

@@ -0,0 +1,3 @@
+# Actual Budget
+
+[Actual Budget](https://actualbudget.org/) is a super fast and privacy-focused app for managing your finances.

+ 39 - 0
ix-dev/community/actual-budget/app.yaml

@@ -0,0 +1,39 @@
+annotations:
+  min_scale_version: 24.10.2.2
+app_version: 25.9.0
+capabilities: []
+categories:
+- financial
+changelog_url: https://actualbudget.org/docs/releases
+date_added: '2024-08-06'
+description: Actual Budget is a super fast and privacy-focused app for managing your
+  finances.
+home: https://actualbudget.org
+host_mounts: []
+icon: https://media.sys.truenas.net/apps/actual-budget/icons/icon.png
+keywords:
+- finance
+- budget
+lib_version: 2.1.49
+lib_version_hash: e71e6b0122c9446fa5ea6fb07e7eb01b11fb42d549a19845426bbd7e21a42634
+maintainers:
+- email: dev@ixsystems.com
+  name: truenas
+  url: https://www.truenas.com/
+name: actual-budget
+run_as_context:
+- description: Actual Budget runs as any non-root user.
+  gid: 568
+  group_name: actual
+  uid: 568
+  user_name: actual
+screenshots:
+- https://media.sys.truenas.net/apps/actual-budget/screenshots/screenshot1.png
+- https://media.sys.truenas.net/apps/actual-budget/screenshots/screenshot2.png
+- https://media.sys.truenas.net/apps/actual-budget/screenshots/screenshot3.png
+sources:
+- https://github.com/actualbudget/actual-server
+- https://hub.docker.com/r/actualbudget/actual-server
+title: Actual Budget
+train: community
+version: 1.3.12

+ 6 - 0
ix-dev/community/actual-budget/app_migrations.yaml

@@ -0,0 +1,6 @@
+migrations:
+- file: ip_port_migration
+  from:
+    max_version: 1.2.20
+  target:
+    min_version: 1.3.0

+ 10 - 0
ix-dev/community/actual-budget/item.yaml

@@ -0,0 +1,10 @@
+categories:
+- financial
+icon_url: https://media.sys.truenas.net/apps/actual-budget/icons/icon.png
+screenshots:
+- https://media.sys.truenas.net/apps/actual-budget/screenshots/screenshot1.png
+- https://media.sys.truenas.net/apps/actual-budget/screenshots/screenshot2.png
+- https://media.sys.truenas.net/apps/actual-budget/screenshots/screenshot3.png
+tags:
+- finance
+- budget

+ 10 - 0
ix-dev/community/actual-budget/ix_values.yaml

@@ -0,0 +1,10 @@
+images:
+  image:
+    repository: actualbudget/actual-server
+    tag: 25.9.0
+consts:
+  actual_budget_container_name: actual-budget
+  perms_container_name: permissions
+  base_data_path: /data
+  ssl_key_path: /certs/tls.key
+  ssl_cert_path: /certs/tls.crt

+ 23 - 0
ix-dev/community/actual-budget/migrations/ip_port_migration

@@ -0,0 +1,23 @@
+#!/usr/bin/python3
+
+import os
+import sys
+import yaml
+
+
+def migrate(values):
+    values["network"]["web_port"] = {
+        "port_number": values["network"]["web_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()))))

+ 409 - 0
ix-dev/community/actual-budget/questions.yaml

@@ -0,0 +1,409 @@
+groups:
+  - name: Actual Budget Configuration
+    description: Configure Actual Budget
+  - name: User and Group Configuration
+    description: Configure User and Group for Actual Budget
+  - name: Network Configuration
+    description: Configure Network for Actual Budget
+  - name: Storage Configuration
+    description: Configure Storage for Actual Budget
+  - name: Labels Configuration
+    description: Configure Labels for Actual Budget
+  - name: Resources Configuration
+    description: Configure Resources for Actual Budget
+
+questions:
+  - variable: actual_budget
+    label: ""
+    group: Actual Budget Configuration
+    schema:
+      type: dict
+      attrs:
+        - variable: additional_envs
+          label: Additional Environment Variables
+          description: Configure additional environment variables for Actual Budget.
+          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
+  - variable: run_as
+    label: ""
+    group: User and Group Configuration
+    schema:
+      type: dict
+      attrs:
+        - variable: user
+          label: User ID
+          description: The user id that Actual Budget files will be owned by.
+          schema:
+            type: int
+            min: 568
+            default: 568
+            required: true
+        - variable: group
+          label: Group ID
+          description: The group id that Actual Budget files will be owned by.
+          schema:
+            type: int
+            min: 568
+            default: 568
+            required: true
+
+  - variable: network
+    label: ""
+    group: Network Configuration
+    schema:
+      type: dict
+      attrs:
+        - variable: web_port
+          label: WebUI Port
+          schema:
+            type: dict
+            attrs:
+              - variable: bind_mode
+                label: Port Bind Mode
+                description: |
+                  The port bind mode.</br>
+                  - Publish: The port will be published on the host for external access.</br>
+                  - Expose: The port will be exposed for inter-container communication.</br>
+                  - None: The port will not be exposed or published.</br>
+                  Note: If the Dockerfile defines an EXPOSE directive,
+                  the port will still be exposed for inter-container communication regardless of this setting.
+                schema:
+                  type: string
+                  default: "published"
+                  enum:
+                    - value: "published"
+                      description: Publish port on the host for external access
+                    - value: "exposed"
+                      description: Expose port for inter-container communication
+                    - value: ""
+                      description: None
+              - variable: port_number
+                label: Port Number
+                schema:
+                  type: int
+                  default: 31012
+                  min: 1
+                  max: 65535
+                  required: true
+              - variable: host_ips
+                label: Host IPs
+                description: IPs on the host to bind this port
+                schema:
+                  type: list
+                  show_if: [["bind_mode", "=", "published"]]
+                  default: []
+                  items:
+                    - variable: host_ip
+                      label: Host IP
+                      schema:
+                        type: string
+                        required: true
+                        $ref:
+                          - definitions/node_bind_ip
+        - variable: host_network
+          label: Host Network
+          description: |
+            Bind to the host network. It's recommended to keep this disabled.
+          schema:
+            type: boolean
+            default: false
+        - variable: certificate_id
+          label: Certificate
+          description: The certificate to use for Portainer.
+          schema:
+            type: int
+            "null": true
+            $ref:
+              - "definitions/certificate"
+  - variable: storage
+    label: ""
+    group: Storage Configuration
+    schema:
+      type: dict
+      attrs:
+        - variable: data
+          label: Actual Budget Data Storage
+          description: The path to store Actual Budget Data.
+          schema:
+            type: dict
+            attrs:
+              - variable: type
+                label: Type
+                description: |
+                  ixVolume: Is dataset created automatically by the system.</br>
+                  Host Path: Is a path that already exists on the system.
+                schema:
+                  type: string
+                  required: true
+                  default: "ix_volume"
+                  enum:
+                    - value: "host_path"
+                      description: Host Path (Path that already exists on the system)
+                    - value: "ix_volume"
+                      description: ixVolume (Dataset created automatically by the system)
+              - variable: ix_volume_config
+                label: ixVolume Configuration
+                description: The configuration for the ixVolume dataset.
+                schema:
+                  type: dict
+                  show_if: [["type", "=", "ix_volume"]]
+                  $ref:
+                    - "normalize/ix_volume"
+                  attrs:
+                    - variable: acl_enable
+                      label: Enable ACL
+                      description: Enable ACL for the storage.
+                      schema:
+                        type: boolean
+                        default: false
+                    - variable: dataset_name
+                      label: Dataset Name
+                      description: The name of the dataset to use for storage.
+                      schema:
+                        type: string
+                        required: true
+                        hidden: true
+                        default: "data"
+                    - variable: acl_entries
+                      label: ACL Configuration
+                      schema:
+                        type: dict
+                        show_if: [["acl_enable", "=", true]]
+                        attrs: []
+              - variable: host_path_config
+                label: Host Path Configuration
+                schema:
+                  type: dict
+                  show_if: [["type", "=", "host_path"]]
+                  attrs:
+                    - variable: acl_enable
+                      label: Enable ACL
+                      description: Enable ACL for the storage.
+                      schema:
+                        type: boolean
+                        default: false
+                    - variable: acl
+                      label: ACL Configuration
+                      schema:
+                        type: dict
+                        show_if: [["acl_enable", "=", true]]
+                        attrs: []
+                        $ref:
+                          - "normalize/acl"
+                    - variable: path
+                      label: Host Path
+                      description: The host path to use for storage.
+                      schema:
+                        type: hostpath
+                        show_if: [["acl_enable", "=", false]]
+                        required: true
+        - variable: additional_storage
+          label: Additional Storage
+          schema:
+            type: list
+            default: []
+            items:
+              - variable: storageEntry
+                label: Storage Entry
+                schema:
+                  type: dict
+                  attrs:
+                    - variable: type
+                      label: Type
+                      description: |
+                        ixVolume: Is dataset created automatically by the system.</br>
+                        Host Path: Is a path that already exists on the system.</br>
+                        SMB Share: Is a SMB share that is mounted to as a volume.
+                      schema:
+                        type: string
+                        required: true
+                        default: "ix_volume"
+                        enum:
+                          - value: "host_path"
+                            description: Host Path (Path that already exists on the system)
+                          - value: "ix_volume"
+                            description: ixVolume (Dataset created automatically by the system)
+                          - value: "cifs"
+                            description: SMB/CIFS Share (Mounts a volume to a SMB share)
+                    - variable: read_only
+                      label: Read Only
+                      description: Mount the volume as read only.
+                      schema:
+                        type: boolean
+                        default: false
+                    - variable: mount_path
+                      label: Mount Path
+                      description: The path inside the container to mount the storage.
+                      schema:
+                        type: path
+                        required: true
+                    - variable: host_path_config
+                      label: Host Path Configuration
+                      schema:
+                        type: dict
+                        show_if: [["type", "=", "host_path"]]
+                        attrs:
+                          - variable: acl_enable
+                            label: Enable ACL
+                            description: Enable ACL for the storage.
+                            schema:
+                              type: boolean
+                              default: false
+                          - variable: acl
+                            label: ACL Configuration
+                            schema:
+                              type: dict
+                              show_if: [["acl_enable", "=", true]]
+                              attrs: []
+                              $ref:
+                                - "normalize/acl"
+                          - variable: path
+                            label: Host Path
+                            description: The host path to use for storage.
+                            schema:
+                              type: hostpath
+                              show_if: [["acl_enable", "=", false]]
+                              required: true
+                    - variable: ix_volume_config
+                      label: ixVolume Configuration
+                      description: The configuration for the ixVolume dataset.
+                      schema:
+                        type: dict
+                        show_if: [["type", "=", "ix_volume"]]
+                        $ref:
+                          - "normalize/ix_volume"
+                        attrs:
+                          - variable: acl_enable
+                            label: Enable ACL
+                            description: Enable ACL for the storage.
+                            schema:
+                              type: boolean
+                              default: false
+                          - variable: dataset_name
+                            label: Dataset Name
+                            description: The name of the dataset to use for storage.
+                            schema:
+                              type: string
+                              required: true
+                              default: "storage_entry"
+                          - variable: acl_entries
+                            label: ACL Configuration
+                            schema:
+                              type: dict
+                              show_if: [["acl_enable", "=", true]]
+                              attrs: []
+                    - variable: cifs_config
+                      label: SMB Configuration
+                      description: The configuration for the SMB dataset.
+                      schema:
+                        type: dict
+                        show_if: [["type", "=", "cifs"]]
+                        attrs:
+                          - variable: server
+                            label: Server
+                            description: The server to mount the SMB share.
+                            schema:
+                              type: string
+                              required: true
+                          - variable: path
+                            label: Path
+                            description: The path to mount the SMB share.
+                            schema:
+                              type: string
+                              required: true
+                          - variable: username
+                            label: Username
+                            description: The username to use for the SMB share.
+                            schema:
+                              type: string
+                              required: true
+                          - variable: password
+                            label: Password
+                            description: The password to use for the SMB share.
+                            schema:
+                              type: string
+                              required: true
+                              private: true
+                          - variable: domain
+                            label: Domain
+                            description: The domain to use for the SMB share.
+                            schema:
+                              type: string
+  - variable: labels
+    label: ""
+    group: Labels Configuration
+    schema:
+      type: list
+      default: []
+      items:
+        - variable: label
+          label: Label
+          schema:
+            type: dict
+            attrs:
+              - variable: key
+                label: Key
+                schema:
+                  type: string
+                  required: true
+              - variable: value
+                label: Value
+                schema:
+                  type: string
+                  required: true
+              - variable: containers
+                label: Containers
+                description: Containers where the label should be applied
+                schema:
+                  type: list
+                  items:
+                    - variable: container
+                      label: Container
+                      schema:
+                        type: string
+                        required: true
+                        enum:
+                          - value: actual-budget
+                            description: actual-budget
+
+  - variable: resources
+    label: ""
+    group: Resources Configuration
+    schema:
+      type: dict
+      attrs:
+        - variable: limits
+          label: Limits
+          schema:
+            type: dict
+            attrs:
+              - variable: cpus
+                label: CPUs
+                description: CPUs limit for Actual Budget.
+                schema:
+                  type: int
+                  default: 2
+                  required: true
+              - variable: memory
+                label: Memory (in MB)
+                description: Memory limit for Actual Budget.
+                schema:
+                  type: int
+                  default: 4096
+                  required: true

+ 42 - 0
ix-dev/community/actual-budget/templates/docker-compose.yaml

@@ -0,0 +1,42 @@
+{% set tpl = ix_lib.base.render.Render(values) %}
+
+{% set c1 = tpl.add_container(values.consts.actual_budget_container_name, "image") %}
+{% set perm_container = tpl.deps.perms(values.consts.perms_container_name) %}
+{% set perms_config = {"uid": values.run_as.user, "gid": values.run_as.group, "mode": "check"} %}
+
+{% do c1.set_user(values.run_as.user, values.run_as.group) %}
+{% do c1.healthcheck.set_custom_test("NPM_CONFIG_UPDATE_NOTIFIER=false NODE_TLS_REJECT_UNAUTHORIZED=0 node /app/src/scripts/health-check.js") %}
+
+{% do c1.environment.add_env("ACTUAL_PORT", values.network.web_port.port_number) %}
+{% do c1.environment.add_env("ACTUAL_HOSTNAME", "0.0.0.0") %}
+{% do c1.environment.add_env("ACTUAL_SERVER_FILES", "%s/server-files" | format(values.consts.base_data_path)) %}
+{% do c1.environment.add_env("ACTUAL_USER_FILES", "%s/user-files" | format(values.consts.base_data_path)) %}
+{% do c1.environment.add_env("NODE_ENV", "production") %}
+{% if values.network.certificate_id %}
+  {% do c1.environment.add_env("ACTUAL_HTTPS_KEY", values.consts.ssl_key_path) %}
+  {% do c1.environment.add_env("ACTUAL_HTTPS_CERT", values.consts.ssl_cert_path) %}
+
+  {% set cert = values.ix_certificates[values.network.certificate_id] %}
+  {% do c1.configs.add("private", cert.privatekey, values.consts.ssl_key_path) %}
+  {% do c1.configs.add("public", cert.certificate, values.consts.ssl_cert_path) %}
+{% endif %}
+
+{% do c1.environment.add_user_envs(values.actual_budget.additional_envs) %}
+{% do c1.add_port(values.network.web_port) %}
+
+{% do c1.add_storage("/data", values.storage.data) %}
+{% do perm_container.add_or_skip_action("data", values.storage.data, perms_config) %}
+
+{% for store in values.storage.additional_storage %}
+  {% do c1.add_storage(store.mount_path, store) %}
+  {% do perm_container.add_or_skip_action(store.mount_path, store, perms_config) %}
+{% endfor %}
+
+{% if perm_container.has_actions() %}
+  {% do perm_container.activate() %}
+  {% do c1.depends.add_dependency(values.consts.perms_container_name, "service_completed_successfully") %}
+{% endif %}
+
+{% do tpl.portals.add(values.network.web_port, {"scheme": "https" if values.network.certificate_id else "http"}) %}
+
+{{ tpl.render() | tojson }}

+ 0 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/__init__.py


+ 70 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/client.py

@@ -0,0 +1,70 @@
+import os
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+
+try:
+    from .error import RenderError
+except ImportError:
+    from error import RenderError
+
+
+def is_truenas_system():
+    """Check if we're running on a TrueNAS system"""
+    return "truenas" in os.uname().release
+
+
+# Import based on system detection
+if is_truenas_system():
+    from truenas_api_client import Client as TrueNASClient
+
+    try:
+        # 25.04 and later
+        from truenas_api_client.exc import ValidationErrors
+    except ImportError:
+        # 24.10 and earlier
+        from truenas_api_client import ValidationErrors
+else:
+    # Mock classes for non-TrueNAS systems
+    class TrueNASClient:
+        def call(self, *args, **kwargs):
+            return None
+
+    class ValidationErrors(Exception):
+        def __init__(self, errors):
+            self.errors = errors
+
+
+class Client:
+    def __init__(self, render_instance: "Render"):
+        self.client = TrueNASClient()
+        self._render_instance = render_instance
+        self._app_name: str = self._render_instance.values.get("ix_context", {}).get("app_name", "") or "unknown"
+
+    def validate_ip_port_combo(self, ip: str, port: int) -> None:
+        # Example of an error messages:
+        # The port is being used by following services: 1) "0.0.0.0:80" used by WebUI Service
+        # The port is being used by following services: 1) "0.0.0.0:9998" used by Applications ('$app_name' application)
+        try:
+            self.client.call("port.validate_port", f"render.{self._app_name}.schema", port, ip, None, True)
+        except ValidationErrors as e:
+            err_str = str(e)
+            # If the IP:port combo appears more than once in the error message,
+            # means that the port is used by more than one service/app.
+            # This shouldn't happen in a well-configured system.
+            # Notice that the ip portion is not included check,
+            # because input might be a specific IP, but another service or app
+            # might be using the same port on a wildcard IP
+            if err_str.count(f':{port}" used by') > 1:
+                raise RenderError(err_str) from None
+
+            # If the error complains about the current app, we ignore it
+            # This is to handle cases where the app is being updated or edited
+            if f"Applications ('{self._app_name}' application)" in err_str:
+                # During upgrade, we want to ignore the error if it is related to the current app
+                return
+
+            raise RenderError(err_str) from None
+        except Exception:
+            pass

+ 86 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/configs.py

@@ -0,0 +1,86 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+
+try:
+    from .error import RenderError
+    from .formatter import escape_dollar
+    from .validations import valid_octal_mode_or_raise, valid_fs_path_or_raise
+except ImportError:
+    from error import RenderError
+    from formatter import escape_dollar
+    from validations import valid_octal_mode_or_raise, valid_fs_path_or_raise
+
+
+class Configs:
+    def __init__(self, render_instance: "Render"):
+        self._render_instance = render_instance
+        self._configs: dict[str, dict] = {}
+
+    def add(self, name: str, data: str):
+        if not isinstance(data, str):
+            raise RenderError(f"Expected [data] to be a string, got [{type(data)}]")
+
+        if name not in self._configs:
+            self._configs[name] = {"name": name, "data": data}
+            return
+
+        if data == self._configs[name]["data"]:
+            return
+
+        raise RenderError(f"Config [{name}] already added with different data")
+
+    def has_configs(self):
+        return bool(self._configs)
+
+    def render(self):
+        return {
+            c["name"]: {"content": escape_dollar(c["data"])}
+            for c in sorted(self._configs.values(), key=lambda c: c["name"])
+        }
+
+
+class ContainerConfigs:
+    def __init__(self, render_instance: "Render", configs: Configs):
+        self._render_instance = render_instance
+        self.top_level_configs: Configs = configs
+        self.container_configs: set[ContainerConfig] = set()
+
+    def add(self, name: str, data: str, target: str, mode: str = ""):
+        self.top_level_configs.add(name, data)
+
+        if target == "":
+            raise RenderError(f"Expected [target] to be set for config [{name}]")
+        if mode != "":
+            mode = valid_octal_mode_or_raise(mode)
+
+        if target in [c.target for c in self.container_configs]:
+            raise RenderError(f"Target [{target}] already used for another config")
+        target = valid_fs_path_or_raise(target)
+        self.container_configs.add(ContainerConfig(self._render_instance, name, target, mode))
+
+    def has_configs(self):
+        return bool(self.container_configs)
+
+    def render(self):
+        return [c.render() for c in sorted(self.container_configs, key=lambda c: c.source)]
+
+
+class ContainerConfig:
+    def __init__(self, render_instance: "Render", source: str, target: str, mode: str):
+        self._render_instance = render_instance
+        self.source = source
+        self.target = target
+        self.mode = mode
+
+    def render(self):
+        result: dict[str, str | int] = {
+            "source": self.source,
+            "target": self.target,
+        }
+
+        if self.mode:
+            result["mode"] = int(self.mode, 8)
+
+        return result

+ 450 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/container.py

@@ -0,0 +1,450 @@
+from typing import Any, TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+    from storage import IxStorage
+
+try:
+    from .configs import ContainerConfigs
+    from .depends import Depends
+    from .deploy import Deploy
+    from .device_cgroup_rules import DeviceCGroupRules
+    from .devices import Devices
+    from .dns import Dns
+    from .environment import Environment
+    from .error import RenderError
+    from .expose import Expose
+    from .extra_hosts import ExtraHosts
+    from .formatter import escape_dollar, get_image_with_hashed_data
+    from .healthcheck import Healthcheck
+    from .labels import Labels
+    from .ports import Ports
+    from .restart import RestartPolicy
+    from .tmpfs import Tmpfs
+    from .validations import (
+        valid_cap_or_raise,
+        valid_cgroup_or_raise,
+        valid_ipc_mode_or_raise,
+        valid_network_mode_or_raise,
+        valid_pid_mode_or_raise,
+        valid_port_bind_mode_or_raise,
+        valid_port_mode_or_raise,
+        valid_pull_policy_or_raise,
+    )
+    from .security_opts import SecurityOpts
+    from .storage import Storage
+    from .sysctls import Sysctls
+except ImportError:
+    from configs import ContainerConfigs
+    from depends import Depends
+    from deploy import Deploy
+    from device_cgroup_rules import DeviceCGroupRules
+    from devices import Devices
+    from dns import Dns
+    from environment import Environment
+    from error import RenderError
+    from expose import Expose
+    from extra_hosts import ExtraHosts
+    from formatter import escape_dollar, get_image_with_hashed_data
+    from healthcheck import Healthcheck
+    from labels import Labels
+    from ports import Ports
+    from restart import RestartPolicy
+    from tmpfs import Tmpfs
+    from validations import (
+        valid_cap_or_raise,
+        valid_cgroup_or_raise,
+        valid_ipc_mode_or_raise,
+        valid_network_mode_or_raise,
+        valid_pid_mode_or_raise,
+        valid_port_bind_mode_or_raise,
+        valid_port_mode_or_raise,
+        valid_pull_policy_or_raise,
+    )
+    from security_opts import SecurityOpts
+    from storage import Storage
+    from sysctls import Sysctls
+
+
+class Container:
+    def __init__(self, render_instance: "Render", name: str, image: str):
+        self._render_instance = render_instance
+
+        self._name: str = name
+        self._image: str = self._resolve_image(image)
+        self._build_image: str = ""
+        self._pull_policy: str = ""
+        self._user: str = ""
+        self._tty: bool = False
+        self._stdin_open: bool = False
+        self._init: bool | None = None
+        self._read_only: bool | None = None
+        self._extra_hosts: ExtraHosts = ExtraHosts(self._render_instance)
+        self._hostname: str = ""
+        self._cap_drop: set[str] = set(["ALL"])  # Drop all capabilities by default and add caps granularly
+        self._cap_add: set[str] = set()
+        self._security_opt: SecurityOpts = SecurityOpts(self._render_instance)
+        self._privileged: bool = False
+        self._group_add: set[int | str] = set()
+        self._network_mode: str = ""
+        self._entrypoint: list[str] = []
+        self._command: list[str] = []
+        self._grace_period: int | None = None
+        self._shm_size: int | None = None
+        self._storage: Storage = Storage(self._render_instance, self)
+        self._tmpfs: Tmpfs = Tmpfs(self._render_instance, self)
+        self._ipc_mode: str | None = None
+        self._pid_mode: str | None = None
+        self._cgroup: str | None = None
+        self._device_cgroup_rules: DeviceCGroupRules = DeviceCGroupRules(self._render_instance)
+        self.sysctls: Sysctls = Sysctls(self._render_instance, self)
+        self.configs: ContainerConfigs = ContainerConfigs(self._render_instance, self._render_instance.configs)
+        self.deploy: Deploy = Deploy(self._render_instance)
+        self.networks: set[str] = set()
+        self.devices: Devices = Devices(self._render_instance)
+        self.environment: Environment = Environment(self._render_instance, self.deploy.resources)
+        self.dns: Dns = Dns(self._render_instance)
+        self.depends: Depends = Depends(self._render_instance)
+        self.healthcheck: Healthcheck = Healthcheck(self._render_instance)
+        self.labels: Labels = Labels(self._render_instance)
+        self.restart: RestartPolicy = RestartPolicy(self._render_instance)
+        self.ports: Ports = Ports(self._render_instance)
+        self.expose: Expose = Expose(self._render_instance)
+
+        self._auto_set_network_mode()
+        self._auto_add_labels()
+        self._auto_add_groups()
+
+    def _auto_add_groups(self):
+        self.add_group(568)
+
+    def _auto_set_network_mode(self):
+        if self._render_instance.values.get("network", {}).get("host_network", False):
+            self.set_network_mode("host")
+
+    def _auto_add_labels(self):
+        labels = self._render_instance.values.get("labels", [])
+        if not labels:
+            return
+
+        for label in labels:
+            containers = label.get("containers", [])
+            if not containers:
+                raise RenderError(f'Label [{label.get("key", "")}] must have at least one container')
+
+            if self._name in containers:
+                self.labels.add_label(label["key"], label["value"])
+
+    def _resolve_image(self, image: str):
+        images = self._render_instance.values["images"]
+        if image not in images:
+            raise RenderError(
+                f"Image [{image}] not found in values. " f"Available images: [{', '.join(images.keys())}]"
+            )
+        repo = images[image].get("repository", "")
+        tag = images[image].get("tag", "")
+
+        if not repo:
+            raise RenderError(f"Repository not found for image [{image}]")
+        if not tag:
+            raise RenderError(f"Tag not found for image [{image}]")
+
+        return f"{repo}:{tag}"
+
+    def build_image(self, content: list[str | None]):
+        dockerfile = f"FROM {self._image}\n"
+        for line in content:
+            line = line.strip() if line else ""
+            if not line:
+                continue
+            if line.startswith("FROM"):
+                # TODO: This will also block multi-stage builds
+                # We can revisit this later if we need it
+                raise RenderError(
+                    "FROM cannot be used in build image. Define the base image when creating the container."
+                )
+            dockerfile += line + "\n"
+
+        self._build_image = dockerfile
+        self._image = get_image_with_hashed_data(self._image, dockerfile)
+
+    def set_pull_policy(self, pull_policy: str):
+        self._pull_policy = valid_pull_policy_or_raise(pull_policy)
+
+    def set_user(self, user: int, group: int):
+        for i in (user, group):
+            if not isinstance(i, int) or i < 0:
+                raise RenderError(f"User/Group [{i}] is not valid")
+        self._user = f"{user}:{group}"
+
+    def add_extra_host(self, host: str, ip: str):
+        self._extra_hosts.add_host(host, ip)
+
+    def add_group(self, group: int | str):
+        if isinstance(group, str):
+            group = str(group).strip()
+            if group.isdigit():
+                raise RenderError(f"Group is a number [{group}] but passed as a string")
+
+        if group in self._group_add:
+            raise RenderError(f"Group [{group}] already added")
+        self._group_add.add(group)
+
+    def get_additional_groups(self) -> list[int | str]:
+        result = []
+        if self.deploy.resources.has_gpus() or self.devices.has_gpus():
+            result.append(44)  # video
+            result.append(107)  # render
+        return result
+
+    def get_current_groups(self) -> list[str]:
+        result = [str(g) for g in self._group_add]
+        result.extend([str(g) for g in self.get_additional_groups()])
+        return result
+
+    def set_tty(self, enabled: bool = False):
+        self._tty = enabled
+
+    def set_stdin(self, enabled: bool = False):
+        self._stdin_open = enabled
+
+    def set_ipc_mode(self, ipc_mode: str):
+        self._ipc_mode = valid_ipc_mode_or_raise(ipc_mode, self._render_instance.container_names())
+
+    def set_pid_mode(self, mode: str = ""):
+        self._pid_mode = valid_pid_mode_or_raise(mode, self._render_instance.container_names())
+
+    def add_device_cgroup_rule(self, dev_grp_rule: str):
+        self._device_cgroup_rules.add_rule(dev_grp_rule)
+
+    def set_cgroup(self, cgroup: str):
+        self._cgroup = valid_cgroup_or_raise(cgroup)
+
+    def set_init(self, enabled: bool = False):
+        self._init = enabled
+
+    def set_read_only(self, enabled: bool = False):
+        self._read_only = enabled
+
+    def set_hostname(self, hostname: str):
+        self._hostname = hostname
+
+    def set_grace_period(self, grace_period: int):
+        if grace_period < 0:
+            raise RenderError(f"Grace period [{grace_period}] cannot be negative")
+        self._grace_period = grace_period
+
+    def set_privileged(self, enabled: bool = False):
+        self._privileged = enabled
+
+    def clear_caps(self):
+        self._cap_add.clear()
+        self._cap_drop.clear()
+
+    def add_caps(self, caps: list[str]):
+        for c in caps:
+            if c in self._cap_add:
+                raise RenderError(f"Capability [{c}] already added")
+            self._cap_add.add(valid_cap_or_raise(c))
+
+    def add_security_opt(self, key: str, value: str | bool | None = None, arg: str | None = None):
+        self._security_opt.add_opt(key, value, arg)
+
+    def remove_security_opt(self, key: str):
+        self._security_opt.remove_opt(key)
+
+    def set_network_mode(self, mode: str):
+        self._network_mode = valid_network_mode_or_raise(mode, self._render_instance.container_names())
+
+    def add_port(self, port_config: dict | None = None, dev_config: dict | None = None):
+        port_config = port_config or {}
+        dev_config = dev_config or {}
+        # Merge port_config and dev_config (dev_config has precedence)
+        config = port_config | dev_config
+        bind_mode = valid_port_bind_mode_or_raise(config.get("bind_mode", ""))
+        # Skip port if its neither published nor exposed
+        if not bind_mode:
+            return
+
+        # Collect port config
+        mode = valid_port_mode_or_raise(config.get("mode", "ingress"))
+        host_port = config.get("port_number", 0)
+        container_port = config.get("container_port", 0) or host_port
+        protocol = config.get("protocol", "tcp")
+        host_ips = config.get("host_ips") or ["0.0.0.0", "::"]
+        if not isinstance(host_ips, list):
+            raise RenderError(f"Expected [host_ips] to be a list, got [{host_ips}]")
+
+        if bind_mode == "published":
+            for host_ip in host_ips:
+                self.ports._add_port(
+                    host_port, container_port, {"protocol": protocol, "host_ip": host_ip, "mode": mode}
+                )
+        elif bind_mode == "exposed":
+            self.expose.add_port(container_port, protocol)
+
+    def set_entrypoint(self, entrypoint: list[str]):
+        self._entrypoint = [escape_dollar(str(e)) for e in entrypoint]
+
+    def set_command(self, command: list[str]):
+        self._command = [escape_dollar(str(e)) for e in command]
+
+    def add_storage(self, mount_path: str, config: "IxStorage"):
+        if config.get("type", "") == "tmpfs":
+            self._tmpfs.add(mount_path, config)
+        else:
+            self._storage.add(mount_path, config)
+
+    def add_docker_socket(self, read_only: bool = True, mount_path: str = "/var/run/docker.sock"):
+        self.add_group(999)
+        self._storage._add_docker_socket(read_only, mount_path)
+
+    def add_udev(self, read_only: bool = True, mount_path: str = "/run/udev"):
+        self._storage._add_udev(read_only, mount_path)
+
+    def add_tun_device(self):
+        self.devices._add_tun_device()
+
+    def add_snd_device(self):
+        self.add_group(29)
+        self.devices._add_snd_device()
+
+    def set_shm_size_mb(self, size: int):
+        self._shm_size = size
+
+    # Easily remove devices from the container
+    # Useful in dependencies like postgres and redis
+    # where there is no need to pass devices to them
+    def remove_devices(self):
+        self.deploy.resources.remove_devices()
+        self.devices.remove_devices()
+
+    @property
+    def storage(self):
+        return self._storage
+
+    def render(self) -> dict[str, Any]:
+        if self._network_mode and self.networks:
+            raise RenderError("Cannot set both [network_mode] and [networks]")
+
+        result = {
+            "image": self._image,
+            "platform": "linux/amd64",
+            "tty": self._tty,
+            "stdin_open": self._stdin_open,
+            "restart": self.restart.render(),
+        }
+
+        if self._pull_policy:
+            result["pull_policy"] = self._pull_policy
+
+        if self.healthcheck.has_healthcheck():
+            result["healthcheck"] = self.healthcheck.render()
+
+        if self._hostname:
+            result["hostname"] = self._hostname
+
+        if self._build_image:
+            result["build"] = {"tags": [self._image], "dockerfile_inline": self._build_image}
+
+        if self.configs.has_configs():
+            result["configs"] = self.configs.render()
+
+        if self._ipc_mode is not None:
+            result["ipc"] = self._ipc_mode
+
+        if self._pid_mode is not None:
+            result["pid"] = self._pid_mode
+
+        if self._device_cgroup_rules.has_rules():
+            result["device_cgroup_rules"] = self._device_cgroup_rules.render()
+
+        if self._cgroup is not None:
+            result["cgroup"] = self._cgroup
+
+        if self._extra_hosts.has_hosts():
+            result["extra_hosts"] = self._extra_hosts.render()
+
+        if self._init is not None:
+            result["init"] = self._init
+
+        if self._read_only is not None:
+            result["read_only"] = self._read_only
+
+        if self._grace_period is not None:
+            result["stop_grace_period"] = f"{self._grace_period}s"
+
+        if self._user:
+            result["user"] = self._user
+
+        for g in self.get_additional_groups():
+            self.add_group(g)
+
+        if self._group_add:
+            result["group_add"] = sorted(self._group_add, key=lambda g: (isinstance(g, str), g))
+
+        if self._shm_size is not None:
+            result["shm_size"] = f"{self._shm_size}M"
+
+        if self._privileged is not None:
+            result["privileged"] = self._privileged
+
+        if self._cap_drop:
+            result["cap_drop"] = sorted(self._cap_drop)
+
+        if self._cap_add:
+            result["cap_add"] = sorted(self._cap_add)
+
+        if self._security_opt.has_opts():
+            result["security_opt"] = self._security_opt.render()
+
+        if self._network_mode:
+            result["network_mode"] = self._network_mode
+
+        if self.sysctls.has_sysctls():
+            result["sysctls"] = self.sysctls.render()
+
+        if self._network_mode != "host":
+            if self.ports.has_ports():
+                result["ports"] = self.ports.render()
+
+            if self.expose.has_ports():
+                result["expose"] = self.expose.render()
+
+        if self._entrypoint:
+            result["entrypoint"] = self._entrypoint
+
+        if self._command:
+            result["command"] = self._command
+
+        if self.devices.has_devices():
+            result["devices"] = self.devices.render()
+
+        if self.deploy.has_deploy():
+            result["deploy"] = self.deploy.render()
+
+        if self.environment.has_variables():
+            result["environment"] = self.environment.render()
+
+        if self.labels.has_labels():
+            result["labels"] = self.labels.render()
+
+        if self.dns.has_dns_nameservers():
+            result["dns"] = self.dns.render_dns_nameservers()
+
+        if self.dns.has_dns_searches():
+            result["dns_search"] = self.dns.render_dns_searches()
+
+        if self.dns.has_dns_opts():
+            result["dns_opt"] = self.dns.render_dns_opts()
+
+        if self.depends.has_dependencies():
+            result["depends_on"] = self.depends.render()
+
+        if self._storage.has_mounts():
+            result["volumes"] = self._storage.render()
+
+        if self._tmpfs.has_tmpfs():
+            result["tmpfs"] = self._tmpfs.render()
+
+        return result

+ 34 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/depends.py

@@ -0,0 +1,34 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+
+try:
+    from .error import RenderError
+    from .validations import valid_depend_condition_or_raise
+except ImportError:
+    from error import RenderError
+    from validations import valid_depend_condition_or_raise
+
+
+class Depends:
+    def __init__(self, render_instance: "Render"):
+        self._render_instance = render_instance
+        self._dependencies: dict[str, str] = {}
+
+    def add_dependency(self, name: str, condition: str):
+        condition = valid_depend_condition_or_raise(condition)
+        if name in self._dependencies.keys():
+            raise RenderError(f"Dependency [{name}] already added")
+        if name not in self._render_instance.container_names():
+            raise RenderError(
+                f"Dependency [{name}] not found in defined containers. "
+                f"Available containers: [{', '.join(self._render_instance.container_names())}]"
+            )
+        self._dependencies[name] = condition
+
+    def has_dependencies(self):
+        return len(self._dependencies) > 0
+
+    def render(self):
+        return {d: {"condition": c} for d, c in self._dependencies.items()}

+ 24 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/deploy.py

@@ -0,0 +1,24 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+
+try:
+    from .resources import Resources
+except ImportError:
+    from resources import Resources
+
+
+class Deploy:
+    def __init__(self, render_instance: "Render"):
+        self._render_instance = render_instance
+        self.resources: Resources = Resources(self._render_instance)
+
+    def has_deploy(self):
+        return self.resources.has_resources()
+
+    def render(self):
+        if self.resources.has_resources():
+            return {"resources": self.resources.render()}
+
+        return {}

+ 47 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/deps.py

@@ -0,0 +1,47 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+
+try:
+    from .deps_elastic import ElasticSearchContainer, ElasticConfig
+    from .deps_mariadb import MariadbContainer, MariadbConfig
+    from .deps_meilisearch import MeilisearchContainer, MeiliConfig
+    from .deps_mongodb import MongoDBContainer, MongoDBConfig
+    from .deps_perms import PermsContainer
+    from .deps_postgres import PostgresContainer, PostgresConfig
+    from .deps_redis import RedisContainer, RedisConfig
+except ImportError:
+    from deps_elastic import ElasticSearchContainer, ElasticConfig
+    from deps_mariadb import MariadbContainer, MariadbConfig
+    from deps_meilisearch import MeilisearchContainer, MeiliConfig
+    from deps_mongodb import MongoDBContainer, MongoDBConfig
+    from deps_perms import PermsContainer
+    from deps_postgres import PostgresContainer, PostgresConfig
+    from deps_redis import RedisContainer, RedisConfig
+
+
+class Deps:
+    def __init__(self, render_instance: "Render"):
+        self._render_instance = render_instance
+
+    def perms(self, name: str):
+        return PermsContainer(self._render_instance, name)
+
+    def postgres(self, name: str, image: str, config: PostgresConfig, perms_instance: PermsContainer):
+        return PostgresContainer(self._render_instance, name, image, config, perms_instance)
+
+    def redis(self, name: str, image: str, config: RedisConfig, perms_instance: PermsContainer):
+        return RedisContainer(self._render_instance, name, image, config, perms_instance)
+
+    def mariadb(self, name: str, image: str, config: MariadbConfig, perms_instance: PermsContainer):
+        return MariadbContainer(self._render_instance, name, image, config, perms_instance)
+
+    def mongodb(self, name: str, image: str, config: MongoDBConfig, perms_instance: PermsContainer):
+        return MongoDBContainer(self._render_instance, name, image, config, perms_instance)
+
+    def meilisearch(self, name: str, image: str, config: MeiliConfig, perms_instance: PermsContainer):
+        return MeilisearchContainer(self._render_instance, name, image, config, perms_instance)
+
+    def elasticsearch(self, name: str, image: str, config: ElasticConfig, perms_instance: PermsContainer):
+        return ElasticSearchContainer(self._render_instance, name, image, config, perms_instance)

+ 95 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/deps_elastic.py

@@ -0,0 +1,95 @@
+from typing import TYPE_CHECKING, TypedDict, NotRequired
+
+
+if TYPE_CHECKING:
+    from render import Render
+    from storage import IxStorage
+
+
+try:
+    from .error import RenderError
+    from .deps_perms import PermsContainer
+except ImportError:
+    from error import RenderError
+    from deps_perms import PermsContainer
+
+
+class ElasticConfig(TypedDict):
+    password: str
+    node_name: str
+    port: NotRequired[int]
+    volume: "IxStorage"
+
+
+class ElasticSearchContainer:
+    def __init__(
+        self, render_instance: "Render", name: str, image: str, config: ElasticConfig, perms_instance: PermsContainer
+    ):
+        self._render_instance = render_instance
+        self._name = name
+        self._config = config
+        self._data_dir = "/usr/share/elasticsearch/data"
+
+        for key in ("password", "node_name", "volume"):
+            if key not in config:
+                raise RenderError(f"Expected [{key}] to be set for ElasticSearch")
+
+        c = self._render_instance.add_container(name, image)
+
+        c.set_user(1000, 1000)
+        basic_auth_header = self._render_instance.funcs["basic_auth_header"]("elastic", config["password"])
+        c.healthcheck.set_test(
+            "curl",
+            {
+                "port": self.get_port(),
+                "path": "/_cluster/health?local=true",
+                "headers": [("Authorization", basic_auth_header)],
+            },
+        )
+        c.remove_devices()
+        c.add_storage(self._data_dir, config["volume"])
+
+        c.environment.add_env("ELASTIC_PASSWORD", config["password"])
+        c.environment.add_env("http.port", self.get_port())
+        c.environment.add_env("path.data", self._data_dir)
+        c.environment.add_env("path.repo", self.get_snapshots_dir())
+        c.environment.add_env("node.name", config["node_name"])
+        c.environment.add_env("discovery.type", "single-node")
+        c.environment.add_env("xpack.security.enabled", True)
+        c.environment.add_env("xpack.security.transport.ssl.enabled", False)
+
+        perms_instance.add_or_skip_action(
+            f"{self._name}_elastic_data", config["volume"], {"uid": 1000, "gid": 1000, "mode": "check"}
+        )
+
+        self._get_repo(image, ("docker.elastic.co/elasticsearch/elasticsearch"))
+
+        # Store container for further configuration
+        # For example: c.depends.add_dependency("other_container", "service_started")
+        self._container = c
+
+    @property
+    def container(self):
+        return self._container
+
+    def _get_repo(self, image, supported_repos):
+        images = self._render_instance.values["images"]
+        if image not in images:
+            raise RenderError(f"Image [{image}] not found in values. Available images: [{', '.join(images.keys())}]")
+        repo = images[image].get("repository")
+        if not repo:
+            raise RenderError("Could not determine repo")
+        if repo not in supported_repos:
+            raise RenderError(
+                f"Unsupported repo [{repo}] for elastic search. Supported repos: {', '.join(supported_repos)}"
+            )
+        return repo
+
+    def get_port(self):
+        return self._config.get("port") or 9200
+
+    def get_url(self):
+        return f"http://{self._name}:{self.get_port()}"
+
+    def get_snapshots_dir(self):
+        return f"{self._data_dir}/snapshots"

+ 81 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/deps_mariadb.py

@@ -0,0 +1,81 @@
+from typing import TYPE_CHECKING, TypedDict, NotRequired
+
+if TYPE_CHECKING:
+    from render import Render
+    from storage import IxStorage
+
+try:
+    from .error import RenderError
+    from .deps_perms import PermsContainer
+    from .validations import valid_port_or_raise
+except ImportError:
+    from error import RenderError
+    from deps_perms import PermsContainer
+    from validations import valid_port_or_raise
+
+
+class MariadbConfig(TypedDict):
+    user: str
+    password: str
+    database: str
+    root_password: NotRequired[str]
+    port: NotRequired[int]
+    auto_upgrade: NotRequired[bool]
+    volume: "IxStorage"
+
+
+class MariadbContainer:
+    def __init__(
+        self, render_instance: "Render", name: str, image: str, config: MariadbConfig, perms_instance: PermsContainer
+    ):
+        self._render_instance = render_instance
+        self._name = name
+        self._config = config
+
+        for key in ("user", "password", "database", "volume"):
+            if key not in config:
+                raise RenderError(f"Expected [{key}] to be set for mariadb")
+
+        port = valid_port_or_raise(self.get_port())
+        root_password = config.get("root_password") or config["password"]
+        auto_upgrade = config.get("auto_upgrade", True)
+
+        self._get_repo(image, ("mariadb"))
+        c = self._render_instance.add_container(name, image)
+        c.set_user(999, 999)
+        c.healthcheck.set_test("mariadb", {"password": root_password})
+        c.remove_devices()
+
+        c.add_storage("/var/lib/mysql", config["volume"])
+        perms_instance.add_or_skip_action(
+            f"{self._name}_mariadb_data", config["volume"], {"uid": 999, "gid": 999, "mode": "check"}
+        )
+
+        c.environment.add_env("MARIADB_USER", config["user"])
+        c.environment.add_env("MARIADB_PASSWORD", config["password"])
+        c.environment.add_env("MARIADB_ROOT_PASSWORD", root_password)
+        c.environment.add_env("MARIADB_DATABASE", config["database"])
+        c.environment.add_env("MARIADB_AUTO_UPGRADE", str(auto_upgrade).lower())
+        c.set_command(["--port", str(port)])
+
+        # Store container for further configuration
+        # For example: c.depends.add_dependency("other_container", "service_started")
+        self._container = c
+
+    def _get_repo(self, image, supported_repos):
+        images = self._render_instance.values["images"]
+        if image not in images:
+            raise RenderError(f"Image [{image}] not found in values. Available images: [{', '.join(images.keys())}]")
+        repo = images[image].get("repository")
+        if not repo:
+            raise RenderError("Could not determine repo")
+        if repo not in supported_repos:
+            raise RenderError(f"Unsupported repo [{repo}] for mariadb. Supported repos: {', '.join(supported_repos)}")
+        return repo
+
+    def get_port(self):
+        return self._config.get("port") or 3306
+
+    @property
+    def container(self):
+        return self._container

+ 85 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/deps_meilisearch.py

@@ -0,0 +1,85 @@
+from typing import TYPE_CHECKING, TypedDict, NotRequired
+
+
+if TYPE_CHECKING:
+    from render import Render
+    from storage import IxStorage
+
+
+try:
+    from .error import RenderError
+    from .deps_perms import PermsContainer
+except ImportError:
+    from error import RenderError
+    from deps_perms import PermsContainer
+
+
+class MeiliConfig(TypedDict):
+    master_key: str
+    port: NotRequired[int]
+    volume: "IxStorage"
+
+
+class MeilisearchContainer:
+    def __init__(
+        self, render_instance: "Render", name: str, image: str, config: MeiliConfig, perms_instance: PermsContainer
+    ):
+        self._render_instance = render_instance
+        self._name = name
+        self._config = config
+        self._data_dir = "/meili_data"
+
+        for key in ("master_key", "volume"):
+            if key not in config:
+                raise RenderError(f"Expected [{key}] to be set for meilisearch")
+
+        c = self._render_instance.add_container(name, image)
+
+        user, group = 568, 568
+        run_as = self._render_instance.values.get("run_as")
+        if run_as:
+            user = run_as["user"] or user  # Avoids running as root
+            group = run_as["group"] or group  # Avoids running as root
+
+        c.set_user(user, group)
+        c.healthcheck.set_test("curl", {"port": self.get_port(), "path": "/health"})
+        c.remove_devices()
+        c.add_storage(self._data_dir, config["volume"])
+
+        c.environment.add_env("MEILI_HTTP_ADDR", f"0.0.0.0:{self.get_port()}")
+        c.environment.add_env("MEILI_NO_ANALYTICS", True)
+        c.environment.add_env("MEILI_EXPERIMENTAL_DUMPLESS_UPGRADE", True)
+        c.environment.add_env("MEILI_MASTER_KEY", config["master_key"])
+
+        perms_instance.add_or_skip_action(
+            f"{self._name}_meili_data", config["volume"], {"uid": user, "gid": group, "mode": "check"}
+        )
+
+        self._get_repo(image, ("getmeili/meilisearch",))
+
+        # Store container for further configuration
+        # For example: c.depends.add_dependency("other_container", "service_started")
+        self._container = c
+
+    @property
+    def container(self):
+        return self._container
+
+    def _get_repo(self, image, supported_repos):
+        images = self._render_instance.values["images"]
+        if image not in images:
+            raise RenderError(f"Image [{image}] not found in values. Available images: [{', '.join(images.keys())}]")
+        repo = images[image].get("repository")
+        if not repo:
+            raise RenderError("Could not determine repo")
+        if repo not in supported_repos:
+            raise RenderError(
+                f"Unsupported repo [{repo}] for meilisearch. Supported repos: {', '.join(supported_repos)}"
+            )
+        return repo
+
+    def get_port(self):
+        return self._config.get("port") or 7700
+
+    def get_url(self):
+        return f"http://{self._name}:{self.get_port()}"

+ 91 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/deps_mongodb.py

@@ -0,0 +1,91 @@
+import urllib.parse
+from typing import TYPE_CHECKING, TypedDict
+
+
+if TYPE_CHECKING:
+    from render import Render
+    from storage import IxStorage
+
+
+try:
+    from .error import RenderError
+    from .deps_perms import PermsContainer
+except ImportError:
+    from error import RenderError
+    from deps_perms import PermsContainer
+
+
+class MongoDBConfig(TypedDict):
+    user: str
+    password: str
+    database: str
+    volume: "IxStorage"
+
+
+class MongoDBContainer:
+    def __init__(
+        self, render_instance: "Render", name: str, image: str, config: MongoDBConfig, perms_instance: PermsContainer
+    ):
+        self._render_instance = render_instance
+        self._name = name
+        self._config = config
+        self._data_dir = "/data/db"
+
+        for key in ("user", "password", "database", "volume"):
+            if key not in config:
+                raise RenderError(f"Expected [{key}] to be set for mongodb")
+
+        c = self._render_instance.add_container(name, image)
+
+        c.set_user(999, 999)
+        c.healthcheck.set_test("mongodb", {"db": config["database"]})
+        c.remove_devices()
+        c.add_storage(self._data_dir, config["volume"])
+
+        c.environment.add_env("MONGO_INITDB_ROOT_USERNAME", config["user"])
+        c.environment.add_env("MONGO_INITDB_ROOT_PASSWORD", config["password"])
+        c.environment.add_env("MONGO_INITDB_DATABASE", config["database"])
+
+        perms_instance.add_or_skip_action(
+            f"{self._name}_mongodb_data", config["volume"], {"uid": 999, "gid": 999, "mode": "check"}
+        )
+
+        self._get_repo(image, ("mongodb"))
+
+        # Store container for further configuration
+        # For example: c.depends.add_dependency("other_container", "service_started")
+        self._container = c
+
+    @property
+    def container(self):
+        return self._container
+
+    def _get_repo(self, image, supported_repos):
+        images = self._render_instance.values["images"]
+        if image not in images:
+            raise RenderError(f"Image [{image}] not found in values. Available images: [{', '.join(images.keys())}]")
+        repo = images[image].get("repository")
+        if not repo:
+            raise RenderError("Could not determine repo")
+        if repo not in supported_repos:
+            raise RenderError(f"Unsupported repo [{repo}] for mongodb. Supported repos: {', '.join(supported_repos)}")
+        return repo
+
+    def get_port(self):
+        return self._config.get("port") or 27017
+
+    def get_url(self, variant: str):
+        user = urllib.parse.quote_plus(self._config["user"])
+        password = urllib.parse.quote_plus(self._config["password"])
+        creds = f"{user}:{password}"
+        addr = f"{self._name}:{self.get_port()}"
+        db = self._config["database"]
+
+        urls = {
+            "mongodb": f"mongodb://{creds}@{addr}/{db}",
+            "host_port": addr,
+        }
+
+        if variant not in urls:
+            raise RenderError(f"Expected [variant] to be one of [{', '.join(urls.keys())}], got [{variant}]")
+        return urls[variant]

+ 259 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/deps_perms.py

@@ -0,0 +1,259 @@
+import json
+import pathlib
+from typing import TYPE_CHECKING
+
+
+if TYPE_CHECKING:
+    from render import Render
+    from storage import IxStorage
+
+try:
+    from .error import RenderError
+    from .validations import valid_octal_mode_or_raise, valid_fs_path_or_raise
+except ImportError:
+    from error import RenderError
+    from validations import valid_octal_mode_or_raise, valid_fs_path_or_raise
+
+
+class PermsContainer:
+    def __init__(self, render_instance: "Render", name: str):
+        self._render_instance = render_instance
+        self._name = name
+        self.actions: set[str] = set()
+        self.parsed_configs: list[dict] = []
+
+    def add_or_skip_action(self, identifier: str, volume_config: "IxStorage", action_config: dict):
+        identifier = self.normalize_identifier_for_path(identifier)
+        if identifier in self.actions:
+            raise RenderError(f"Action with id [{identifier}] already used for another permission action")
+
+        parsed_action = self.parse_action(identifier, volume_config, action_config)
+        if parsed_action:
+            self.parsed_configs.append(parsed_action)
+            self.actions.add(identifier)
+
+    def parse_action(self, identifier: str, volume_config: "IxStorage", action_config: dict):
+        valid_modes = [
+            "always",  # Always set permissions, without checking.
+            "check",  # Checks if permissions are correct, and set them if not.
+        ]
+        mode = action_config.get("mode", "check")
+        uid = action_config.get("uid", None)
+        gid = action_config.get("gid", None)
+        chmod = action_config.get("chmod", None)
+        recursive = action_config.get("recursive", False)
+        mount_path = pathlib.Path("/mnt/permission", identifier).as_posix()
+        read_only = volume_config.get("read_only", False)
+        is_temporary = False
+
+        vol_type = volume_config.get("type", "")
+        match vol_type:
+            case "temporary":
+                # If it is a temporary volume, we force auto permissions
+                # and set is_temporary to True, so it will be cleaned up
+                is_temporary = True
+                recursive = True
+            case "volume":
+                if not volume_config.get("volume_config", {}).get("auto_permissions", False):
+                    return None
+            case "host_path":
+                host_path_config = volume_config.get("host_path_config", {})
+                # Skip when ACL enabled
+                if host_path_config.get("acl_enable", False):
+                    return None
+                if not host_path_config.get("auto_permissions", False):
+                    return None
+            case "ix_volume":
+                ix_vol_config = volume_config.get("ix_volume_config", {})
+                # Skip when ACL enabled
+                if ix_vol_config.get("acl_enable", False):
+                    return None
+                # For ix_volumes, we default to auto_permissions = True
+                if not ix_vol_config.get("auto_permissions", True):
+                    return None
+            case _:
+                # Skip for other types
+                return None
+
+        if mode not in valid_modes:
+            raise RenderError(f"Expected [mode] to be one of [{', '.join(valid_modes)}], got [{mode}]")
+        if not isinstance(uid, int) or not isinstance(gid, int):
+            raise RenderError("Expected [uid] and [gid] to be set when [auto_permissions] is enabled")
+        if chmod is not None:
+            chmod = valid_octal_mode_or_raise(chmod)
+
+        mount_path = valid_fs_path_or_raise(mount_path)
+        return {
+            "mount_path": mount_path,
+            "volume_config": volume_config,
+            "action_data": {
+                "read_only": read_only,
+                "mount_path": mount_path,
+                "is_temporary": is_temporary,
+                "identifier": identifier,
+                "recursive": recursive,
+                "mode": mode,
+                "uid": uid,
+                "gid": gid,
+                "chmod": chmod,
+            },
+        }
+
+    def normalize_identifier_for_path(self, identifier: str):
+        return identifier.rstrip("/").lstrip("/").lower().replace("/", "_").replace(".", "-").replace(" ", "-")
+
+    def has_actions(self):
+        return bool(self.actions)
+
+    def activate(self):
+        if len(self.parsed_configs) != len(self.actions):
+            raise RenderError("Number of actions and parsed configs does not match")
+
+        if not self.has_actions():
+            raise RenderError("No actions added. Check if there are actions before activating")
+
+        # Add the container and set it up
+        c = self._render_instance.add_container(self._name, "python_permissions_image")
+        c.set_user(0, 0)
+        c.add_caps(["CHOWN", "FOWNER", "DAC_OVERRIDE"])
+        c.set_network_mode("none")
+
+        # Don't attach any devices
+        c.remove_devices()
+
+        c.deploy.resources.set_profile("medium")
+        c.restart.set_policy("on-failure", maximum_retry_count=1)
+        c.healthcheck.disable()
+
+        c.set_entrypoint(["python3", "/script/run.py"])
+        script = "#!/usr/bin/env python3\n"
+        script += get_script()
+        c.configs.add("permissions_run_script", script, "/script/run.py", "0700")
+
+        actions_data: list[dict] = []
+        for parsed in self.parsed_configs:
+            if not parsed["action_data"]["read_only"]:
+                c.add_storage(parsed["mount_path"], parsed["volume_config"])
+            actions_data.append(parsed["action_data"])
+
+        actions_data_json = json.dumps(actions_data)
+        c.configs.add("permissions_actions_data", actions_data_json, "/script/actions.json", "0500")
+
+
+def get_script():
+    return """
+import os
+import json
+import time
+import shutil
+
+with open("/script/actions.json", "r") as f:
+    actions_data = json.load(f)
+
+if not actions_data:
+    # If this script is called, there should be actions data
+    raise ValueError("No actions data found")
+
+def fix_perms(path, chmod, recursive=False):
+    print(f"Changing permissions{' recursively ' if recursive else ' '}to {chmod} on: [{path}]")
+    os.chmod(path, int(chmod, 8))
+    if recursive:
+        for root, dirs, files in os.walk(path):
+            for f in files:
+                os.chmod(os.path.join(root, f), int(chmod, 8))
+    print("Permissions after changes:")
+    print_chmod_stat()
+
+def fix_owner(path, uid, gid, recursive=False):
+    print(f"Changing ownership{' recursively ' if recursive else ' '}to {uid}:{gid} on: [{path}]")
+    os.chown(path, uid, gid)
+    if recursive:
+        for root, dirs, files in os.walk(path):
+            for f in files:
+                os.chown(os.path.join(root, f), uid, gid)
+    print("Ownership after changes:")
+    print_chown_stat()
+
+def print_chown_stat():
+    curr_stat = os.stat(action["mount_path"])
+    print(f"Ownership: [{curr_stat.st_uid}:{curr_stat.st_gid}]")
+
+def print_chmod_stat():
+    curr_stat = os.stat(action["mount_path"])
+    print(f"Permissions: [{oct(curr_stat.st_mode)[3:]}]")
+
+def print_chown_diff(curr_stat, uid, gid):
+    print(f"Ownership: wanted [{uid}:{gid}], got [{curr_stat.st_uid}:{curr_stat.st_gid}].")
+
+def print_chmod_diff(curr_stat, mode):
+    print(f"Permissions: wanted [{mode}], got [{oct(curr_stat.st_mode)[3:]}].")
+
+def perform_action(action):
+    if action["read_only"]:
+        print(f"Path for action [{action['identifier']}] is read-only, skipping...")
+        return
+
+    start_time = time.time()
+    print(f"=== Applying configuration on volume with identifier [{action['identifier']}] ===")
+
+    if not os.path.isdir(action["mount_path"]):
+        print(f"Path [{action['mount_path']}] is not a directory, skipping...")
+        return
+
+    if action["is_temporary"]:
+        print(f"Path [{action['mount_path']}] is a temporary directory, ensuring it is empty...")
+        for item in os.listdir(action["mount_path"]):
+            item_path = os.path.join(action["mount_path"], item)
+
+            # Exclude the safe directory, where we can use to mount files temporarily
+            if os.path.basename(item_path) == "ix-safe":
+                continue
+            if os.path.isdir(item_path):
+                shutil.rmtree(item_path)
+            else:
+                os.remove(item_path)
+
+    if not action["is_temporary"] and os.listdir(action["mount_path"]):
+        print(f"Path [{action['mount_path']}] is not empty, skipping...")
+        return
+
+    print(f"Current Ownership and Permissions on [{action['mount_path']}]:")
+    curr_stat = os.stat(action["mount_path"])
+    print_chown_diff(curr_stat, action["uid"], action["gid"])
+    print_chmod_diff(curr_stat, action["chmod"])
+    print("---")
+
+    if action["mode"] == "always":
+        fix_owner(action["mount_path"], action["uid"], action["gid"], action["recursive"])
+        if not action["chmod"]:
+            print("Skipping permissions check, chmod is falsy")
+        else:
+            fix_perms(action["mount_path"], action["chmod"], action["recursive"])
+        return
+
+    elif action["mode"] == "check":
+        if curr_stat.st_uid != action["uid"] or curr_stat.st_gid != action["gid"]:
+            print("Ownership is incorrect. Fixing...")
+            fix_owner(action["mount_path"], action["uid"], action["gid"], action["recursive"])
+        else:
+            print("Ownership is correct. Skipping...")
+
+        if not action["chmod"]:
+            print("Skipping permissions check, chmod is falsy")
+        else:
+            if oct(curr_stat.st_mode)[3:] != action["chmod"]:
+                print("Permissions are incorrect. Fixing...")
+                fix_perms(action["mount_path"], action["chmod"], action["recursive"])
+            else:
+                print("Permissions are correct. Skipping...")
+
+    print(f"Time taken: {(time.time() - start_time) * 1000:.2f}ms")
+    print(f"=== Finished applying configuration on volume with identifier [{action['identifier']}] ==")
+    print()
+
+if __name__ == "__main__":
+    start_time = time.time()
+    for action in actions_data:
+        perform_action(action)
+    print(f"Total time taken: {(time.time() - start_time) * 1000:.2f}ms")
+"""

+ 160 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/deps_postgres.py

@@ -0,0 +1,160 @@
+import urllib.parse
+from typing import TYPE_CHECKING, TypedDict, NotRequired
+
+
+if TYPE_CHECKING:
+    from render import Render
+    from storage import IxStorage
+
+
+try:
+    from .error import RenderError
+    from .deps_perms import PermsContainer
+    from .validations import valid_port_or_raise
+except ImportError:
+    from error import RenderError
+    from deps_perms import PermsContainer
+    from validations import valid_port_or_raise
+
+
+class PostgresConfig(TypedDict):
+    user: str
+    password: str
+    database: str
+    port: NotRequired[int]
+    volume: "IxStorage"
+
+
+MAX_POSTGRES_VERSION = 17
+
+
+class PostgresContainer:
+    def __init__(
+        self, render_instance: "Render", name: str, image: str, config: PostgresConfig, perms_instance: PermsContainer
+    ):
+        self._render_instance = render_instance
+        self._name = name
+        self._config = config
+        self._data_dir = "/var/lib/postgresql/data"
+        self._upgrade_name = f"{self._name}_upgrade"
+        self._upgrade_container = None
+
+        for key in ("user", "password", "database", "volume"):
+            if key not in config:
+                raise RenderError(f"Expected [{key}] to be set for postgres")
+
+        port = valid_port_or_raise(self.get_port())
+
+        c = self._render_instance.add_container(name, image)
+
+        c.set_user(999, 999)
+        c.healthcheck.set_test("postgres", {"user": config["user"], "db": config["database"]})
+        c.remove_devices()
+        c.add_storage(self._data_dir, config["volume"])
+
+        common_variables = {
+            "POSTGRES_USER": config["user"],
+            "POSTGRES_PASSWORD": config["password"],
+            "POSTGRES_DB": config["database"],
+            "PGPORT": port,
+        }
+
+        for k, v in common_variables.items():
+            c.environment.add_env(k, v)
+
+        perms_instance.add_or_skip_action(
+            f"{self._name}_postgres_data", config["volume"], {"uid": 999, "gid": 999, "mode": "check"}
+        )
+
+        repo = self._get_repo(
+            image,
+            (
+                "postgres",
+                "postgis/postgis",
+                "pgvector/pgvector",
+                "tensorchord/pgvecto-rs",
+                "ghcr.io/immich-app/postgres",
+            ),
+        )
+        # eg we don't want to handle upgrades of pg_vector at the moment
+        if repo == "postgres":
+            target_major_version = self._get_target_version(image)
+            upg = self._render_instance.add_container(self._upgrade_name, "postgres_upgrade_image")
+            upg.set_entrypoint(["/bin/bash", "-c", "/upgrade.sh"])
+            upg.restart.set_policy("on-failure", 1)
+            upg.set_user(999, 999)
+            upg.healthcheck.disable()
+            upg.remove_devices()
+            upg.add_storage(self._data_dir, config["volume"])
+            for k, v in common_variables.items():
+                upg.environment.add_env(k, v)
+
+            upg.environment.add_env("TARGET_VERSION", target_major_version)
+            upg.environment.add_env("DATA_DIR", self._data_dir)
+
+            self._upgrade_container = upg
+
+            c.depends.add_dependency(self._upgrade_name, "service_completed_successfully")
+
+        # Store container for further configuration
+        # For example: c.depends.add_dependency("other_container", "service_started")
+        self._container = c
+
+    @property
+    def container(self):
+        return self._container
+
+    def add_dependency(self, container_name: str, condition: str):
+        self._container.depends.add_dependency(container_name, condition)
+        if self._upgrade_container:
+            self._upgrade_container.depends.add_dependency(container_name, condition)
+
+    def _get_repo(self, image, supported_repos):
+        images = self._render_instance.values["images"]
+        if image not in images:
+            raise RenderError(f"Image [{image}] not found in values. Available images: [{', '.join(images.keys())}]")
+        repo = images[image].get("repository")
+        if not repo:
+            raise RenderError("Could not determine repo")
+        if repo not in supported_repos:
+            raise RenderError(f"Unsupported repo [{repo}] for postgres. Supported repos: {', '.join(supported_repos)}")
+        return repo
+
+    def _get_target_version(self, image):
+        images = self._render_instance.values["images"]
+        if image not in images:
+            raise RenderError(f"Image [{image}] not found in values. Available images: [{', '.join(images.keys())}]")
+        tag = images[image].get("tag", "")
+        tag = str(tag)  # Account for tags like 16.6
+        target_major_version = tag.split(".")[0]
+
+        try:
+            target_major_version = int(target_major_version)
+        except ValueError:
+            raise RenderError(f"Could not determine target major version from tag [{tag}]")
+
+        if target_major_version > MAX_POSTGRES_VERSION:
+            raise RenderError(f"Postgres version [{target_major_version}] is not supported")
+
+        return target_major_version
+
+    def get_port(self):
+        return self._config.get("port") or 5432
+
+    def get_url(self, variant: str):
+        user = urllib.parse.quote_plus(self._config["user"])
+        password = urllib.parse.quote_plus(self._config["password"])
+        creds = f"{user}:{password}"
+        addr = f"{self._name}:{self.get_port()}"
+        db = self._config["database"]
+
+        urls = {
+            "postgres": f"postgres://{creds}@{addr}/{db}?sslmode=disable",
+            "postgresql": f"postgresql://{creds}@{addr}/{db}?sslmode=disable",
+            "postgresql_no_creds": f"postgresql://{addr}/{db}?sslmode=disable",
+            "host_port": addr,
+        }
+
+        if variant not in urls:
+            raise RenderError(f"Expected [variant] to be one of [{', '.join(urls.keys())}], got [{variant}]")
+        return urls[variant]

+ 90 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/deps_redis.py

@@ -0,0 +1,90 @@
+import urllib.parse
+from typing import TYPE_CHECKING, TypedDict, NotRequired
+
+if TYPE_CHECKING:
+    from render import Render
+    from storage import IxStorage
+
+try:
+    from .error import RenderError
+    from .deps_perms import PermsContainer
+    from .validations import valid_port_or_raise, valid_redis_password_or_raise
+except ImportError:
+    from error import RenderError
+    from deps_perms import PermsContainer
+    from validations import valid_port_or_raise, valid_redis_password_or_raise
+
+
+class RedisConfig(TypedDict):
+    password: str
+    port: NotRequired[int]
+    volume: "IxStorage"
+
+
+class RedisContainer:
+    def __init__(
+        self, render_instance: "Render", name: str, image: str, config: RedisConfig, perms_instance: PermsContainer
+    ):
+        self._render_instance = render_instance
+        self._name = name
+        self._config = config
+
+        for key in ("password", "volume"):
+            if key not in config:
+                raise RenderError(f"Expected [{key}] to be set for redis")
+
+        valid_redis_password_or_raise(config["password"])
+
+        port = valid_port_or_raise(self.get_port())
+        self._get_repo(image, ("redis", "valkey/valkey"))
+
+        user, group = 568, 568
+        run_as = self._render_instance.values.get("run_as")
+        if run_as:
+            user = run_as["user"] or user  # Avoids running as root
+            group = run_as["group"] or group  # Avoids running as root
+        c = self._render_instance.add_container(name, image)
+        c.set_user(user, group)
+        c.remove_devices()
+        c.healthcheck.set_test("redis", {"password": config["password"]})
+
+        cmd = []
+        cmd.extend(["--port", str(port)])
+        cmd.extend(["--requirepass", config["password"]])
+        c.environment.add_env("REDIS_PASSWORD", config["password"])
+        c.set_command(cmd)
+
+        c.add_storage("/data", config["volume"])
+        perms_instance.add_or_skip_action(
+            f"{self._name}_redis_data", config["volume"], {"uid": user, "gid": group, "mode": "check"}
+        )
+
+        # Store container for further configuration
+        # For example: c.depends.add_dependency("other_container", "service_started")
+        self._container = c
+
+    def _get_repo(self, image, supported_repos):
+        images = self._render_instance.values["images"]
+        if image not in images:
+            raise RenderError(f"Image [{image}] not found in values. Available images: [{', '.join(images.keys())}]")
+        repo = images[image].get("repository")
+        if not repo:
+            raise RenderError("Could not determine repo")
+        if repo not in supported_repos:
+            raise RenderError(f"Unsupported repo [{repo}] for redis. Supported repos: {', '.join(supported_repos)}")
+        return repo
+
+    def get_port(self):
+        return self._config.get("port") or 6379
+
+    def get_url(self, variant: str):
+        addr = f"{self._name}:{self.get_port()}"
+        password = urllib.parse.quote_plus(self._config["password"])
+
+        match variant:
+            case "redis":
+                return f"redis://default:{password}@{addr}"
+
+    @property
+    def container(self):
+        return self._container

+ 31 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/device.py

@@ -0,0 +1,31 @@
+try:
+    from .error import RenderError
+    from .validations import valid_fs_path_or_raise, allowed_device_or_raise, valid_cgroup_perm_or_raise
+except ImportError:
+    from error import RenderError
+    from validations import valid_fs_path_or_raise, allowed_device_or_raise, valid_cgroup_perm_or_raise
+
+
+class Device:
+    def __init__(self, host_device: str, container_device: str, cgroup_perm: str = "", allow_disallowed=False):
+        hd = valid_fs_path_or_raise(host_device.rstrip("/"))
+        cd = valid_fs_path_or_raise(container_device.rstrip("/"))
+        if not hd or not cd:
+            raise RenderError(
+                "Expected [host_device] and [container_device] to be set. "
+                f"Got host_device [{host_device}] and container_device [{container_device}]"
+            )
+
+        cgroup_perm = valid_cgroup_perm_or_raise(cgroup_perm)
+        if not allow_disallowed:
+            hd = allowed_device_or_raise(hd)
+
+        self.cgroup_perm: str = cgroup_perm
+        self.host_device: str = hd
+        self.container_device: str = cd
+
+    def render(self):
+        result = f"{self.host_device}:{self.container_device}"
+        if self.cgroup_perm:
+            result += f":{self.cgroup_perm}"
+        return result

+ 54 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/device_cgroup_rules.py

@@ -0,0 +1,54 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+
+try:
+    from .error import RenderError
+    from .validations import valid_device_cgroup_rule_or_raise
+except ImportError:
+    from error import RenderError
+    from validations import valid_device_cgroup_rule_or_raise
+
+
+class DeviceCGroupRule:
+    def __init__(self, rule: str):
+        rule = valid_device_cgroup_rule_or_raise(rule)
+        parts = rule.split(" ")
+        major, minor = parts[1].split(":")
+
+        self._type = parts[0]
+        self._major = major
+        self._minor = minor
+        self._permissions = parts[2]
+
+    def get_key(self):
+        return f"{self._type}_{self._major}_{self._minor}"
+
+    def render(self):
+        return f"{self._type} {self._major}:{self._minor} {self._permissions}"
+
+
+class DeviceCGroupRules:
+    def __init__(self, render_instance: "Render"):
+        self._render_instance = render_instance
+        self._rules: set[DeviceCGroupRule] = set()
+        self._track_rule_combos: set[str] = set()
+
+    def add_rule(self, rule: str):
+        dev_group_rule = DeviceCGroupRule(rule)
+        if dev_group_rule in self._rules:
+            raise RenderError(f"Device Group Rule [{rule}] already added")
+
+        rule_key = dev_group_rule.get_key()
+        if rule_key in self._track_rule_combos:
+            raise RenderError(f"Device Group Rule [{rule}] has already been added for this device group")
+
+        self._rules.add(dev_group_rule)
+        self._track_rule_combos.add(rule_key)
+
+    def has_rules(self):
+        return len(self._rules) > 0
+
+    def render(self):
+        return sorted([rule.render() for rule in self._rules])

+ 71 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/devices.py

@@ -0,0 +1,71 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+
+try:
+    from .error import RenderError
+    from .device import Device
+except ImportError:
+    from error import RenderError
+    from device import Device
+
+
+class Devices:
+    def __init__(self, render_instance: "Render"):
+        self._render_instance = render_instance
+        self._devices: set[Device] = set()
+
+        # Tracks all container device paths to make sure they are not duplicated
+        self._container_device_paths: set[str] = set()
+        # Scan values for devices we should automatically add
+        # for example /dev/dri for gpus
+        self._auto_add_devices_from_values()
+
+    def _auto_add_devices_from_values(self):
+        resources = self._render_instance.values.get("resources", {})
+
+        if resources.get("gpus", {}).get("use_all_gpus", False):
+            self.add_device("/dev/dri", "/dev/dri", allow_disallowed=True)
+            if resources["gpus"].get("kfd_device_exists", False):
+                self.add_device("/dev/kfd", "/dev/kfd", allow_disallowed=True)  # AMD ROCm
+
+    def add_device(self, host_device: str, container_device: str, cgroup_perm: str = "", allow_disallowed=False):
+        # Host device can be mapped to multiple container devices,
+        # so we only make sure container devices are not duplicated
+        if container_device in self._container_device_paths:
+            raise RenderError(f"Device with container path [{container_device}] already added")
+
+        self._devices.add(Device(host_device, container_device, cgroup_perm, allow_disallowed))
+        self._container_device_paths.add(container_device)
+
+    def add_usb_bus(self):
+        self.add_device("/dev/bus/usb", "/dev/bus/usb", allow_disallowed=True)
+
+    def _add_snd_device(self):
+        self.add_device("/dev/snd", "/dev/snd", allow_disallowed=True)
+
+    def _add_tun_device(self):
+        self.add_device("/dev/net/tun", "/dev/net/tun", allow_disallowed=True)
+
+    def has_devices(self):
+        return len(self._devices) > 0
+
+    # Mainly will be used from dependencies
+    # There is no reason to pass devices to
+    # redis or postgres for example
+    def remove_devices(self):
+        self._devices.clear()
+        self._container_device_paths.clear()
+
+    # Check if there are any gpu devices
+    # Used to determine if we should add groups
+    # like 'video' to the container
+    def has_gpus(self):
+        for d in self._devices:
+            if d.host_device == "/dev/dri":
+                return True
+        return False
+
+    def render(self) -> list[str]:
+        return sorted([d.render() for d in self._devices])

+ 79 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/dns.py

@@ -0,0 +1,79 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+
+try:
+    from .error import RenderError
+    from .validations import allowed_dns_opt_or_raise
+except ImportError:
+    from error import RenderError
+    from validations import allowed_dns_opt_or_raise
+
+
+class Dns:
+    def __init__(self, render_instance: "Render"):
+        self._render_instance = render_instance
+        self._dns_options: set[str] = set()
+        self._dns_searches: set[str] = set()
+        self._dns_nameservers: set[str] = set()
+
+        self._auto_add_dns_opts_from_values()
+        self._auto_add_dns_searches_from_values()
+        self._auto_add_dns_nameservers_from_values()
+
+    def _get_dns_opt_keys(self):
+        return [self._get_key_from_opt(opt) for opt in self._dns_options]
+
+    def _get_key_from_opt(self, opt):
+        return opt.split(":")[0]
+
+    def _auto_add_dns_opts_from_values(self):
+        values = self._render_instance.values
+        for dns_opt in values.get("network", {}).get("dns_opts", []):
+            self.add_dns_opt(dns_opt)
+
+    def _auto_add_dns_searches_from_values(self):
+        values = self._render_instance.values
+        for dns_search in values.get("network", {}).get("dns_searches", []):
+            self.add_dns_search(dns_search)
+
+    def _auto_add_dns_nameservers_from_values(self):
+        values = self._render_instance.values
+        for dns_nameserver in values.get("network", {}).get("dns_nameservers", []):
+            self.add_dns_nameserver(dns_nameserver)
+
+    def add_dns_search(self, dns_search):
+        if dns_search in self._dns_searches:
+            raise RenderError(f"DNS Search [{dns_search}] already added")
+        self._dns_searches.add(dns_search)
+
+    def add_dns_nameserver(self, dns_nameserver):
+        if dns_nameserver in self._dns_nameservers:
+            raise RenderError(f"DNS Nameserver [{dns_nameserver}] already added")
+        self._dns_nameservers.add(dns_nameserver)
+
+    def add_dns_opt(self, dns_opt):
+        # eg attempts:3
+        key = allowed_dns_opt_or_raise(self._get_key_from_opt(dns_opt))
+        if key in self._get_dns_opt_keys():
+            raise RenderError(f"DNS Option [{key}] already added")
+        self._dns_options.add(dns_opt)
+
+    def has_dns_opts(self):
+        return len(self._dns_options) > 0
+
+    def has_dns_searches(self):
+        return len(self._dns_searches) > 0
+
+    def has_dns_nameservers(self):
+        return len(self._dns_nameservers) > 0
+
+    def render_dns_searches(self):
+        return sorted(self._dns_searches)
+
+    def render_dns_opts(self):
+        return sorted(self._dns_options)
+
+    def render_dns_nameservers(self):
+        return sorted(self._dns_nameservers)

+ 112 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/environment.py

@@ -0,0 +1,112 @@
+from typing import Any, TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+try:
+    from .error import RenderError
+    from .formatter import escape_dollar
+    from .resources import Resources
+except ImportError:
+    from error import RenderError
+    from formatter import escape_dollar
+    from resources import Resources
+
+
+class Environment:
+    def __init__(self, render_instance: "Render", resources: Resources):
+        self._render_instance = render_instance
+        self._resources = resources
+        # Stores variables that user defined
+        self._user_vars: dict[str, Any] = {}
+        # Stores variables that are automatically added (based on values)
+        self._auto_variables: dict[str, Any] = {}
+        # Stores variables that are added by the application developer
+        self._app_dev_variables: dict[str, Any] = {}
+
+        self._skip_generic_variables: bool = render_instance.values.get("skip_generic_variables", False)
+
+        self._auto_add_variables_from_values()
+
+    def _auto_add_variables_from_values(self):
+        if not self._skip_generic_variables:
+            self._add_generic_variables()
+        self._add_nvidia_variables()
+
+    def _add_generic_variables(self):
+        self._auto_variables["TZ"] = self._render_instance.values.get("TZ", "Etc/UTC")
+        self._auto_variables["UMASK"] = self._render_instance.values.get("UMASK", "002")
+        self._auto_variables["UMASK_SET"] = self._render_instance.values.get("UMASK", "002")
+
+        run_as = self._render_instance.values.get("run_as", {})
+        user = run_as.get("user")
+        group = run_as.get("group")
+        if user:
+            self._auto_variables["PUID"] = user
+            self._auto_variables["UID"] = user
+            self._auto_variables["USER_ID"] = user
+        if group:
+            self._auto_variables["PGID"] = group
+            self._auto_variables["GID"] = group
+            self._auto_variables["GROUP_ID"] = group
+
+    def _add_nvidia_variables(self):
+        if self._resources._nvidia_ids:
+            self._auto_variables["NVIDIA_DRIVER_CAPABILITIES"] = "all"
+            self._auto_variables["NVIDIA_VISIBLE_DEVICES"] = ",".join(sorted(self._resources._nvidia_ids))
+        else:
+            self._auto_variables["NVIDIA_VISIBLE_DEVICES"] = "void"
+
+    def _format_value(self, v: Any) -> str:
+        value = str(v)
+
+        # str(bool) returns "True" or "False",
+        # but we want "true" or "false"
+        if isinstance(v, bool):
+            value = value.lower()
+        return value
+
+    def add_env(self, name: str, value: Any):
+        if not name:
+            raise RenderError(f"Environment variable name cannot be empty. [{name}]")
+        if name in self._app_dev_variables.keys():
+            raise RenderError(
+                f"Found duplicate environment variable [{name}] in application developer environment variables."
+            )
+        self._app_dev_variables[name] = value
+
+    def add_user_envs(self, user_env: list[dict]):
+        for item in user_env:
+            if not item.get("name"):
+                raise RenderError(f"Environment variable name cannot be empty. [{item}]")
+            if item["name"] in self._user_vars.keys():
+                raise RenderError(
+                    f"Found duplicate environment variable [{item['name']}] in user environment variables."
+                )
+            self._user_vars[item["name"]] = item.get("value")
+
+    def has_variables(self):
+        return len(self._auto_variables) > 0 or len(self._user_vars) > 0 or len(self._app_dev_variables) > 0
+
+    def render(self):
+        result: dict[str, str] = {}
+
+        # Add envs from auto variables
+        result.update({k: self._format_value(v) for k, v in self._auto_variables.items()})
+
+        # Track defined keys for faster lookup
+        defined_keys = set(result.keys())
+
+        # Add envs from application developer (prohibit overwriting auto variables)
+        for k, v in self._app_dev_variables.items():
+            if k in defined_keys:
+                raise RenderError(f"Environment variable [{k}] is already defined automatically from the library.")
+            result[k] = self._format_value(v)
+            defined_keys.add(k)
+
+        # Add envs from user (prohibit overwriting app developer envs and auto variables)
+        for k, v in self._user_vars.items():
+            if k in defined_keys:
+                raise RenderError(f"Environment variable [{k}] is already defined from the application developer.")
+            result[k] = self._format_value(v)
+
+        return {k: escape_dollar(v) for k, v in result.items()}

+ 4 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/error.py

@@ -0,0 +1,4 @@
+class RenderError(Exception):
+    """Base class for exceptions in this module."""
+
+    pass

+ 31 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/expose.py

@@ -0,0 +1,31 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+
+try:
+    from .error import RenderError
+    from .validations import valid_port_or_raise, valid_port_protocol_or_raise
+except ImportError:
+    from error import RenderError
+    from validations import valid_port_or_raise, valid_port_protocol_or_raise
+
+
+class Expose:
+    def __init__(self, render_instance: "Render"):
+        self._render_instance = render_instance
+        self._ports: set[str] = set()
+
+    def add_port(self, port: int, protocol: str = "tcp"):
+        port = valid_port_or_raise(port)
+        protocol = valid_port_protocol_or_raise(protocol)
+        key = f"{port}/{protocol}"
+        if key in self._ports:
+            raise RenderError(f"Exposed port [{port}/{protocol}] already added")
+        self._ports.add(key)
+
+    def has_ports(self):
+        return len(self._ports) > 0
+
+    def render(self):
+        return sorted(self._ports)

+ 33 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/extra_hosts.py

@@ -0,0 +1,33 @@
+import ipaddress
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+
+try:
+    from .error import RenderError
+except ImportError:
+    from error import RenderError
+
+
+class ExtraHosts:
+    def __init__(self, render_instance: "Render"):
+        self._render_instance = render_instance
+        self._extra_hosts: dict[str, str] = {}
+
+    def add_host(self, host: str, ip: str):
+        if not ip == "host-gateway":
+            try:
+                ipaddress.ip_address(ip)
+            except ValueError:
+                raise RenderError(f"Invalid IP address [{ip}] for host [{host}]")
+
+        if host in self._extra_hosts:
+            raise RenderError(f"Host [{host}] already added with [{self._extra_hosts[host]}]")
+        self._extra_hosts[host] = ip
+
+    def has_hosts(self):
+        return len(self._extra_hosts) > 0
+
+    def render(self):
+        return {host: ip for host, ip in self._extra_hosts.items()}

+ 26 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/formatter.py

@@ -0,0 +1,26 @@
+import json
+import hashlib
+
+
+def escape_dollar(text: str) -> str:
+    return text.replace("$", "$$")
+
+
+def get_hashed_name_for_volume(prefix: str, config: dict):
+    config_hash = hashlib.sha256(json.dumps(config).encode("utf-8")).hexdigest()
+    return f"{prefix}_{config_hash}"
+
+
+def get_hash_with_prefix(prefix: str, data: str):
+    return f"{prefix}_{hashlib.sha256(data.encode('utf-8')).hexdigest()}"
+
+
+def merge_dicts_no_overwrite(dict1, dict2):
+    overlapping_keys = dict1.keys() & dict2.keys()
+    if overlapping_keys:
+        raise ValueError(f"Merging of dicts failed. Overlapping keys: {overlapping_keys}")
+    return {**dict1, **dict2}
+
+
+def get_image_with_hashed_data(image: str, data: str):
+    return get_hash_with_prefix(f"ix-{image}", data)

+ 210 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/functions.py

@@ -0,0 +1,210 @@
+import re
+import copy
+import bcrypt
+import secrets
+import urllib.parse
+from base64 import b64encode
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+
+try:
+    from .error import RenderError
+    from .volume_sources import HostPathSource, IxVolumeSource
+except ImportError:
+    from error import RenderError
+    from volume_sources import HostPathSource, IxVolumeSource
+
+
+class Functions:
+    def __init__(self, render_instance: "Render"):
+        self._render_instance = render_instance
+
+    def _bcrypt_hash(self, password):
+        hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
+        return hashed
+
+    def _htpasswd(self, username, password):
+        hashed = self._bcrypt_hash(password)
+        return username + ":" + hashed
+
+    def _secure_string(self, length):
+        return secrets.token_urlsafe(length)[:length]
+
+    def _basic_auth(self, username, password):
+        return b64encode(f"{username}:{password}".encode("utf-8")).decode("utf-8")
+
+    def _basic_auth_header(self, username, password):
+        return f"Basic {self._basic_auth(username, password)}"
+
+    def _fail(self, message):
+        raise RenderError(message)
+
+    def _camel_case(self, string):
+        return string.title()
+
+    def _auto_cast(self, value):
+        lower_str_value = str(value).lower()
+        if lower_str_value in ["true", "false"]:
+            return lower_str_value == "true"
+
+        try:
+            float_value = float(value)
+            if float_value.is_integer():
+                return int(float_value)
+            else:
+                return float(value)
+        except ValueError:
+            pass
+
+        return value
+
+    def _match_regex(self, value, regex):
+        if not re.match(regex, value):
+            return False
+        return True
+
+    def _must_match_regex(self, value, regex):
+        if not self._match_regex(value, regex):
+            raise RenderError(f"Expected [{value}] to match [{regex}]")
+        return value
+
+    def _is_boolean(self, string):
+        return string.lower() in ["true", "false"]
+
+    def _is_number(self, string):
+        try:
+            float(string)
+            return True
+        except ValueError:
+            return False
+
+    def _copy_dict(self, dict):
+        return copy.deepcopy(dict)
+
+    def _merge_dicts(self, *dicts):
+        merged_dict = {}
+        for dictionary in dicts:
+            merged_dict.update(dictionary)
+        return merged_dict
+
+    def _disallow_chars(self, string: str, chars: list[str], key: str):
+        for char in chars:
+            if char in string:
+                raise RenderError(f"Disallowed character [{char}] in [{key}]")
+        return string
+
+    def _or_default(self, value, default):
+        if not value:
+            return default
+        return value
+
+    def _url_to_dict(self, url: str, v6_brackets: bool = False):
+        try:
+            # Try parsing as-is first
+            parsed = urllib.parse.urlparse(url)
+
+            # If we didn't get a hostname, try with http:// prefix
+            if not parsed.hostname:
+                parsed = urllib.parse.urlparse(f"http://{url}")
+
+            # Final check that we have a valid result
+            if not parsed.hostname:
+                raise RenderError(
+                    f"Failed to parse URL [{url}]. Ensure it is a valid URL with a hostname and optional port."
+                )
+
+            result = {
+                "host": parsed.hostname,
+                "port": parsed.port,
+            }
+            if v6_brackets and parsed.hostname and ":" in parsed.hostname:
+                result["host"] = f"[{parsed.hostname}]"
+                result["host_no_brackets"] = parsed.hostname
+
+            return result
+
+        except Exception:
+            raise RenderError(
+                f"Failed to parse URL [{url}]. Ensure it is a valid URL with a hostname and optional port."
+            )
+
+    def _require_unique(self, values, key, split_char=""):
+        new_values = []
+        for value in values:
+            new_values.append(value.split(split_char)[0] if split_char else value)
+
+        if len(new_values) != len(set(new_values)):
+            raise RenderError(f"Expected values in [{key}] to be unique, but got [{', '.join(values)}]")
+
+    def _require_no_reserved(self, values, key, reserved, split_char="", starts_with=False):
+        new_values = []
+        for value in values:
+            new_values.append(value.split(split_char)[0] if split_char else value)
+
+        if starts_with:
+            for arg in new_values:
+                for reserved_value in reserved:
+                    if arg.startswith(reserved_value):
+                        raise RenderError(f"Value [{reserved_value}] is reserved and cannot be set in [{key}]")
+            return
+
+        for reserved_value in reserved:
+            if reserved_value in new_values:
+                raise RenderError(f"Value [{reserved_value}] is reserved and cannot be set in [{key}]")
+
+    def _url_encode(self, string):
+        return urllib.parse.quote_plus(string)
+
+    def _temp_config(self, name):
+        if not name:
+            raise RenderError("Expected [name] to be set when calling [temp_config].")
+        return {"type": "temporary", "volume_config": {"volume_name": name}}
+
+    def _get_host_path(self, storage):
+        source_type = storage.get("type", "")
+        if not source_type:
+            raise RenderError("Expected [type] to be set for volume mounts.")
+
+        match source_type:
+            case "host_path":
+                mount_config = storage.get("host_path_config")
+                if mount_config is None:
+                    raise RenderError("Expected [host_path_config] to be set for [host_path] type.")
+                host_source = HostPathSource(self._render_instance, mount_config).get()
+                return host_source
+            case "ix_volume":
+                mount_config = storage.get("ix_volume_config")
+                if mount_config is None:
+                    raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.")
+                ix_source = IxVolumeSource(self._render_instance, mount_config).get()
+                return ix_source
+            case _:
+                raise RenderError(f"Storage type [{source_type}] does not support host path.")
+
+    def func_map(self):
+        return {
+            "auto_cast": self._auto_cast,
+            "basic_auth_header": self._basic_auth_header,
+            "basic_auth": self._basic_auth,
+            "bcrypt_hash": self._bcrypt_hash,
+            "camel_case": self._camel_case,
+            "copy_dict": self._copy_dict,
+            "fail": self._fail,
+            "htpasswd": self._htpasswd,
+            "is_boolean": self._is_boolean,
+            "is_number": self._is_number,
+            "match_regex": self._match_regex,
+            "merge_dicts": self._merge_dicts,
+            "must_match_regex": self._must_match_regex,
+            "secure_string": self._secure_string,
+            "disallow_chars": self._disallow_chars,
+            "get_host_path": self._get_host_path,
+            "or_default": self._or_default,
+            "temp_config": self._temp_config,
+            "require_unique": self._require_unique,
+            "require_no_reserved": self._require_no_reserved,
+            "url_encode": self._url_encode,
+            "url_to_dict": self._url_to_dict,
+        }

+ 268 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/healthcheck.py

@@ -0,0 +1,268 @@
+import json
+from typing import Any, TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+
+try:
+    from .error import RenderError
+    from .formatter import escape_dollar
+    from .validations import valid_http_path_or_raise
+except ImportError:
+    from error import RenderError
+    from formatter import escape_dollar
+    from validations import valid_http_path_or_raise
+
+
+class Healthcheck:
+    def __init__(self, render_instance: "Render"):
+        self._render_instance = render_instance
+        self._test: str | list[str] = ""
+        self._interval_sec: int = 30
+        self._timeout_sec: int = 5
+        self._retries: int = 5
+        self._start_period_sec: int = 15
+        self._start_interval_sec: int = 2
+        self._disabled: bool = False
+        self._use_built_in: bool = False
+
+    def _get_test(self):
+        if isinstance(self._test, str):
+            return escape_dollar(self._test)
+        return [escape_dollar(t) for t in self._test]
+
+    def disable(self):
+        self._disabled = True
+
+    def use_built_in(self):
+        self._use_built_in = True
+
+    def set_custom_test(self, test: str | list[str]):
+        if isinstance(test, list):
+            if test[0] == "CMD" and any(t.startswith("$") for t in test):
+                raise RenderError(f"Healthcheck with 'CMD' cannot contain shell variables '{test}'")
+        if self._disabled:
+            raise RenderError("Cannot set custom test when healthcheck is disabled")
+        self._test = test
+
+    def set_test(self, variant: str, config: dict | None = None):
+        config = config or {}
+        self.set_custom_test(test_mapping(variant, config))
+
+    def set_interval(self, interval: int):
+        self._interval_sec = interval
+
+    def set_timeout(self, timeout: int):
+        self._timeout_sec = timeout
+
+    def set_retries(self, retries: int):
+        self._retries = retries
+
+    def set_start_period(self, start_period: int):
+        self._start_period_sec = start_period
+
+    def set_start_interval(self, start_interval: int):
+        self._start_interval_sec = start_interval
+
+    def has_healthcheck(self):
+        return not self._use_built_in
+
+    def render(self):
+        if self._use_built_in:
+            return RenderError("Should not be called when built in healthcheck is used")
+
+        if self._disabled:
+            return {"disable": True}
+
+        if not self._test:
+            raise RenderError("Healthcheck test is not set")
+
+        return {
+            "test": self._get_test(),
+            "retries": self._retries,
+            "interval": f"{self._interval_sec}s",
+            "timeout": f"{self._timeout_sec}s",
+            "start_period": f"{self._start_period_sec}s",
+            "start_interval": f"{self._start_interval_sec}s",
+        }
+
+
+def test_mapping(variant: str, config: dict | None = None) -> list[str]:
+    config = config or {}
+    tests = {
+        "curl": curl_test,
+        "wget": wget_test,
+        "http": http_test,
+        "netcat": netcat_test,
+        "tcp": tcp_test,
+        "redis": redis_test,
+        "postgres": postgres_test,
+        "mariadb": mariadb_test,
+        "mongodb": mongodb_test,
+    }
+
+    if variant not in tests:
+        raise RenderError(f"Test variant [{variant}] is not valid. Valid options are: [{', '.join(tests.keys())}]")
+
+    return tests[variant](config)
+
+
+def get_key(config: dict, key: str, default: Any, required: bool):
+    if key not in config:
+        if not required:
+            return default
+        raise RenderError(f"Expected [{key}] to be set")
+    return config[key]
+
+
+def curl_test(config: dict) -> list[str]:
+    config = config or {}
+    port = get_key(config, "port", None, True)
+    path = valid_http_path_or_raise(get_key(config, "path", "/", False))
+    scheme = get_key(config, "scheme", "http", False)
+    host = get_key(config, "host", "127.0.0.1", False)
+    headers = get_key(config, "headers", [], False)
+    method = get_key(config, "method", "GET", False)
+    data = get_key(config, "data", None, False)
+
+    cmd = ["CMD", "curl", "--request", method, "--silent", "--output", "/dev/null", "--show-error", "--fail"]
+
+    if scheme == "https":
+        cmd.append("--insecure")
+
+    for header in headers:
+        if not header[0] or not header[1]:
+            raise RenderError("Expected [header] to be a list of two items for curl test")
+        cmd.extend(["--header", f"{header[0]}: {header[1]}"])
+
+    if data is not None:
+        cmd.extend(["--data", json.dumps(data)])
+
+    cmd.append(f"{scheme}://{host}:{port}{path}")
+    return cmd
+
+
+def wget_test(config: dict) -> list[str]:
+    config = config or {}
+    port = get_key(config, "port", None, True)
+    path = valid_http_path_or_raise(get_key(config, "path", "/", False))
+    scheme = get_key(config, "scheme", "http", False)
+    host = get_key(config, "host", "127.0.0.1", False)
+    headers = get_key(config, "headers", [], False)
+    spider = get_key(config, "spider", True, False)
+
+    cmd = ["CMD", "wget", "--quiet"]
+
+    if spider:
+        cmd.append("--spider")
+    else:
+        cmd.extend(["-O", "/dev/null"])
+
+    if scheme == "https":
+        cmd.append("--no-check-certificate")
+
+    for header in headers:
+        if not header[0] or not header[1]:
+            raise RenderError("Expected [header] to be a list of two items for wget test")
+        cmd.extend(["--header", f"{header[0]}: {header[1]}"])
+
+    cmd.append(f"{scheme}://{host}:{port}{path}")
+
+    return cmd
+
+
+def http_test(config: dict) -> list[str]:
+    config = config or {}
+    port = get_key(config, "port", None, True)
+    path = valid_http_path_or_raise(get_key(config, "path", "/", False))
+    host = get_key(config, "host", "127.0.0.1", False)
+
+    return [
+        "CMD-SHELL",
+        f"""/bin/bash -c 'exec {{hc_fd}}<>/dev/tcp/{host}/{port} && echo -e "GET {path} HTTP/1.1\\r\\nHost: {host}\\r\\nConnection: close\\r\\n\\r\\n" >&${{hc_fd}} && cat <&${{hc_fd}} | grep "HTTP" | grep -q "200"'""",  # noqa
+    ]
+
+
+def netcat_test(config: dict) -> list[str]:
+    config = config or {}
+    port = get_key(config, "port", None, True)
+    host = get_key(config, "host", "127.0.0.1", False)
+    udp_mode = get_key(config, "udp", False, False)
+    cmd = ["CMD", "nc", "-z", "-w", "1"]
+
+    if udp_mode:
+        cmd.append("-u")
+
+    cmd.extend([host, str(port)])
+
+    return cmd
+
+
+def tcp_test(config: dict) -> list[str]:
+    config = config or {}
+    port = get_key(config, "port", None, True)
+    host = get_key(config, "host", "127.0.0.1", False)
+
+    return ["CMD", "timeout", "1", "bash", "-c", f"cat < /dev/null > /dev/tcp/{host}/{port}"]
+
+
+def redis_test(config: dict) -> list[str]:
+    config = config or {}
+    port = get_key(config, "port", 6379, False)
+    host = get_key(config, "host", "127.0.0.1", False)
+    password = get_key(config, "password", None, False)
+    cmd = ["CMD", "redis-cli", "-h", host, "-p", str(port)]
+
+    if password:
+        cmd.extend(["-a", password])
+
+    cmd.append("ping")
+
+    return cmd
+
+
+def postgres_test(config: dict) -> list[str]:
+    config = config or {}
+    port = get_key(config, "port", 5432, False)
+    host = get_key(config, "host", "127.0.0.1", False)
+    user = get_key(config, "user", None, True)
+    db = get_key(config, "db", None, True)
+
+    return ["CMD", "pg_isready", "-h", host, "-p", str(port), "-U", user, "-d", db]
+
+
+def mariadb_test(config: dict) -> list[str]:
+    config = config or {}
+    port = get_key(config, "port", 3306, False)
+    host = get_key(config, "host", "127.0.0.1", False)
+    password = get_key(config, "password", None, True)
+
+    return [
+        "CMD",
+        "mariadb-admin",
+        "--user=root",
+        f"--host={host}",
+        f"--port={port}",
+        f"--password={password}",
+        "ping",
+    ]
+
+
+def mongodb_test(config: dict) -> list[str]:
+    config = config or {}
+    port = get_key(config, "port", 27017, False)
+    host = get_key(config, "host", "127.0.0.1", False)
+    db = get_key(config, "db", None, True)
+
+    return [
+        "CMD",
+        "mongosh",
+        "--host",
+        host,
+        "--port",
+        str(port),
+        db,
+        "--eval",
+        'db.adminCommand("ping")',
+        "--quiet",
+    ]

+ 37 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/labels.py

@@ -0,0 +1,37 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+
+try:
+    from .error import RenderError
+    from .formatter import escape_dollar
+except ImportError:
+    from error import RenderError
+    from formatter import escape_dollar
+
+
+class Labels:
+    def __init__(self, render_instance: "Render"):
+        self._render_instance = render_instance
+        self._labels: dict[str, str] = {}
+
+    def add_label(self, key: str, value: str):
+        if not key:
+            raise RenderError("Labels must have a key")
+
+        if key.startswith("com.docker.compose"):
+            raise RenderError(f"Label [{key}] cannot start with [com.docker.compose] as it is reserved")
+
+        if key in self._labels.keys():
+            raise RenderError(f"Label [{key}] already added")
+
+        self._labels[key] = escape_dollar(str(value))
+
+    def has_labels(self) -> bool:
+        return bool(self._labels)
+
+    def render(self) -> dict[str, str]:
+        if not self.has_labels():
+            return {}
+        return {label: value for label, value in sorted(self._labels.items())}

+ 133 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/notes.py

@@ -0,0 +1,133 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+
+SHORT_LIVED = "short-lived"
+
+
+class Notes:
+    def __init__(self, render_instance: "Render"):
+        self._render_instance = render_instance
+        self._app_name: str = ""
+        self._app_train: str = ""
+        self._warnings: list[str] = []
+        self._deprecations: list[str] = []
+        self._security: dict[str, list[str]] = {}
+        self._header: str = ""
+        self._body: str = ""
+        self._footer: str = ""
+
+        self._auto_set_app_name()
+        self._auto_set_app_train()
+        self._auto_set_header()
+        self._auto_set_footer()
+
+    def _is_enterprise_train(self):
+        if self._app_train == "enterprise":
+            return True
+
+    def _auto_set_app_name(self):
+        app_name = self._render_instance.values.get("ix_context", {}).get("app_metadata", {}).get("title", "")
+        self._app_name = app_name or "<app_name>"
+
+    def _auto_set_app_train(self):
+        app_train = self._render_instance.values.get("ix_context", {}).get("app_metadata", {}).get("train", "")
+        self._app_train = app_train or "<app_train>"
+
+    def _auto_set_header(self):
+        self._header = f"# {self._app_name}\n\n"
+
+    def _auto_set_footer(self):
+        url = "https://github.com/truenas/apps"
+        if self._is_enterprise_train():
+            url = "https://ixsystems.atlassian.net"
+        footer = "## Bug Reports and Feature Requests\n\n"
+        footer += "If you find a bug in this app or have an idea for a new feature, please file an issue at\n"
+        footer += f"{url}\n\n"
+        self._footer = footer
+
+    def add_warning(self, warning: str):
+        self._warnings.append(warning)
+
+    def _prepend_warning(self, warning: str):
+        self._warnings.insert(0, warning)
+
+    def add_deprecation(self, deprecation: str):
+        self._deprecations.append(deprecation)
+
+    def set_body(self, body: str):
+        self._body = body
+
+    def scan_containers(self):
+        for name, c in self._render_instance._containers.items():
+            if self._security.get(name) is None:
+                self._security[name] = []
+
+            if c.restart._policy == "on-failure":
+                self._security[name].append(SHORT_LIVED)
+
+            if c._privileged:
+                self._security[name].append("Is running with privileged mode enabled")
+
+            run_as = c._user.split(":") if c._user else [-1, -1]
+            if run_as[0] in ["0", -1]:
+                self._security[name].append(f"Is running as {'root' if run_as[0] == '0' else 'unknown'} user")
+            if run_as[1] in ["0", -1]:
+                self._security[name].append(f"Is running as {'root' if run_as[1] == '0' else 'unknown'} group")
+            if any(x in c._group_add for x in ("root", 0)):
+                self._security[name].append("Is running with supplementary root group")
+
+            if c._ipc_mode == "host":
+                self._security[name].append("Is running with host IPC namespace")
+            if c._pid_mode == "host":
+                self._security[name].append("Is running with host PID namespace")
+            if c._cgroup == "host":
+                self._security[name].append("Is running with host cgroup namespace")
+            if "no-new-privileges=true" not in c._security_opt.render():
+                self._security[name].append("Is running without [no-new-privileges] security option")
+            if c._tty:
+                self._prepend_warning(
+                    f"Container [{name}] is running with a TTY, "
+                    "Logs will not appear correctly in the UI due to an [upstream bug]"
+                    "(https://github.com/docker/docker-py/issues/1394)"
+                )
+
+        self._security = {k: v for k, v in self._security.items() if v}
+
+    def render(self):
+        self.scan_containers()
+
+        result = self._header
+
+        if self._warnings:
+            result += "## Warnings\n\n"
+            for warning in self._warnings:
+                result += f"- {warning}\n"
+            result += "\n"
+
+        if self._deprecations:
+            result += "## Deprecations\n\n"
+            for deprecation in self._deprecations:
+                result += f"- {deprecation}\n"
+            result += "\n"
+
+        if self._security:
+            result += "## Security\n\n"
+            for c_name, security in self._security.items():
+                if SHORT_LIVED in security and len(security) == 1:
+                    continue
+                result += f"### Container: [{c_name}]"
+                if SHORT_LIVED in security:
+                    result += "\n\n**This container is short-lived.**"
+                result += "\n\n"
+                for s in [s for s in security if s != "short-lived"]:
+                    result += f"- {s}\n"
+                result += "\n"
+
+        if self._body:
+            result += self._body.strip() + "\n\n"
+
+        result += self._footer
+
+        return result

+ 73 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/portals.py

@@ -0,0 +1,73 @@
+from typing import TYPE_CHECKING
+
+import copy
+
+if TYPE_CHECKING:
+    from render import Render
+
+try:
+    from .error import RenderError
+    from .validations import valid_portal_scheme_or_raise, valid_http_path_or_raise, valid_port_or_raise
+except ImportError:
+    from error import RenderError
+    from validations import valid_portal_scheme_or_raise, valid_http_path_or_raise, valid_port_or_raise
+
+
+class Portals:
+    def __init__(self, render_instance: "Render"):
+        self._render_instance = render_instance
+        self._portals: set[Portal] = set()
+
+    def add(self, port: dict, config: dict | None = None):
+        config = copy.deepcopy((config or {}))
+        port = copy.deepcopy((port or {}))
+        # If its not published, portal does not make sense
+        if port.get("bind_mode", "") != "published":
+            return
+
+        name = config.get("name", "Web UI")
+
+        if name in [p._name for p in self._portals]:
+            raise RenderError(f"Portal [{name}] already added")
+
+        host = config.get("host", None)
+        host_ips = port.get("host_ips", [])
+        if not isinstance(host_ips, list):
+            raise RenderError("Expected [host_ips] to be a list of strings")
+
+        # Remove wildcard IPs
+        if "::" in host_ips:
+            host_ips.remove("::")
+        if "0.0.0.0" in host_ips:
+            host_ips.remove("0.0.0.0")
+
+        # If host is not set, use the first host_ip (if it exists)
+        if not host and len(host_ips) >= 1:
+            host = host_ips[0]
+
+        config["host"] = host
+        if not config.get("port"):
+            config["port"] = port.get("port_number", 0)
+
+        self._portals.add(Portal(name, config))
+
+    def render(self):
+        return [p.render() for _, p in sorted([(p._name, p) for p in self._portals])]
+
+
+class Portal:
+    def __init__(self, name: str, config: dict):
+        self._name = name
+        self._scheme = valid_portal_scheme_or_raise(config.get("scheme", "http"))
+        self._host = config.get("host", "0.0.0.0") or "0.0.0.0"
+        self._port = valid_port_or_raise(config.get("port", 0))
+        self._path = valid_http_path_or_raise(config.get("path", "/"))
+
+    def render(self):
+        return {
+            "name": self._name,
+            "scheme": self._scheme,
+            "host": self._host,
+            "port": self._port,
+            "path": self._path,
+        }

+ 147 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/ports.py

@@ -0,0 +1,147 @@
+import ipaddress
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+
+try:
+    from .error import RenderError
+    from .validations import (
+        valid_ip_or_raise,
+        valid_port_mode_or_raise,
+        valid_port_or_raise,
+        valid_port_protocol_or_raise,
+    )
+except ImportError:
+    from error import RenderError
+    from validations import (
+        valid_ip_or_raise,
+        valid_port_mode_or_raise,
+        valid_port_or_raise,
+        valid_port_protocol_or_raise,
+    )
+
+
+class Ports:
+    def __init__(self, render_instance: "Render"):
+        self._render_instance = render_instance
+        self._ports: dict[str, dict] = {}
+
+    def _gen_port_key(self, host_port: int, host_ip: str, proto: str, ip_family: int) -> str:
+        return f"{host_port}_{host_ip}_{proto}_{ip_family}"
+
+    def _is_wildcard_ip(self, ip: str) -> bool:
+        return ip in ["0.0.0.0", "::"]
+
+    def _get_opposite_wildcard(self, ip: str) -> str:
+        return "0.0.0.0" if ip == "::" else "::"
+
+    def _get_sort_key(self, p: dict) -> str:
+        return f"{p['published']}_{p['target']}_{p['protocol']}_{p.get('host_ip', '_')}"
+
+    def _is_ports_same(self, port1: dict, port2: dict) -> bool:
+        return (
+            port1["published"] == port2["published"]
+            and port1["target"] == port2["target"]
+            and port1["protocol"] == port2["protocol"]
+            and port1.get("host_ip", "_") == port2.get("host_ip", "_")
+        )
+
+    def _has_opposite_family_port(self, port_config: dict, wildcard_ports: dict) -> bool:
+        comparison_port = port_config.copy()
+        comparison_port["host_ip"] = self._get_opposite_wildcard(port_config["host_ip"])
+        for p in wildcard_ports.values():
+            if self._is_ports_same(comparison_port, p):
+                return True
+        return False
+
+    def _check_port_conflicts(self, port_config: dict, ip_family: int) -> None:
+        host_port = port_config["published"]
+        host_ip = port_config["host_ip"]
+        proto = port_config["protocol"]
+
+        key = self._gen_port_key(host_port, host_ip, proto, ip_family)
+
+        if key in self._ports.keys():
+            raise RenderError(f"Port [{host_port}/{proto}/ipv{ip_family}] already added for [{host_ip}]")
+
+        wildcard_ip = "0.0.0.0" if ip_family == 4 else "::"
+        if host_ip != wildcard_ip:
+            # Check if there is a port with same details but with wildcard IP of the same family
+            wildcard_key = self._gen_port_key(host_port, wildcard_ip, proto, ip_family)
+            if wildcard_key in self._ports.keys():
+                raise RenderError(
+                    f"Cannot bind port [{host_port}/{proto}/ipv{ip_family}] to [{host_ip}], "
+                    f"already bound to [{wildcard_ip}]"
+                )
+        else:
+            # We are adding a port with wildcard IP
+            # Check if there is a port with same details but with specific IP of the same family
+            for p in self._ports.values():
+                # Skip if the port is not for the same family
+                if ip_family != ipaddress.ip_address(p["host_ip"]).version:
+                    continue
+
+                # Make a copy of the port config
+                search_port = p.copy()
+                # Replace the host IP with wildcard IP
+                search_port["host_ip"] = wildcard_ip
+                # If the ports match, means that a port for specific IP is already added
+                # and we are trying to add it again with wildcard IP. Raise an error
+                if self._is_ports_same(search_port, port_config):
+                    raise RenderError(
+                        f"Cannot bind port [{host_port}/{proto}/ipv{ip_family}] to [{host_ip}], "
+                        f"already bound to [{p['host_ip']}]"
+                    )
+
+    def _add_port(self, host_port: int, container_port: int, config: dict | None = None):
+        config = config or {}
+        host_port = valid_port_or_raise(host_port)
+        container_port = valid_port_or_raise(container_port)
+        proto = valid_port_protocol_or_raise(config.get("protocol", "tcp"))
+        mode = valid_port_mode_or_raise(config.get("mode", "ingress"))
+
+        host_ip = valid_ip_or_raise(config.get("host_ip", ""))
+        ip = ipaddress.ip_address(host_ip)
+
+        port_config = {
+            "published": host_port,
+            "target": container_port,
+            "protocol": proto,
+            "mode": mode,
+            "host_ip": host_ip,
+        }
+        self._check_port_conflicts(port_config, ip.version)
+
+        key = self._gen_port_key(host_port, host_ip, proto, ip.version)
+        self._ports[key] = port_config
+        # After all the local validations, lets validate the port with the TrueNAS API
+        self._render_instance.client.validate_ip_port_combo(host_ip, host_port)
+
+    def has_ports(self):
+        return len(self._ports) > 0
+
+    def render(self):
+        specific_ports = []
+        wildcard_ports = {}
+
+        for port_config in self._ports.values():
+            if self._is_wildcard_ip(port_config["host_ip"]):
+                wildcard_ports[id(port_config)] = port_config.copy()
+            else:
+                specific_ports.append(port_config.copy())
+
+        processed_ports = specific_ports.copy()
+        for wild_port in wildcard_ports.values():
+            processed_port = wild_port.copy()
+
+            # Check if there's a matching wildcard port for the opposite IP family
+            has_opposite_family = self._has_opposite_family_port(wild_port, wildcard_ports)
+
+            if has_opposite_family:
+                processed_port.pop("host_ip")
+
+            if processed_port not in processed_ports:
+                processed_ports.append(processed_port)
+
+        return sorted(processed_ports, key=self._get_sort_key)

+ 99 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/render.py

@@ -0,0 +1,99 @@
+import copy
+
+try:
+    from .client import Client
+    from .configs import Configs
+    from .container import Container
+    from .deps import Deps
+    from .error import RenderError
+    from .functions import Functions
+    from .notes import Notes
+    from .portals import Portals
+    from .volumes import Volumes
+except ImportError:
+    from client import Client
+    from configs import Configs
+    from container import Container
+    from deps import Deps
+    from error import RenderError
+    from functions import Functions
+    from notes import Notes
+    from portals import Portals
+    from volumes import Volumes
+
+
+class Render(object):
+    def __init__(self, values):
+        self._containers: dict[str, Container] = {}
+        self.values = values
+        self._add_images_internal_use()
+        # Make a copy after we inject the images
+        self._original_values: dict = copy.deepcopy(self.values)
+
+        self.deps: Deps = Deps(self)
+
+        self.client: Client = Client(render_instance=self)
+
+        self.configs = Configs(render_instance=self)
+        self.funcs = Functions(render_instance=self).func_map()
+        self.portals: Portals = Portals(render_instance=self)
+        self.notes: Notes = Notes(render_instance=self)
+        self.volumes = Volumes(render_instance=self)
+
+    def _add_images_internal_use(self):
+        if not self.values.get("images"):
+            self.values["images"] = {}
+
+        if "python_permissions_image" not in self.values["images"]:
+            self.values["images"]["python_permissions_image"] = {"repository": "python", "tag": "3.13.0-slim-bookworm"}
+
+        if "postgres_upgrade_image" not in self.values["images"]:
+            self.values["images"]["postgres_upgrade_image"] = {
+                "repository": "ixsystems/postgres-upgrade",
+                "tag": "1.0.1",
+            }
+
+    def container_names(self):
+        return list(self._containers.keys())
+
+    def add_container(self, name: str, image: str):
+        name = name.strip()
+        if not name:
+            raise RenderError("Container name cannot be empty")
+        container = Container(self, name, image)
+        if name in self._containers:
+            raise RenderError(f"Container {name} already exists.")
+        self._containers[name] = container
+        return container
+
+    def render(self):
+        if self.values != self._original_values:
+            raise RenderError("Values have been modified since the renderer was created.")
+
+        if not self._containers:
+            raise RenderError("No containers added.")
+
+        result: dict = {
+            "x-notes": self.notes.render(),
+            "x-portals": self.portals.render(),
+            "services": {c._name: c.render() for c in self._containers.values()},
+        }
+
+        # Make sure that after services are rendered
+        # there are no labels that target a non-existent container
+        # This is to prevent typos
+        for label in self.values.get("labels", []):
+            for c in label.get("containers", []):
+                if c not in self.container_names():
+                    raise RenderError(f"Label [{label['key']}] references container [{c}] which does not exist")
+
+        if self.volumes.has_volumes():
+            result["volumes"] = self.volumes.render()
+
+        if self.configs.has_configs():
+            result["configs"] = self.configs.render()
+
+        # if self.networks:
+        #     result["networks"] = {...}
+
+        return result

+ 115 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/resources.py

@@ -0,0 +1,115 @@
+import re
+from typing import Any, TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+
+try:
+    from .error import RenderError
+except ImportError:
+    from error import RenderError
+
+DEFAULT_CPUS = 2.0
+DEFAULT_MEMORY = 4096
+
+
+class Resources:
+    def __init__(self, render_instance: "Render"):
+        self._render_instance = render_instance
+        self._limits: dict = {}
+        self._reservations: dict = {}
+        self._nvidia_ids: set[str] = set()
+        self._auto_add_cpu_from_values()
+        self._auto_add_memory_from_values()
+        self._auto_add_gpus_from_values()
+
+    def _set_cpu(self, cpus: Any):
+        c = str(cpus)
+        if not re.match(r"^[1-9][0-9]*(\.[0-9]+)?$", c):
+            raise RenderError(f"Expected cpus to be a number or a float (minimum 1.0), got [{cpus}]")
+        self._limits.update({"cpus": c})
+
+    def _set_memory(self, memory: Any):
+        m = str(memory)
+        if not re.match(r"^[1-9][0-9]*$", m):
+            raise RenderError(f"Expected memory to be a number, got [{memory}]")
+        self._limits.update({"memory": f"{m}M"})
+
+    def _auto_add_cpu_from_values(self):
+        resources = self._render_instance.values.get("resources", {})
+        self._set_cpu(resources.get("limits", {}).get("cpus", DEFAULT_CPUS))
+
+    def _auto_add_memory_from_values(self):
+        resources = self._render_instance.values.get("resources", {})
+        self._set_memory(resources.get("limits", {}).get("memory", DEFAULT_MEMORY))
+
+    def _auto_add_gpus_from_values(self):
+        resources = self._render_instance.values.get("resources", {})
+        gpus = resources.get("gpus", {}).get("nvidia_gpu_selection", {})
+        if not gpus:
+            return
+
+        for pci, gpu in gpus.items():
+            if gpu.get("use_gpu", False):
+                if not gpu.get("uuid"):
+                    raise RenderError(f"Expected [uuid] to be set for GPU in slot [{pci}] in [nvidia_gpu_selection]")
+                self._nvidia_ids.add(gpu["uuid"])
+
+        if self._nvidia_ids:
+            if not self._reservations:
+                self._reservations["devices"] = []
+            self._reservations["devices"].append(
+                {
+                    "capabilities": ["gpu"],
+                    "driver": "nvidia",
+                    "device_ids": sorted(self._nvidia_ids),
+                }
+            )
+
+    # This is only used on ix-app that we allow
+    # disabling cpus and memory. GPUs are only added
+    # if the user has requested them.
+    def remove_cpus_and_memory(self):
+        self._limits.pop("cpus", None)
+        self._limits.pop("memory", None)
+
+    # Mainly will be used from dependencies
+    # There is no reason to pass devices to
+    # redis or postgres for example
+    def remove_devices(self):
+        self._reservations.pop("devices", None)
+
+    def set_profile(self, profile: str):
+        cpu, memory = profile_mapping(profile)
+        self._set_cpu(cpu)
+        self._set_memory(memory)
+
+    def has_resources(self):
+        return len(self._limits) > 0 or len(self._reservations) > 0
+
+    def has_gpus(self):
+        gpu_devices = [d for d in self._reservations.get("devices", []) if "gpu" in d["capabilities"]]
+        return len(gpu_devices) > 0
+
+    def render(self):
+        result = {}
+        if self._limits:
+            result["limits"] = self._limits
+        if self._reservations:
+            result["reservations"] = self._reservations
+
+        return result
+
+
+def profile_mapping(profile: str):
+    profiles = {
+        "low": (1, 512),
+        "medium": (2, 1024),
+    }
+
+    if profile not in profiles:
+        raise RenderError(
+            f"Resource profile [{profile}] is not valid. Valid options are: [{', '.join(profiles.keys())}]"
+        )
+
+    return profiles[profile]

+ 25 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/restart.py

@@ -0,0 +1,25 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+
+try:
+    from .validations import valid_restart_policy_or_raise
+except ImportError:
+    from validations import valid_restart_policy_or_raise
+
+
+class RestartPolicy:
+    def __init__(self, render_instance: "Render"):
+        self._render_instance = render_instance
+        self._policy: str = "unless-stopped"
+        self._maximum_retry_count: int = 0
+
+    def set_policy(self, policy: str, maximum_retry_count: int = 0):
+        self._policy = valid_restart_policy_or_raise(policy, maximum_retry_count)
+        self._maximum_retry_count = maximum_retry_count
+
+    def render(self):
+        if self._policy == "on-failure" and self._maximum_retry_count > 0:
+            return f"{self._policy}:{self._maximum_retry_count}"
+        return self._policy

+ 52 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/security_opts.py

@@ -0,0 +1,52 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+
+try:
+    from .error import RenderError
+    from .validations import valid_security_opt_or_raise
+except ImportError:
+    from error import RenderError
+    from validations import valid_security_opt_or_raise
+
+
+class SecurityOpt:
+    def __init__(self, opt: str, value: str | bool | None = None, arg: str | None = None):
+        self._opt: str = valid_security_opt_or_raise(opt)
+        self._value = str(value).lower() if isinstance(value, bool) else value
+        self._arg: str | None = arg
+
+    def render(self):
+        result = self._opt
+        if self._value is not None:
+            result = f"{result}={self._value}"
+        if self._arg is not None:
+            result = f"{result}:{self._arg}"
+        return result
+
+
+class SecurityOpts:
+    def __init__(self, render_instance: "Render"):
+        self._render_instance = render_instance
+        self._opts: dict[str, SecurityOpt] = dict()
+        self.add_opt("no-new-privileges", True)
+
+    def add_opt(self, key: str, value: str | bool | None, arg: str | None = None):
+        if key in self._opts:
+            raise RenderError(f"Security Option [{key}] already added")
+        self._opts[key] = SecurityOpt(key, value, arg)
+
+    def remove_opt(self, key: str):
+        if key not in self._opts:
+            raise RenderError(f"Security Option [{key}] not found")
+        del self._opts[key]
+
+    def has_opts(self):
+        return len(self._opts) > 0
+
+    def render(self):
+        result = []
+        for opt in sorted(self._opts.values(), key=lambda o: o._opt):
+            result.append(opt.render())
+        return result

+ 125 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/storage.py

@@ -0,0 +1,125 @@
+from typing import TYPE_CHECKING, TypedDict, Literal, NotRequired, Union
+
+if TYPE_CHECKING:
+    from container import Container
+    from render import Render
+
+try:
+    from .error import RenderError
+    from .validations import valid_fs_path_or_raise
+    from .volume_mount import VolumeMount
+except ImportError:
+    from error import RenderError
+    from validations import valid_fs_path_or_raise
+    from volume_mount import VolumeMount
+
+
+class IxStorageTmpfsConfig(TypedDict):
+    size: NotRequired[int]
+    mode: NotRequired[str]
+    uid: NotRequired[int]
+    gid: NotRequired[int]
+
+
+class AclConfig(TypedDict, total=False):
+    path: str
+
+
+class IxStorageHostPathConfig(TypedDict):
+    path: NotRequired[str]  # Either this or acl.path must be set
+    acl_enable: NotRequired[bool]
+    acl: NotRequired[AclConfig]
+    create_host_path: NotRequired[bool]
+    propagation: NotRequired[Literal["shared", "slave", "private", "rshared", "rslave", "rprivate"]]
+    auto_permissions: NotRequired[bool]  # Only when acl_enable is false
+
+
+class IxStorageIxVolumeConfig(TypedDict):
+    dataset_name: str
+    acl_enable: NotRequired[bool]
+    acl_entries: NotRequired[AclConfig]
+    create_host_path: NotRequired[bool]
+    propagation: NotRequired[Literal["shared", "slave", "private", "rshared", "rslave", "rprivate"]]
+    auto_permissions: NotRequired[bool]  # Only when acl_enable is false
+
+
+class IxStorageVolumeConfig(TypedDict):
+    volume_name: NotRequired[str]
+    nocopy: NotRequired[bool]
+    auto_permissions: NotRequired[bool]
+
+
+class IxStorageNfsConfig(TypedDict):
+    server: str
+    path: str
+    options: NotRequired[list[str]]
+
+
+class IxStorageCifsConfig(TypedDict):
+    server: str
+    path: str
+    username: str
+    password: str
+    domain: NotRequired[str]
+    options: NotRequired[list[str]]
+
+
+IxStorageVolumeLikeConfigs = Union[IxStorageVolumeConfig, IxStorageNfsConfig, IxStorageCifsConfig, IxStorageTmpfsConfig]
+IxStorageBindLikeConfigs = Union[IxStorageHostPathConfig, IxStorageIxVolumeConfig]
+IxStorageLikeConfigs = Union[IxStorageBindLikeConfigs, IxStorageVolumeLikeConfigs]
+
+
+class IxStorage(TypedDict):
+    type: Literal["ix_volume", "host_path", "tmpfs", "volume", "anonymous", "temporary"]
+    read_only: NotRequired[bool]
+
+    ix_volume_config: NotRequired[IxStorageIxVolumeConfig]
+    host_path_config: NotRequired[IxStorageHostPathConfig]
+    tmpfs_config: NotRequired[IxStorageTmpfsConfig]
+    volume_config: NotRequired[IxStorageVolumeConfig]
+    nfs_config: NotRequired[IxStorageNfsConfig]
+    cifs_config: NotRequired[IxStorageCifsConfig]
+
+
+class Storage:
+    def __init__(self, render_instance: "Render", container_instance: "Container"):
+        self._container_instance = container_instance
+        self._render_instance = render_instance
+        self._volume_mounts: set[VolumeMount] = set()
+
+    def add(self, mount_path: str, config: "IxStorage"):
+        mount_path = valid_fs_path_or_raise(mount_path)
+        if self.is_defined(mount_path):
+            raise RenderError(f"Mount path [{mount_path}] already used for another volume mount")
+        if self._container_instance._tmpfs.is_defined(mount_path):
+            raise RenderError(f"Mount path [{mount_path}] already used for another volume mount")
+
+        volume_mount = VolumeMount(self._render_instance, mount_path, config)
+        self._volume_mounts.add(volume_mount)
+
+    def is_defined(self, mount_path: str):
+        return mount_path in [m.mount_path for m in self._volume_mounts]
+
+    def _add_docker_socket(self, read_only: bool = True, mount_path: str = ""):
+        mount_path = valid_fs_path_or_raise(mount_path)
+        cfg: "IxStorage" = {
+            "type": "host_path",
+            "read_only": read_only,
+            "host_path_config": {"path": "/var/run/docker.sock", "create_host_path": False},
+        }
+        self.add(mount_path, cfg)
+
+    def _add_udev(self, read_only: bool = True, mount_path: str = ""):
+        mount_path = valid_fs_path_or_raise(mount_path)
+        cfg: "IxStorage" = {
+            "type": "host_path",
+            "read_only": read_only,
+            "host_path_config": {"path": "/run/udev", "create_host_path": False},
+        }
+        self.add(mount_path, cfg)
+
+    def has_mounts(self) -> bool:
+        return bool(self._volume_mounts)
+
+    def render(self):
+        return [vm.render() for vm in sorted(self._volume_mounts, key=lambda vm: vm.mount_path)]

+ 38 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/sysctls.py

@@ -0,0 +1,38 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+    from container import Container
+
+try:
+    from .error import RenderError
+    from .validations import valid_sysctl_or_raise
+except ImportError:
+    from error import RenderError
+    from validations import valid_sysctl_or_raise
+
+
+class Sysctls:
+    def __init__(self, render_instance: "Render", container_instance: "Container"):
+        self._render_instance = render_instance
+        self._container_instance = container_instance
+        self._sysctls: dict = {}
+
+    def add(self, key: str, value):
+        key = key.strip()
+        if not key:
+            raise RenderError("Sysctls key cannot be empty")
+        if value is None:
+            raise RenderError(f"Sysctl [{key}] requires a value")
+        if key in self._sysctls:
+            raise RenderError(f"Sysctl [{key}] already added")
+        self._sysctls[key] = str(value)
+
+    def has_sysctls(self):
+        return bool(self._sysctls)
+
+    def render(self):
+        if not self.has_sysctls():
+            return {}
+        host_net = self._container_instance._network_mode == "host"
+        return {valid_sysctl_or_raise(k, host_net): v for k, v in self._sysctls.items()}

+ 0 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/__init__.py


+ 57 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_build_image.py

@@ -0,0 +1,57 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+    return {
+        "images": {
+            "test_image": {
+                "repository": "nginx",
+                "tag": "latest",
+            }
+        },
+    }
+
+
+def test_build_image_with_from(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.build_image(["FROM test_image"])
+
+
+def test_build_image_with_from_with_whitespace(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.build_image([" FROM test_image"])
+
+
+def test_build_image(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.build_image(
+        [
+            "RUN echo hello",
+            None,
+            "",
+            "RUN echo world",
+        ]
+    )
+    output = render.render()
+    assert (
+        output["services"]["test_container"]["image"]
+        == "ix-nginx:latest_4a127145ea4c25511707e57005dd0ed457fe2f4932082c8f9faa339a450b6a99"
+    )
+    assert output["services"]["test_container"]["build"] == {
+        "tags": ["ix-nginx:latest_4a127145ea4c25511707e57005dd0ed457fe2f4932082c8f9faa339a450b6a99"],
+        "dockerfile_inline": """FROM nginx:latest
+RUN echo hello
+RUN echo world
+""",
+    }

+ 63 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_configs.py

@@ -0,0 +1,63 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+    return {
+        "images": {
+            "test_image": {
+                "repository": "nginx",
+                "tag": "latest",
+            }
+        },
+    }
+
+
+def test_add_duplicate_config_with_different_data(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.configs.add("test_config", "test_data", "/some/path")
+    with pytest.raises(Exception):
+        c1.configs.add("test_config", "test_data2", "/some/path")
+
+
+def test_add_config_with_empty_target(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.configs.add("test_config", "test_data", "")
+
+
+def test_add_duplicate_target(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.configs.add("test_config", "test_data", "/some/path")
+    with pytest.raises(Exception):
+        c1.configs.add("test_config2", "test_data2", "/some/path")
+
+
+def test_add_config(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.configs.add("test_config", "$test_data", "/some/path")
+    output = render.render()
+    assert output["configs"]["test_config"]["content"] == "$$test_data"
+    assert output["services"]["test_container"]["configs"] == [{"source": "test_config", "target": "/some/path"}]
+
+
+def test_add_config_with_mode(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.configs.add("test_config", "test_data", "/some/path", "0777")
+    output = render.render()
+    assert output["configs"]["test_config"]["content"] == "test_data"
+    assert output["services"]["test_container"]["configs"] == [
+        {"source": "test_config", "target": "/some/path", "mode": 511}
+    ]

+ 458 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_container.py

@@ -0,0 +1,458 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+    return {
+        "images": {
+            "test_image": {
+                "repository": "nginx",
+                "tag": "latest",
+            }
+        },
+    }
+
+
+def test_empty_container_name(mock_values):
+    render = Render(mock_values)
+    with pytest.raises(Exception):
+        render.add_container("  ", "test_image")
+
+
+def test_resolve_image(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    output = render.render()
+    assert output["services"]["test_container"]["image"] == "nginx:latest"
+
+
+def test_missing_repo(mock_values):
+    mock_values["images"]["test_image"]["repository"] = ""
+    render = Render(mock_values)
+    with pytest.raises(Exception):
+        render.add_container("test_container", "test_image")
+
+
+def test_missing_tag(mock_values):
+    mock_values["images"]["test_image"]["tag"] = ""
+    render = Render(mock_values)
+    with pytest.raises(Exception):
+        render.add_container("test_container", "test_image")
+
+
+def test_non_existing_image(mock_values):
+    render = Render(mock_values)
+    with pytest.raises(Exception):
+        render.add_container("test_container", "non_existing_image")
+
+
+def test_pull_policy(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.set_pull_policy("always")
+    c1.healthcheck.disable()
+    output = render.render()
+    assert output["services"]["test_container"]["pull_policy"] == "always"
+
+
+def test_invalid_pull_policy(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    with pytest.raises(Exception):
+        c1.set_pull_policy("invalid_policy")
+
+
+def test_clear_caps(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.add_caps(["NET_ADMIN"])
+    c1.clear_caps()
+    c1.healthcheck.disable()
+    output = render.render()
+    assert "cap_drop" not in output["services"]["test_container"]
+    assert "cap_add" not in output["services"]["test_container"]
+
+
+def test_privileged(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.set_privileged(True)
+    c1.healthcheck.disable()
+    output = render.render()
+    assert output["services"]["test_container"]["privileged"] is True
+
+
+def test_tty(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.set_tty(True)
+    c1.healthcheck.disable()
+    output = render.render()
+    assert output["services"]["test_container"]["tty"] is True
+
+
+def test_init(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.set_init(True)
+    c1.healthcheck.disable()
+    output = render.render()
+    assert output["services"]["test_container"]["init"] is True
+
+
+def test_read_only(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.set_read_only(True)
+    c1.healthcheck.disable()
+    output = render.render()
+    assert output["services"]["test_container"]["read_only"] is True
+
+
+def test_stdin(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.set_stdin(True)
+    c1.healthcheck.disable()
+    output = render.render()
+    assert output["services"]["test_container"]["stdin_open"] is True
+
+
+def test_hostname(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.set_hostname("test_hostname")
+    c1.healthcheck.disable()
+    output = render.render()
+    assert output["services"]["test_container"]["hostname"] == "test_hostname"
+
+
+def test_grace_period(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.set_grace_period(10)
+    c1.healthcheck.disable()
+    output = render.render()
+    assert output["services"]["test_container"]["stop_grace_period"] == "10s"
+
+
+def test_user(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.set_user(1000, 1000)
+    c1.healthcheck.disable()
+    output = render.render()
+    assert output["services"]["test_container"]["user"] == "1000:1000"
+
+
+def test_invalid_user(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.set_user(-100, 1000)
+
+
+def test_add_group(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.add_group(1000)
+    c1.add_group("video")
+    output = render.render()
+    assert output["services"]["test_container"]["group_add"] == [568, 1000, "video"]
+
+
+def test_add_duplicate_group(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.add_group(1000)
+    with pytest.raises(Exception):
+        c1.add_group(1000)
+
+
+def test_add_group_as_string(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.add_group("1000")
+
+
+def test_add_docker_socket(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.add_docker_socket()
+    output = render.render()
+    assert output["services"]["test_container"]["group_add"] == [568, 999]
+    assert output["services"]["test_container"]["volumes"] == [
+        {
+            "type": "bind",
+            "source": "/var/run/docker.sock",
+            "target": "/var/run/docker.sock",
+            "read_only": True,
+            "bind": {
+                "propagation": "rprivate",
+                "create_host_path": False,
+            },
+        }
+    ]
+
+
+def test_snd_device(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.add_snd_device()
+    output = render.render()
+    assert output["services"]["test_container"]["devices"] == ["/dev/snd:/dev/snd"]
+    assert output["services"]["test_container"]["group_add"] == [29, 568]
+
+
+def test_shm_size(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.set_shm_size_mb(10)
+    output = render.render()
+    assert output["services"]["test_container"]["shm_size"] == "10M"
+
+
+def test_valid_caps(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.add_caps(["ALL", "NET_ADMIN"])
+    output = render.render()
+    assert output["services"]["test_container"]["cap_add"] == ["ALL", "NET_ADMIN"]
+    assert output["services"]["test_container"]["cap_drop"] == ["ALL"]
+
+
+def test_add_duplicate_caps(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.add_caps(["ALL", "NET_ADMIN", "NET_ADMIN"])
+
+
+def test_invalid_caps(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.add_caps(["invalid_cap"])
+
+
+def test_network_mode(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.set_network_mode("host")
+    output = render.render()
+    assert output["services"]["test_container"]["network_mode"] == "host"
+
+
+def test_auto_network_mode_with_host_network(mock_values):
+    mock_values["network"] = {"host_network": True}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    output = render.render()
+    assert output["services"]["test_container"]["network_mode"] == "host"
+
+
+def test_network_mode_with_container(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.set_network_mode("service:test_container")
+    output = render.render()
+    assert output["services"]["test_container"]["network_mode"] == "service:test_container"
+
+
+def test_network_mode_with_container_missing(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.set_network_mode("service:missing_container")
+
+
+def test_invalid_network_mode(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.set_network_mode("invalid_mode")
+
+
+def test_entrypoint(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.set_entrypoint(["/bin/bash", "-c", "echo hello $MY_ENV"])
+    c1.healthcheck.disable()
+    output = render.render()
+    assert output["services"]["test_container"]["entrypoint"] == ["/bin/bash", "-c", "echo hello $$MY_ENV"]
+
+
+def test_command(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.set_command(["echo", "hello $MY_ENV"])
+    c1.healthcheck.disable()
+    output = render.render()
+    assert output["services"]["test_container"]["command"] == ["echo", "hello $$MY_ENV"]
+
+
+def test_add_ports(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.add_port({"port_number": 8081, "container_port": 8080, "bind_mode": "published"})
+    c1.add_port({"port_number": 8082, "container_port": 8080, "bind_mode": "published", "protocol": "udp"})
+    c1.add_port({"port_number": 8083, "container_port": 8080, "bind_mode": "exposed"})
+    c1.add_port({"port_number": 8084, "container_port": 8080, "bind_mode": ""})
+    c1.add_port(
+        {"port_number": 9091, "container_port": 9091, "bind_mode": "published"},
+        {"container_port": 9092, "protocol": "udp"},
+    )
+    output = render.render()
+    assert output["services"]["test_container"]["ports"] == [
+        {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"},
+        {"published": 8082, "target": 8080, "protocol": "udp", "mode": "ingress"},
+        {"published": 9091, "target": 9092, "protocol": "udp", "mode": "ingress"},
+    ]
+    assert output["services"]["test_container"]["expose"] == ["8080/tcp"]
+
+
+def test_add_ports_with_invalid_host_ips(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.add_port({"port_number": 8081, "container_port": 8080, "bind_mode": "published", "host_ips": "invalid"})
+
+
+def test_add_ports_with_empty_host_ips(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.add_port({"port_number": 8081, "container_port": 8080, "bind_mode": "published", "host_ips": []})
+    output = render.render()
+    assert output["services"]["test_container"]["ports"] == [
+        {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"}
+    ]
+
+
+def test_set_ipc_mode(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.set_ipc_mode("host")
+    output = render.render()
+    assert output["services"]["test_container"]["ipc"] == "host"
+
+
+def test_set_ipc_empty_mode(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.set_ipc_mode("")
+    output = render.render()
+    assert output["services"]["test_container"]["ipc"] == ""
+
+
+def test_set_ipc_mode_with_invalid_ipc_mode(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.set_ipc_mode("invalid")
+
+
+def test_set_ipc_mode_with_container_ipc_mode(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c2 = render.add_container("test_container2", "test_image")
+    c2.healthcheck.disable()
+    c1.set_ipc_mode("container:test_container2")
+    output = render.render()
+    assert output["services"]["test_container"]["ipc"] == "container:test_container2"
+
+
+def test_set_ipc_mode_with_container_ipc_mode_and_invalid_container(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.set_ipc_mode("container:invalid")
+
+
+def test_set_pid_mode(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.set_pid_mode("host")
+    output = render.render()
+    assert output["services"]["test_container"]["pid"] == "host"
+
+
+def test_set_pid_empty_mode(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.set_pid_mode("")
+    output = render.render()
+    assert output["services"]["test_container"]["pid"] == ""
+
+
+def test_set_pid_mode_with_invalid_pid_mode(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.set_pid_mode("invalid")
+
+
+def test_set_pid_mode_with_container_pid_mode(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c2 = render.add_container("test_container2", "test_image")
+    c2.healthcheck.disable()
+    c1.set_pid_mode("container:test_container2")
+    output = render.render()
+    assert output["services"]["test_container"]["pid"] == "container:test_container2"
+
+
+def test_set_pid_mode_with_container_pid_mode_and_invalid_container(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.set_pid_mode("container:invalid")
+
+
+def test_set_cgroup(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.set_cgroup("host")
+    output = render.render()
+    assert output["services"]["test_container"]["cgroup"] == "host"
+
+
+def test_set_cgroup_invalid(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.set_cgroup("invalid")

+ 54 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_depends.py

@@ -0,0 +1,54 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+    return {
+        "images": {
+            "test_image": {
+                "repository": "nginx",
+                "tag": "latest",
+            }
+        },
+    }
+
+
+def test_add_dependency(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c2 = render.add_container("test_container2", "test_image")
+    c1.healthcheck.disable()
+    c2.healthcheck.disable()
+    c1.depends.add_dependency("test_container2", "service_started")
+    output = render.render()
+    assert output["services"]["test_container"]["depends_on"]["test_container2"] == {"condition": "service_started"}
+
+
+def test_add_dependency_invalid_condition(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    render.add_container("test_container2", "test_image")
+    with pytest.raises(Exception):
+        c1.depends.add_dependency("test_container2", "invalid_condition")
+
+
+def test_add_dependency_missing_container(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.depends.add_dependency("test_container2", "service_started")
+
+
+def test_add_dependency_duplicate(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    render.add_container("test_container2", "test_image")
+    c1.depends.add_dependency("test_container2", "service_started")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.depends.add_dependency("test_container2", "service_started")

+ 840 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_deps.py

@@ -0,0 +1,840 @@
+import json
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+    return {
+        "images": {
+            "test_image": {
+                "repository": "nginx",
+                "tag": "latest",
+            }
+        },
+    }
+
+
+def test_add_postgres_missing_config(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        render.deps.postgres(
+            "pg_container",
+            "test_image",
+            {"user": "test_user", "password": "test_password", "database": "test_database"},  # type: ignore
+        )
+
+
+def test_add_postgres_unsupported_repo(mock_values):
+    mock_values["images"]["pg_image"] = {"repository": "unsupported_repo", "tag": "16"}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    perms_container = render.deps.perms("perms_container")
+    with pytest.raises(Exception):
+        render.deps.postgres(
+            "pg_container",
+            "pg_image",
+            {
+                "user": "test_user",
+                "password": "test_@password",
+                "database": "test_database",
+                "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+            },
+            perms_container,
+        )
+
+
+def test_add_postgres(mock_values):
+    mock_values["images"]["pg_image"] = {"repository": "postgres", "tag": "16"}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    perms_container = render.deps.perms("perms_container")
+    p = render.deps.postgres(
+        "pg_container",
+        "pg_image",
+        {
+            "user": "test_user",
+            "password": "test_@password",
+            "database": "test_database",
+            "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+        },
+        perms_container,
+    )
+    if perms_container.has_actions():
+        perms_container.activate()
+        p.container.depends.add_dependency("perms_container", "service_completed_successfully")
+    output = render.render()
+    assert (
+        p.get_url("postgres") == "postgres://test_user:test_%40password@pg_container:5432/test_database?sslmode=disable"
+    )
+    assert "devices" not in output["services"]["pg_container"]
+    assert "reservations" not in output["services"]["pg_container"]["deploy"]["resources"]
+    assert output["services"]["pg_container"]["image"] == "postgres:16"
+    assert output["services"]["pg_container"]["user"] == "999:999"
+    assert output["services"]["pg_container"]["deploy"]["resources"]["limits"]["cpus"] == "2.0"
+    assert output["services"]["pg_container"]["deploy"]["resources"]["limits"]["memory"] == "4096M"
+    assert output["services"]["pg_container"]["healthcheck"] == {
+        "test": [
+            "CMD",
+            "pg_isready",
+            "-h",
+            "127.0.0.1",
+            "-p",
+            "5432",
+            "-U",
+            "test_user",
+            "-d",
+            "test_database",
+        ],
+        "interval": "30s",
+        "timeout": "5s",
+        "retries": 5,
+        "start_period": "15s",
+        "start_interval": "2s",
+    }
+    assert output["services"]["pg_container"]["volumes"] == [
+        {
+            "type": "volume",
+            "source": "test_volume",
+            "target": "/var/lib/postgresql/data",
+            "read_only": False,
+            "volume": {"nocopy": False},
+        }
+    ]
+    assert output["services"]["pg_container"]["environment"] == {
+        "TZ": "Etc/UTC",
+        "UMASK": "002",
+        "UMASK_SET": "002",
+        "NVIDIA_VISIBLE_DEVICES": "void",
+        "POSTGRES_USER": "test_user",
+        "POSTGRES_PASSWORD": "test_@password",
+        "POSTGRES_DB": "test_database",
+        "PGPORT": "5432",
+    }
+    assert output["services"]["pg_container"]["depends_on"] == {
+        "perms_container": {"condition": "service_completed_successfully"},
+        "pg_container_upgrade": {"condition": "service_completed_successfully"},
+    }
+    assert output["services"]["perms_container"]["restart"] == "on-failure:1"
+
+
+def test_add_redis_missing_config(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        render.deps.redis(
+            "redis_container",
+            "test_image",
+            {"password": "test_password", "volume": {}},  # type: ignore
+        )
+
+
+def test_add_redis_unsupported_repo(mock_values):
+    mock_values["images"]["redis_image"] = {"repository": "unsupported_repo", "tag": "latest"}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    perms_container = render.deps.perms("perms_container")
+    with pytest.raises(Exception):
+        render.deps.redis(
+            "redis_container",
+            "redis_image",
+            {
+                "password": "test&password@",
+                "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+            },
+            perms_container,
+        )
+
+
+def test_add_redis_with_password_with_spaces(mock_values):
+    mock_values["images"]["redis_image"] = {"repository": "redis", "tag": "latest"}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        render.deps.redis(
+            "redis_container",
+            "redis_image",
+            {"password": "test password", "volume": {}},  # type: ignore
+        )
+
+
+def test_add_redis(mock_values):
+    mock_values["images"]["redis_image"] = {"repository": "valkey/valkey", "tag": "latest"}
+    mock_values["run_as"] = {"user": 0, "group": 0}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    perms_container = render.deps.perms("perms_container")
+    r = render.deps.redis(
+        "redis_container",
+        "redis_image",
+        {
+            "password": "test&password@",
+            "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+        },
+        perms_container,
+    )
+    c1.environment.add_env("REDIS_URL", r.get_url("redis"))
+    if perms_container.has_actions():
+        perms_container.activate()
+        r.container.depends.add_dependency("perms_container", "service_completed_successfully")
+    output = render.render()
+    assert "devices" not in output["services"]["redis_container"]
+    assert "reservations" not in output["services"]["redis_container"]["deploy"]["resources"]
+    assert (
+        output["services"]["test_container"]["environment"]["REDIS_URL"]
+        == "redis://default:test%26password%40@redis_container:6379"
+    )
+    assert output["services"]["redis_container"]["image"] == "valkey/valkey:latest"
+    assert output["services"]["redis_container"]["user"] == "568:568"
+    assert output["services"]["redis_container"]["deploy"]["resources"]["limits"]["cpus"] == "2.0"
+    assert output["services"]["redis_container"]["deploy"]["resources"]["limits"]["memory"] == "4096M"
+    assert output["services"]["redis_container"]["healthcheck"] == {
+        "test": [
+            "CMD",
+            "redis-cli",
+            "-h",
+            "127.0.0.1",
+            "-p",
+            "6379",
+            "-a",
+            "test&password@",
+            "ping",
+        ],
+        "interval": "30s",
+        "timeout": "5s",
+        "retries": 5,
+        "start_period": "15s",
+        "start_interval": "2s",
+    }
+    assert output["services"]["redis_container"]["volumes"] == [
+        {
+            "type": "volume",
+            "source": "test_volume",
+            "target": "/data",
+            "read_only": False,
+            "volume": {"nocopy": False},
+        }
+    ]
+    assert output["services"]["redis_container"]["environment"] == {
+        "TZ": "Etc/UTC",
+        "UMASK": "002",
+        "UMASK_SET": "002",
+        "NVIDIA_VISIBLE_DEVICES": "void",
+        "REDIS_PASSWORD": "test&password@",
+    }
+    assert output["services"]["redis_container"]["depends_on"] == {
+        "perms_container": {"condition": "service_completed_successfully"}
+    }
+
+
+def test_add_mariadb_missing_config(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        render.deps.mariadb(
+            "mariadb_container",
+            "test_image",
+            {"user": "test_user", "password": "test_password", "database": "test_database"},  # type: ignore
+        )
+
+
+def test_add_mariadb_unsupported_repo(mock_values):
+    mock_values["images"]["mariadb_image"] = {"repository": "unsupported_repo", "tag": "latest"}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    perms_container = render.deps.perms("perms_container")
+    with pytest.raises(Exception):
+        render.deps.mariadb(
+            "mariadb_container",
+            "mariadb_image",
+            {
+                "user": "test_user",
+                "password": "test_password",
+                "database": "test_database",
+                "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+            },
+            perms_container,
+        )
+
+
+def test_add_mariadb(mock_values):
+    mock_values["images"]["mariadb_image"] = {"repository": "mariadb", "tag": "latest"}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    perms_container = render.deps.perms("perms_container")
+    m = render.deps.mariadb(
+        "mariadb_container",
+        "mariadb_image",
+        {
+            "user": "test_user",
+            "password": "test_password",
+            "database": "test_database",
+            "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+        },
+        perms_container,
+    )
+    if perms_container.has_actions():
+        perms_container.activate()
+        m.container.depends.add_dependency("perms_container", "service_completed_successfully")
+    output = render.render()
+    assert "devices" not in output["services"]["mariadb_container"]
+    assert "reservations" not in output["services"]["mariadb_container"]["deploy"]["resources"]
+    assert output["services"]["mariadb_container"]["image"] == "mariadb:latest"
+    assert output["services"]["mariadb_container"]["user"] == "999:999"
+    assert output["services"]["mariadb_container"]["deploy"]["resources"]["limits"]["cpus"] == "2.0"
+    assert output["services"]["mariadb_container"]["deploy"]["resources"]["limits"]["memory"] == "4096M"
+    assert output["services"]["mariadb_container"]["healthcheck"] == {
+        "test": [
+            "CMD",
+            "mariadb-admin",
+            "--user=root",
+            "--host=127.0.0.1",
+            "--port=3306",
+            "--password=test_password",
+            "ping",
+        ],
+        "interval": "30s",
+        "timeout": "5s",
+        "retries": 5,
+        "start_period": "15s",
+        "start_interval": "2s",
+    }
+    assert output["services"]["mariadb_container"]["volumes"] == [
+        {
+            "type": "volume",
+            "source": "test_volume",
+            "target": "/var/lib/mysql",
+            "read_only": False,
+            "volume": {"nocopy": False},
+        }
+    ]
+    assert output["services"]["mariadb_container"]["environment"] == {
+        "TZ": "Etc/UTC",
+        "UMASK": "002",
+        "UMASK_SET": "002",
+        "NVIDIA_VISIBLE_DEVICES": "void",
+        "MARIADB_USER": "test_user",
+        "MARIADB_PASSWORD": "test_password",
+        "MARIADB_ROOT_PASSWORD": "test_password",
+        "MARIADB_DATABASE": "test_database",
+        "MARIADB_AUTO_UPGRADE": "true",
+    }
+    assert output["services"]["mariadb_container"]["depends_on"] == {
+        "perms_container": {"condition": "service_completed_successfully"}
+    }
+
+
+def test_add_perms_container(mock_values):
+    mock_values["ix_volumes"] = {
+        "test_dataset1": "/mnt/test/1",
+        "test_dataset2": "/mnt/test/2",
+        "test_dataset3": "/mnt/test/3",
+    }
+    mock_values["images"]["postgres_image"] = {"repository": "postgres", "tag": "17"}
+    mock_values["images"]["redis_image"] = {"repository": "valkey/valkey", "tag": "latest"}
+    mock_values["images"]["mariadb_image"] = {"repository": "mariadb", "tag": "latest"}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+
+    # fmt: off
+    volume_perms = {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}}
+    volume_no_perms = {"type": "volume", "volume_config": {"volume_name": "test_volume"}}
+    host_path_perms = {"type": "host_path", "host_path_config": {"path": "/mnt/test", "auto_permissions": True}}
+    host_path_no_perms = {"type": "host_path", "host_path_config": {"path": "/mnt/test"}}
+    host_path_acl_perms = {"type": "host_path", "host_path_config": {"acl": {"path": "/mnt/test"}, "acl_enable": True, "auto_permissions": True}} # noqa
+    ix_volume_no_perms = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "test_dataset1"}}
+    ix_volume_perms = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "test_dataset2", "auto_permissions": True}} # noqa
+    ix_volume_acl_perms = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "test_dataset3", "acl_enable": True, "auto_permissions": True}} # noqa
+    temp_volume = {"type": "temporary", "volume_config": {"volume_name": "test_temp_volume"}}
+    read_only_volume = {"type": "volume", "read_only": True, "volume_config": {"volume_name": "test_read_only_volume", "auto_permissions": True}} # noqa
+    # fmt: on
+
+    c1.add_storage("/some/path1", volume_perms)
+    c1.add_storage("/some/path2", volume_no_perms)
+    c1.add_storage("/some/path3", host_path_perms)
+    c1.add_storage("/some/path4", host_path_no_perms)
+    c1.add_storage("/some/path5", host_path_acl_perms)
+    c1.add_storage("/some/path6", ix_volume_no_perms)
+    c1.add_storage("/some/path7", ix_volume_perms)
+    c1.add_storage("/some/path8", ix_volume_acl_perms)
+    c1.add_storage("/some/path9", temp_volume)
+    c1.add_storage("/some/path10", read_only_volume)
+
+    perms_container = render.deps.perms("test_perms_container")
+    perms_container.add_or_skip_action("data", volume_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+    perms_container.add_or_skip_action("data2", volume_no_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+    perms_container.add_or_skip_action("data3", host_path_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+    perms_container.add_or_skip_action("data4", host_path_no_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+    perms_container.add_or_skip_action("data5", host_path_acl_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+    perms_container.add_or_skip_action("data6", ix_volume_no_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+    perms_container.add_or_skip_action("data7", ix_volume_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+    perms_container.add_or_skip_action("data8", ix_volume_acl_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+    perms_container.add_or_skip_action("data9", temp_volume, {"uid": 1000, "gid": 1000, "mode": "check"})
+    perms_container.add_or_skip_action("data10", read_only_volume, {"uid": 1000, "gid": 1000, "mode": "check"})
+    postgres = render.deps.postgres(
+        "postgres_container",
+        "postgres_image",
+        {
+            "user": "test_user",
+            "password": "test_password",
+            "database": "test_database",
+            "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+        },
+        perms_container,
+    )
+    redis = render.deps.redis(
+        "redis_container",
+        "redis_image",
+        {
+            "password": "test_password",
+            "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+        },
+        perms_container,
+    )
+    mariadb = render.deps.mariadb(
+        "mariadb_container",
+        "mariadb_image",
+        {
+            "user": "test_user",
+            "password": "test_password",
+            "database": "test_database",
+            "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+        },
+        perms_container,
+    )
+
+    if perms_container.has_actions():
+        perms_container.activate()
+        c1.depends.add_dependency("test_perms_container", "service_completed_successfully")
+        postgres.container.depends.add_dependency("test_perms_container", "service_completed_successfully")
+        redis.container.depends.add_dependency("test_perms_container", "service_completed_successfully")
+        mariadb.container.depends.add_dependency("test_perms_container", "service_completed_successfully")
+    output = render.render()
+    assert output["services"]["test_perms_container"]["network_mode"] == "none"
+    assert output["services"]["test_container"]["depends_on"] == {
+        "test_perms_container": {"condition": "service_completed_successfully"}
+    }
+    assert output["configs"]["permissions_run_script"]["content"] != ""
+    # fmt: off
+    content = [
+        {"read_only": False, "mount_path": "/mnt/permission/data", "is_temporary": False, "identifier": "data", "recursive": False, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+        {"read_only": False, "mount_path": "/mnt/permission/data3", "is_temporary": False, "identifier": "data3", "recursive": False, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+        {"read_only": False, "mount_path": "/mnt/permission/data6", "is_temporary": False, "identifier": "data6", "recursive": False, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+        {"read_only": False, "mount_path": "/mnt/permission/data7", "is_temporary": False, "identifier": "data7", "recursive": False, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+        {"read_only": False, "mount_path": "/mnt/permission/data9", "is_temporary": True, "identifier": "data9", "recursive": True, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+        {"read_only": True, "mount_path": "/mnt/permission/data10", "is_temporary": False, "identifier": "data10", "recursive": False, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+        {"read_only": False, "mount_path": "/mnt/permission/postgres_container_postgres_data", "is_temporary": False, "identifier": "postgres_container_postgres_data", "recursive": False, "mode": "check", "uid": 999, "gid": 999, "chmod": None}, # noqa
+        {"read_only": False, "mount_path": "/mnt/permission/redis_container_redis_data", "is_temporary": False, "identifier": "redis_container_redis_data", "recursive": False, "mode": "check", "uid": 568, "gid": 568, "chmod": None}, # noqa
+        {"read_only": False, "mount_path": "/mnt/permission/mariadb_container_mariadb_data", "is_temporary": False, "identifier": "mariadb_container_mariadb_data", "recursive": False, "mode": "check", "uid": 999, "gid": 999, "chmod": None}, # noqa
+    ]
+    # fmt: on
+    assert output["configs"]["permissions_actions_data"]["content"] == json.dumps(content)
+
+
+def test_add_duplicate_perms_action(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    vol_config = {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}}
+    c1.add_storage("/some/path", vol_config)
+    perms_container = render.deps.perms("test_perms_container")
+    perms_container.add_or_skip_action("data", vol_config, {"uid": 1000, "gid": 1000, "mode": "check"})
+    with pytest.raises(Exception):
+        perms_container.add_or_skip_action("data", vol_config, {"uid": 1000, "gid": 1000, "mode": "check"})
+
+
+def test_add_perm_action_without_auto_perms_enabled(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    vol_config = {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": False}}
+    c1.add_storage("/some/path", vol_config)
+    perms_container = render.deps.perms("test_perms_container")
+    perms_container.add_or_skip_action("data", vol_config, {"uid": 1000, "gid": 1000, "mode": "check"})
+    if perms_container.has_actions():
+        perms_container.activate()
+        c1.depends.add_dependency("test_perms_container", "service_completed_successfully")
+    output = render.render()
+    assert "configs" not in output
+    assert "ix-test_perms_container" not in output["services"]
+    assert "depends_on" not in output["services"]["test_container"]
+
+
+def test_add_unsupported_postgres_version(mock_values):
+    mock_values["images"]["pg_image"] = {"repository": "postgres", "tag": "99"}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        render.deps.postgres(
+            "test_container",
+            "test_image",
+            {"user": "test_user", "password": "test_password", "database": "test_database"},  # type: ignore
+        )
+
+
+def test_add_postgres_with_invalid_tag(mock_values):
+    mock_values["images"]["pg_image"] = {"repository": "postgres", "tag": "latest"}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        render.deps.postgres(
+            "pg_container",
+            "pg_image",
+            {"user": "test_user", "password": "test_password", "database": "test_database"},  # type: ignore
+        )
+
+
+def test_no_upgrade_container_with_non_postgres_image(mock_values):
+    mock_values["images"]["postgres_image"] = {"repository": "tensorchord/pgvecto-rs", "tag": "pg15-v0.2.0"}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    perms_container = render.deps.perms("test_perms_container")
+    pg = render.deps.postgres(
+        "postgres_container",
+        "postgres_image",
+        {
+            "user": "test_user",
+            "password": "test_password",
+            "database": "test_database",
+            "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+        },
+        perms_container,
+    )
+    if perms_container.has_actions():
+        perms_container.activate()
+        pg.add_dependency("test_perms_container", "service_completed_successfully")
+    output = render.render()
+    assert len(output["services"]) == 3  # c1, pg, perms
+    assert output["services"]["postgres_container"]["depends_on"] == {
+        "test_perms_container": {"condition": "service_completed_successfully"}
+    }
+
+
+def test_postgres_with_upgrade_container(mock_values):
+    mock_values["images"]["pg_image"] = {"repository": "postgres", "tag": 16.6}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    perms_container = render.deps.perms("test_perms_container")
+    pg = render.deps.postgres(
+        "postgres_container",
+        "pg_image",
+        {
+            "user": "test_user",
+            "password": "test_password",
+            "database": "test_database",
+            "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+        },
+        perms_container,
+    )
+    if perms_container.has_actions():
+        perms_container.activate()
+        pg.add_dependency("test_perms_container", "service_completed_successfully")
+    output = render.render()
+    pg = output["services"]["postgres_container"]
+    pgup = output["services"]["postgres_container_upgrade"]
+    assert pg["volumes"] == pgup["volumes"]
+    assert pg["user"] == pgup["user"]
+    assert pgup["environment"]["TARGET_VERSION"] == "16"
+    assert pgup["environment"]["DATA_DIR"] == "/var/lib/postgresql/data"
+    pgup_env = pgup["environment"]
+    pgup_env.pop("TARGET_VERSION")
+    pgup_env.pop("DATA_DIR")
+    assert pg["environment"] == pgup_env
+    assert pg["depends_on"] == {
+        "test_perms_container": {"condition": "service_completed_successfully"},
+        "postgres_container_upgrade": {"condition": "service_completed_successfully"},
+    }
+    assert pgup["depends_on"] == {"test_perms_container": {"condition": "service_completed_successfully"}}
+    assert pgup["restart"] == "on-failure:1"
+    assert pgup["healthcheck"] == {"disable": True}
+    assert pgup["image"] == "ixsystems/postgres-upgrade:1.0.1"
+    assert pgup["entrypoint"] == ["/bin/bash", "-c", "/upgrade.sh"]
+
+
+def test_add_mongodb(mock_values):
+    mock_values["images"]["mongodb_image"] = {"repository": "mongodb", "tag": "latest"}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    perms_container = render.deps.perms("perms_container")
+    m = render.deps.mongodb(
+        "mongodb_container",
+        "mongodb_image",
+        {
+            "user": "test_user",
+            "password": "test_password",
+            "database": "test_database",
+            "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+        },
+        perms_container,
+    )
+    if perms_container.has_actions():
+        perms_container.activate()
+        m.container.depends.add_dependency("perms_container", "service_completed_successfully")
+    output = render.render()
+    assert "devices" not in output["services"]["mongodb_container"]
+    assert "reservations" not in output["services"]["mongodb_container"]["deploy"]["resources"]
+    assert output["services"]["mongodb_container"]["image"] == "mongodb:latest"
+    assert output["services"]["mongodb_container"]["user"] == "999:999"
+    assert output["services"]["mongodb_container"]["deploy"]["resources"]["limits"]["cpus"] == "2.0"
+    assert output["services"]["mongodb_container"]["deploy"]["resources"]["limits"]["memory"] == "4096M"
+    assert output["services"]["mongodb_container"]["healthcheck"] == {
+        "test": [
+            "CMD",
+            "mongosh",
+            "--host",
+            "127.0.0.1",
+            "--port",
+            "27017",
+            "test_database",
+            "--eval",
+            'db.adminCommand("ping")',
+            "--quiet",
+        ],
+        "interval": "30s",
+        "timeout": "5s",
+        "retries": 5,
+        "start_period": "15s",
+        "start_interval": "2s",
+    }
+    assert output["services"]["mongodb_container"]["volumes"] == [
+        {
+            "type": "volume",
+            "source": "test_volume",
+            "target": "/data/db",
+            "read_only": False,
+            "volume": {"nocopy": False},
+        }
+    ]
+    assert output["services"]["mongodb_container"]["environment"] == {
+        "TZ": "Etc/UTC",
+        "UMASK": "002",
+        "UMASK_SET": "002",
+        "NVIDIA_VISIBLE_DEVICES": "void",
+        "MONGO_INITDB_ROOT_USERNAME": "test_user",
+        "MONGO_INITDB_ROOT_PASSWORD": "test_password",
+        "MONGO_INITDB_DATABASE": "test_database",
+    }
+    assert output["services"]["mongodb_container"]["depends_on"] == {
+        "perms_container": {"condition": "service_completed_successfully"}
+    }
+
+
+def test_add_mongodb_unsupported_repo(mock_values):
+    mock_values["images"]["mongo_image"] = {"repository": "unsupported_repo", "tag": "7"}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    perms_container = render.deps.perms("perms_container")
+    with pytest.raises(Exception):
+        render.deps.mongodb(
+            "mongo_container",
+            "mongo_image",
+            {
+                "user": "test_user",
+                "password": "test_@password",
+                "database": "test_database",
+                "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+            },
+            perms_container,
+        )
+
+
+def test_add_meilisearch(mock_values):
+    mock_values["images"]["meili_image"] = {"repository": "getmeili/meilisearch", "tag": "v1.17.0"}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    perms_container = render.deps.perms("perms_container")
+    m = render.deps.meilisearch(
+        "meili_container",
+        "meili_image",
+        {
+            "master_key": "test_master_key",
+            "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+        },
+        perms_container,
+    )
+    if perms_container.has_actions():
+        perms_container.activate()
+        m.container.depends.add_dependency("perms_container", "service_completed_successfully")
+    output = render.render()
+    assert "devices" not in output["services"]["meili_container"]
+    assert "reservations" not in output["services"]["meili_container"]["deploy"]["resources"]
+    assert output["services"]["meili_container"]["image"] == "getmeili/meilisearch:v1.17.0"
+    assert output["services"]["meili_container"]["user"] == "568:568"
+    assert output["services"]["meili_container"]["deploy"]["resources"]["limits"]["cpus"] == "2.0"
+    assert output["services"]["meili_container"]["deploy"]["resources"]["limits"]["memory"] == "4096M"
+    assert output["services"]["meili_container"]["healthcheck"] == {
+        "test": [
+            "CMD",
+            "curl",
+            "--request",
+            "GET",
+            "--silent",
+            "--output",
+            "/dev/null",
+            "--show-error",
+            "--fail",
+            "http://127.0.0.1:7700/health",
+        ],
+        "interval": "30s",
+        "timeout": "5s",
+        "retries": 5,
+        "start_period": "15s",
+        "start_interval": "2s",
+    }
+    assert output["services"]["meili_container"]["volumes"] == [
+        {
+            "type": "volume",
+            "source": "test_volume",
+            "target": "/meili_data",
+            "read_only": False,
+            "volume": {"nocopy": False},
+        }
+    ]
+    assert output["services"]["meili_container"]["environment"] == {
+        "TZ": "Etc/UTC",
+        "UMASK": "002",
+        "UMASK_SET": "002",
+        "NVIDIA_VISIBLE_DEVICES": "void",
+        "MEILI_MASTER_KEY": "test_master_key",
+        "MEILI_HTTP_ADDR": "0.0.0.0:7700",
+        "MEILI_NO_ANALYTICS": "true",
+        "MEILI_EXPERIMENTAL_DUMPLESS_UPGRADE": "true",
+    }
+    assert output["services"]["meili_container"]["depends_on"] == {
+        "perms_container": {"condition": "service_completed_successfully"}
+    }
+
+
+def test_add_meilisearch_unsupported_repo(mock_values):
+    mock_values["images"]["meili_image"] = {"repository": "unsupported_repo", "tag": "7"}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    perms_container = render.deps.perms("perms_container")
+    with pytest.raises(Exception):
+        render.deps.meilisearch(
+            "meili_container",
+            "meili_image",
+            {
+                "master_key": "test_master_key",
+                "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+            },
+            perms_container,
+        )
+
+
+def test_add_elasticsearch(mock_values):
+    mock_values["images"]["elastic_image"] = {
+        "repository": "docker.elastic.co/elasticsearch/elasticsearch",
+        "tag": "9.1.2",
+    }
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    perms_container = render.deps.perms("perms_container")
+    m = render.deps.elasticsearch(
+        "elastic_container",
+        "elastic_image",
+        {
+            "password": "test_password",
+            "node_name": "some_test_node",
+            "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+        },
+        perms_container,
+    )
+    if perms_container.has_actions():
+        perms_container.activate()
+        m.container.depends.add_dependency("perms_container", "service_completed_successfully")
+    output = render.render()
+    assert "devices" not in output["services"]["elastic_container"]
+    assert "reservations" not in output["services"]["elastic_container"]["deploy"]["resources"]
+    assert output["services"]["elastic_container"]["image"] == "docker.elastic.co/elasticsearch/elasticsearch:9.1.2"
+    assert output["services"]["elastic_container"]["user"] == "1000:1000"
+    assert output["services"]["elastic_container"]["deploy"]["resources"]["limits"]["cpus"] == "2.0"
+    assert output["services"]["elastic_container"]["deploy"]["resources"]["limits"]["memory"] == "4096M"
+    assert output["services"]["elastic_container"]["healthcheck"] == {
+        "test": [
+            "CMD",
+            "curl",
+            "--request",
+            "GET",
+            "--silent",
+            "--output",
+            "/dev/null",
+            "--show-error",
+            "--fail",
+            "--header",
+            "Authorization: Basic ZWxhc3RpYzp0ZXN0X3Bhc3N3b3Jk",
+            "http://127.0.0.1:9200/_cluster/health?local=true",
+        ],  # noqa
+        "interval": "30s",
+        "timeout": "5s",
+        "retries": 5,
+        "start_period": "15s",
+        "start_interval": "2s",
+    }
+    assert output["services"]["elastic_container"]["volumes"] == [
+        {
+            "type": "volume",
+            "source": "test_volume",
+            "target": "/usr/share/elasticsearch/data",
+            "read_only": False,
+            "volume": {"nocopy": False},
+        }
+    ]
+    assert output["services"]["elastic_container"]["environment"] == {
+        "TZ": "Etc/UTC",
+        "UMASK": "002",
+        "UMASK_SET": "002",
+        "NVIDIA_VISIBLE_DEVICES": "void",
+        "ELASTIC_PASSWORD": "test_password",
+        "http.port": "9200",
+        "path.data": "/usr/share/elasticsearch/data",
+        "path.repo": "/usr/share/elasticsearch/data/snapshots",
+        "node.name": "some_test_node",
+        "discovery.type": "single-node",
+        "xpack.security.enabled": "true",
+        "xpack.security.transport.ssl.enabled": "false",
+    }
+    assert output["services"]["elastic_container"]["depends_on"] == {
+        "perms_container": {"condition": "service_completed_successfully"}
+    }
+
+
+def test_add_elasticsearch_unsupported_repo(mock_values):
+    mock_values["images"]["elastic_image"] = {"repository": "unsupported_repo", "tag": "7"}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    perms_container = render.deps.perms("perms_container")
+    with pytest.raises(Exception):
+        render.deps.elasticsearch(
+            "elastic_container",
+            "elastic_image",
+            {
+                "password": "test_password",
+                "node_name": "some_test_node",
+                "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+            },
+            perms_container,
+        )

+ 150 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_device.py

@@ -0,0 +1,150 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+    return {
+        "images": {
+            "test_image": {
+                "repository": "nginx",
+                "tag": "latest",
+            }
+        },
+    }
+
+
+def test_add_device(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.devices.add_device("/h/dev/sda", "/c/dev/sda")
+    c1.devices.add_device("/h/dev/sdb", "/c/dev/sdb", "rwm")
+    output = render.render()
+    assert output["services"]["test_container"]["devices"] == ["/h/dev/sda:/c/dev/sda", "/h/dev/sdb:/c/dev/sdb:rwm"]
+
+
+def test_devices_without_host(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.devices.add_device("", "/c/dev/sda")
+
+
+def test_devices_without_container(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.devices.add_device("/h/dev/sda", "")
+
+
+def test_add_duplicate_device(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.devices.add_device("/h/dev/sda", "/c/dev/sda")
+    with pytest.raises(Exception):
+        c1.devices.add_device("/h/dev/sda", "/c/dev/sda")
+
+
+def test_add_device_with_invalid_container_path(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.devices.add_device("/h/dev/sda", "c/dev/sda")
+
+
+def test_add_device_with_invalid_host_path(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.devices.add_device("h/dev/sda", "/c/dev/sda")
+
+
+def test_add_disallowed_device(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.devices.add_device("/dev/dri", "/c/dev/sda")
+
+
+def test_add_device_with_invalid_cgroup_perm(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.devices.add_device("/h/dev/sda", "/c/dev/sda", "invalid")
+
+
+def test_automatically_add_gpu_devices(mock_values):
+    mock_values["resources"] = {"gpus": {"use_all_gpus": True}}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    output = render.render()
+    assert output["services"]["test_container"]["devices"] == ["/dev/dri:/dev/dri"]
+    assert output["services"]["test_container"]["group_add"] == [44, 107, 568]
+
+
+def test_automatically_add_gpu_devices_and_kfd(mock_values):
+    mock_values["resources"] = {"gpus": {"use_all_gpus": True, "kfd_device_exists": True}}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    output = render.render()
+    assert output["services"]["test_container"]["devices"] == ["/dev/dri:/dev/dri", "/dev/kfd:/dev/kfd"]
+    assert output["services"]["test_container"]["group_add"] == [44, 107, 568]
+
+
+def test_remove_gpu_devices(mock_values):
+    mock_values["resources"] = {"gpus": {"use_all_gpus": True}}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.devices.remove_devices()
+    output = render.render()
+    assert "devices" not in output["services"]["test_container"]
+    assert output["services"]["test_container"]["group_add"] == [568]
+
+
+def test_add_usb_bus(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.devices.add_usb_bus()
+    output = render.render()
+    assert output["services"]["test_container"]["devices"] == ["/dev/bus/usb:/dev/bus/usb"]
+
+
+def test_add_usb_bus_disallowed(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.devices.add_device("/dev/bus/usb", "/dev/bus/usb")
+
+
+def test_add_snd_device(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.add_snd_device()
+    output = render.render()
+    assert output["services"]["test_container"]["devices"] == ["/dev/snd:/dev/snd"]
+    assert output["services"]["test_container"]["group_add"] == [29, 568]
+
+
+def test_add_tun_device(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.add_tun_device()
+    output = render.render()
+    assert output["services"]["test_container"]["devices"] == ["/dev/net/tun:/dev/net/tun"]

+ 79 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_device_cgroup_rules.py

@@ -0,0 +1,79 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+    return {
+        "images": {
+            "test_image": {
+                "repository": "nginx",
+                "tag": "latest",
+            }
+        },
+    }
+
+
+def test_device_cgroup_rule(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.add_device_cgroup_rule("c 13:* rwm")
+    c1.add_device_cgroup_rule("b 10:20 rwm")
+    output = render.render()
+    assert output["services"]["test_container"]["device_cgroup_rules"] == [
+        "b 10:20 rwm",
+        "c 13:* rwm",
+    ]
+
+
+def test_device_cgroup_rule_duplicate(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.add_device_cgroup_rule("c 13:* rwm")
+    with pytest.raises(Exception):
+        c1.add_device_cgroup_rule("c 13:* rwm")
+
+
+def test_device_cgroup_rule_duplicate_group(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.add_device_cgroup_rule("c 13:* rwm")
+    with pytest.raises(Exception):
+        c1.add_device_cgroup_rule("c 13:* rm")
+
+
+def test_device_cgroup_rule_invalid_device(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.add_device_cgroup_rule("d 10:20 rwm")
+
+
+def test_device_cgroup_rule_invalid_perm(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.add_device_cgroup_rule("a 10:20 rwd")
+
+
+def test_device_cgroup_rule_invalid_format(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.add_device_cgroup_rule("a 10 20 rwd")
+
+
+def test_device_cgroup_rule_invalid_format_missing_major(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.add_device_cgroup_rule("a 10 rwd")

+ 64 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_dns.py

@@ -0,0 +1,64 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+    return {
+        "images": {
+            "test_image": {
+                "repository": "nginx",
+                "tag": "latest",
+            }
+        },
+    }
+
+
+def test_auto_add_dns_opts(mock_values):
+    mock_values["network"] = {"dns_opts": ["attempts:3", "opt1", "opt2"]}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    output = render.render()
+    assert output["services"]["test_container"]["dns_opt"] == ["attempts:3", "opt1", "opt2"]
+
+
+def test_auto_add_dns_searches(mock_values):
+    mock_values["network"] = {"dns_searches": ["search1", "search2"]}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    output = render.render()
+    assert output["services"]["test_container"]["dns_search"] == ["search1", "search2"]
+
+
+def test_auto_add_dns_nameservers(mock_values):
+    mock_values["network"] = {"dns_nameservers": ["nameserver1", "nameserver2"]}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    output = render.render()
+    assert output["services"]["test_container"]["dns"] == ["nameserver1", "nameserver2"]
+
+
+def test_add_duplicate_dns_nameservers(mock_values):
+    mock_values["network"] = {"dns_nameservers": ["nameserver1", "nameserver1"]}
+    render = Render(mock_values)
+    with pytest.raises(Exception):
+        render.add_container("test_container", "test_image")
+
+
+def test_add_duplicate_dns_searches(mock_values):
+    mock_values["network"] = {"dns_searches": ["search1", "search1"]}
+    render = Render(mock_values)
+    with pytest.raises(Exception):
+        render.add_container("test_container", "test_image")
+
+
+def test_add_duplicate_dns_opts(mock_values):
+    mock_values["network"] = {"dns_opts": ["attempts:3", "attempts:5"]}
+    render = Render(mock_values)
+    with pytest.raises(Exception):
+        render.add_container("test_container", "test_image")

+ 196 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_environment.py

@@ -0,0 +1,196 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+    return {
+        "images": {
+            "test_image": {
+                "repository": "nginx",
+                "tag": "latest",
+            }
+        },
+    }
+
+
+def test_auto_add_vars(mock_values):
+    mock_values["TZ"] = "Etc/UTC"
+    mock_values["run_as"] = {"user": "1000", "group": "1000"}
+    mock_values["resources"] = {
+        "gpus": {
+            "nvidia_gpu_selection": {
+                "pci_slot_0": {"uuid": "uuid_0", "use_gpu": True},
+                "pci_slot_1": {"uuid": "uuid_1", "use_gpu": True},
+            },
+        }
+    }
+
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    output = render.render()
+    envs = output["services"]["test_container"]["environment"]
+    assert len(envs) == 11
+    assert envs["TZ"] == "Etc/UTC"
+    assert envs["PUID"] == "1000"
+    assert envs["UID"] == "1000"
+    assert envs["USER_ID"] == "1000"
+    assert envs["PGID"] == "1000"
+    assert envs["GID"] == "1000"
+    assert envs["GROUP_ID"] == "1000"
+    assert envs["UMASK"] == "002"
+    assert envs["UMASK_SET"] == "002"
+    assert envs["NVIDIA_DRIVER_CAPABILITIES"] == "all"
+    assert envs["NVIDIA_VISIBLE_DEVICES"] == "uuid_0,uuid_1"
+
+
+def test_skip_generic_variables(mock_values):
+    mock_values["skip_generic_variables"] = True
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    output = render.render()
+    envs = output["services"]["test_container"]["environment"]
+
+    assert len(envs) == 1
+    assert envs["NVIDIA_VISIBLE_DEVICES"] == "void"
+
+
+def test_add_from_all_sources(mock_values):
+    mock_values["TZ"] = "Etc/UTC"
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.environment.add_env("APP_ENV", "test_value")
+    c1.environment.add_user_envs(
+        [
+            {"name": "USER_ENV", "value": "test_value2"},
+        ]
+    )
+    output = render.render()
+    envs = output["services"]["test_container"]["environment"]
+    assert envs["APP_ENV"] == "test_value"
+    assert envs["USER_ENV"] == "test_value2"
+    assert envs["TZ"] == "Etc/UTC"
+
+
+def test_user_add_vars(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.environment.add_user_envs(
+        [
+            {"name": "MY_ENV", "value": "test_value"},
+            {"name": "MY_ENV2", "value": "test_value2"},
+        ]
+    )
+    output = render.render()
+    envs = output["services"]["test_container"]["environment"]
+    assert envs["MY_ENV"] == "test_value"
+    assert envs["MY_ENV2"] == "test_value2"
+
+
+def test_user_add_duplicate_vars(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.environment.add_user_envs(
+            [
+                {"name": "MY_ENV", "value": "test_value"},
+                {"name": "MY_ENV", "value": "test_value2"},
+            ]
+        )
+
+
+def test_user_env_without_name(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.environment.add_user_envs(
+            [
+                {"name": "", "value": "test_value"},
+            ]
+        )
+
+
+def test_user_env_try_to_overwrite_auto_vars(mock_values):
+    mock_values["TZ"] = "Etc/UTC"
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.environment.add_user_envs(
+        [
+            {"name": "TZ", "value": "test_value"},
+        ]
+    )
+    with pytest.raises(Exception):
+        render.render()
+
+
+def test_user_env_try_to_overwrite_app_dev_vars(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.environment.add_user_envs(
+        [
+            {"name": "PORT", "value": "test_value"},
+        ]
+    )
+    c1.environment.add_env("PORT", "test_value2")
+    with pytest.raises(Exception):
+        render.render()
+
+
+def test_app_dev_vars_try_to_overwrite_auto_vars(mock_values):
+    mock_values["TZ"] = "Etc/UTC"
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.environment.add_env("TZ", "test_value")
+    with pytest.raises(Exception):
+        render.render()
+
+
+def test_app_dev_no_name(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.environment.add_env("", "test_value")
+
+
+def test_app_dev_duplicate_vars(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.environment.add_env("PORT", "test_value")
+    with pytest.raises(Exception):
+        c1.environment.add_env("PORT", "test_value2")
+
+
+def test_format_vars(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.environment.add_env("APP_ENV", "test_$value")
+    c1.environment.add_env("APP_ENV_BOOL", True)
+    c1.environment.add_env("APP_ENV_INT", 10)
+    c1.environment.add_env("APP_ENV_FLOAT", 10.5)
+    c1.environment.add_user_envs(
+        [
+            {"name": "USER_ENV", "value": "test_$value2"},
+        ]
+    )
+
+    output = render.render()
+    envs = output["services"]["test_container"]["environment"]
+    assert envs["APP_ENV"] == "test_$$value"
+    assert envs["USER_ENV"] == "test_$$value2"
+    assert envs["APP_ENV_BOOL"] == "true"
+    assert envs["APP_ENV_INT"] == "10"
+    assert envs["APP_ENV_FLOAT"] == "10.5"

+ 46 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_expose.py

@@ -0,0 +1,46 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+    return {
+        "images": {
+            "test_image": {
+                "repository": "nginx",
+                "tag": "latest",
+            }
+        },
+    }
+
+
+def test_add_expose_ports(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.expose.add_port(8081)
+    c1.expose.add_port(8081, "udp")
+    c1.expose.add_port(8082, "udp")
+    output = render.render()
+    assert output["services"]["test_container"]["expose"] == ["8081/tcp", "8081/udp", "8082/udp"]
+
+
+def test_add_duplicate_expose_ports(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.expose.add_port(8081)
+    with pytest.raises(Exception):
+        c1.expose.add_port(8081)
+
+
+def test_add_expose_ports_with_host_network(mock_values):
+    mock_values["network"] = {"host_network": True}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.expose.add_port(8081)
+    output = render.render()
+    assert "expose" not in output["services"]["test_container"]

+ 57 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_extra_hosts.py

@@ -0,0 +1,57 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+    return {
+        "images": {
+            "test_image": {
+                "repository": "nginx",
+                "tag": "latest",
+            }
+        },
+    }
+
+
+def test_add_extra_host(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.add_extra_host("test_host", "127.0.0.1")
+    c1.add_extra_host("test_host2", "127.0.0.2")
+    c1.add_extra_host("host.docker.internal", "host-gateway")
+    output = render.render()
+    assert output["services"]["test_container"]["extra_hosts"] == {
+        "host.docker.internal": "host-gateway",
+        "test_host": "127.0.0.1",
+        "test_host2": "127.0.0.2",
+    }
+
+
+def test_add_duplicate_extra_host(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.add_extra_host("test_host", "127.0.0.1")
+    with pytest.raises(Exception):
+        c1.add_extra_host("test_host", "127.0.0.2")
+
+
+def test_add_extra_host_with_ipv6(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.add_extra_host("test_host", "::1")
+    output = render.render()
+    assert output["services"]["test_container"]["extra_hosts"] == {"test_host": "::1"}
+
+
+def test_add_extra_host_with_invalid_ip(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.add_extra_host("test_host", "invalid_ip")

+ 13 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_formatter.py

@@ -0,0 +1,13 @@
+from formatter import escape_dollar
+
+
+def test_escape_dollar():
+    cases = [
+        {"input": "test", "expected": "test"},
+        {"input": "$test", "expected": "$$test"},
+        {"input": "$$test", "expected": "$$$$test"},
+        {"input": "$$$test", "expected": "$$$$$$test"},
+        {"input": "$test$", "expected": "$$test$$"},
+    ]
+    for case in cases:
+        assert escape_dollar(case["input"]) == case["expected"]

+ 133 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_functions.py

@@ -0,0 +1,133 @@
+import re
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+    return {
+        "images": {
+            "test_image": {
+                "repository": "nginx",
+                "tag": "latest",
+            }
+        },
+    }
+
+
+def test_funcs(mock_values):
+    mock_values["ix_volumes"] = {"test": "/mnt/test123"}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+
+    tests = [
+        {"func": "auto_cast", "values": ["1"], "expected": 1},
+        {"func": "auto_cast", "values": ["TrUe"], "expected": True},
+        {"func": "auto_cast", "values": ["FaLsE"], "expected": False},
+        {"func": "auto_cast", "values": ["0.2"], "expected": 0.2},
+        {"func": "auto_cast", "values": [True], "expected": True},
+        {"func": "basic_auth_header", "values": ["my_user", "my_pass"], "expected": "Basic bXlfdXNlcjpteV9wYXNz"},
+        {"func": "basic_auth", "values": ["my_user", "my_pass"], "expected": "bXlfdXNlcjpteV9wYXNz"},
+        {
+            "func": "bcrypt_hash",
+            "values": ["my_pass"],
+            "expect_regex": r"^\$2b\$12\$[a-zA-Z0-9-_\.\/]+$",
+        },
+        {"func": "camel_case", "values": ["my_user"], "expected": "My_User"},
+        {"func": "copy_dict", "values": [{"a": 1}], "expected": {"a": 1}},
+        {"func": "fail", "values": ["my_message"], "expect_raise": True},
+        {
+            "func": "htpasswd",
+            "values": ["my_user", "my_pass"],
+            "expect_regex": r"^my_user:\$2b\$12\$[a-zA-Z0-9-_\.\/]+$",
+        },
+        {"func": "is_boolean", "values": ["true"], "expected": True},
+        {"func": "is_boolean", "values": ["false"], "expected": True},
+        {"func": "is_number", "values": ["1"], "expected": True},
+        {"func": "is_number", "values": ["1.1"], "expected": True},
+        {"func": "match_regex", "values": ["value", "^[a-zA-Z0-9]+$"], "expected": True},
+        {"func": "match_regex", "values": ["value", "^[0-9]+$"], "expected": False},
+        {"func": "merge_dicts", "values": [{"a": 1}, {"b": 2}], "expected": {"a": 1, "b": 2}},
+        {"func": "must_match_regex", "values": ["my_user", "^[0-9]$"], "expect_raise": True},
+        {"func": "must_match_regex", "values": ["1", "^[0-9]$"], "expected": "1"},
+        {"func": "secure_string", "values": [10], "expect_regex": r"^[a-zA-Z0-9-_]+$"},
+        {"func": "disallow_chars", "values": ["my_user", ["$", "@"], "my_key"], "expected": "my_user"},
+        {"func": "disallow_chars", "values": ["my_user$", ["$", "@"], "my_key"], "expect_raise": True},
+        {
+            "func": "get_host_path",
+            "values": [{"type": "host_path", "host_path_config": {"path": "/mnt/test"}}],
+            "expected": "/mnt/test",
+        },
+        {
+            "func": "get_host_path",
+            "values": [{"type": "ix_volume", "ix_volume_config": {"dataset_name": "test"}}],
+            "expected": "/mnt/test123",
+        },
+        {"func": "or_default", "values": [None, 1], "expected": 1},
+        {"func": "or_default", "values": [1, None], "expected": 1},
+        {"func": "or_default", "values": [False, 1], "expected": 1},
+        {"func": "or_default", "values": [True, 1], "expected": True},
+        {"func": "temp_config", "values": [""], "expect_raise": True},
+        {
+            "func": "temp_config",
+            "values": ["test"],
+            "expected": {"type": "temporary", "volume_config": {"volume_name": "test"}},
+        },
+        {"func": "require_unique", "values": [["a=1", "b=2", "c"], "values.key", "="], "expected": None},
+        {
+            "func": "require_unique",
+            "values": [["a=1", "b=2", "b=3"], "values.key", "="],
+            "expect_raise": True,
+        },
+        {
+            "func": "require_no_reserved",
+            "values": [["a=1", "b=2", "c"], "values.key", ["d"], "="],
+            "expected": None,
+        },
+        {
+            "func": "require_no_reserved",
+            "values": [["a=1", "b=2", "c"], "values.key", ["a"], "="],
+            "expect_raise": True,
+        },
+        {
+            "func": "require_no_reserved",
+            "values": [["a=1", "b=2", "c"], "values.key", ["b"], "=", True],
+            "expect_raise": True,
+        },
+        {
+            "func": "url_encode",
+            "values": ["7V!@@%%63r@a5#e!2X9!68g4b"],
+            "expected": "7V%21%40%40%25%2563r%40a5%23e%212X9%2168g4b",
+        },
+        {
+            "func": "url_to_dict",
+            "values": ["192.168.1.1:8080"],
+            "expected": {"host": "192.168.1.1", "port": 8080},
+        },
+        {
+            "func": "url_to_dict",
+            "values": ["[::]:8080"],
+            "expected": {"host": "::", "port": 8080},
+        },
+        {
+            "func": "url_to_dict",
+            "values": ["[::]:8080", True],
+            "expected": {"host": "[::]", "port": 8080, "host_no_brackets": "::"},
+        },
+    ]
+
+    for test in tests:
+        print(test["func"], test)
+        func = render.funcs[test["func"]]
+        if test.get("expect_raise", False):
+            with pytest.raises(Exception):
+                func(*test["values"])
+        elif test.get("expect_regex"):
+            r = func(*test["values"])
+            assert re.match(test["expect_regex"], r) is not None
+        else:
+            r = func(*test["values"])
+            assert r == test["expected"]

+ 353 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_healthcheck.py

@@ -0,0 +1,353 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+    return {
+        "images": {
+            "test_image": {
+                "repository": "nginx",
+                "tag": "latest",
+            }
+        },
+    }
+
+
+def test_disable_healthcheck(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    output = render.render()
+    assert output["services"]["test_container"]["healthcheck"] == {"disable": True}
+
+
+def test_use_built_in_healthcheck(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.use_built_in()
+    output = render.render()
+    assert "healthcheck" not in output["services"]["test_container"]
+
+
+def test_set_custom_test(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.set_custom_test("echo $1")
+    output = render.render()
+    assert output["services"]["test_container"]["healthcheck"] == {
+        "test": "echo $$1",
+        "interval": "30s",
+        "timeout": "5s",
+        "retries": 5,
+        "start_period": "15s",
+        "start_interval": "2s",
+    }
+
+
+def test_set_custom_test_array(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.set_custom_test(["CMD", "echo", "1"])
+    output = render.render()
+    assert output["services"]["test_container"]["healthcheck"] == {
+        "test": ["CMD", "echo", "1"],
+        "interval": "30s",
+        "timeout": "5s",
+        "retries": 5,
+        "start_period": "15s",
+        "start_interval": "2s",
+    }
+
+
+def test_CMD_with_var_should_fail(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    with pytest.raises(Exception):
+        c1.healthcheck.set_custom_test(["CMD", "echo", "$1"])
+
+
+def test_set_options(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.set_custom_test(["CMD", "echo", "123$567"])
+    c1.healthcheck.set_interval(9)
+    c1.healthcheck.set_timeout(8)
+    c1.healthcheck.set_retries(7)
+    c1.healthcheck.set_start_period(6)
+    c1.healthcheck.set_start_interval(5)
+    output = render.render()
+    assert output["services"]["test_container"]["healthcheck"] == {
+        "test": ["CMD", "echo", "123$$567"],
+        "interval": "9s",
+        "timeout": "8s",
+        "retries": 7,
+        "start_period": "6s",
+        "start_interval": "5s",
+    }
+
+
+def test_adding_test_when_disabled(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.healthcheck.set_custom_test("echo $1")
+
+
+def test_not_adding_test(mock_values):
+    render = Render(mock_values)
+    render.add_container("test_container", "test_image")
+    with pytest.raises(Exception):
+        render.render()
+
+
+def test_invalid_path(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    with pytest.raises(Exception):
+        c1.healthcheck.set_test("http", {"port": 8080, "path": "invalid"})
+
+
+def test_http_healthcheck(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.set_test("http", {"port": 8080})
+    output = render.render()
+    assert output["services"]["test_container"]["healthcheck"]["test"] == [
+        "CMD-SHELL",
+        f"""/bin/bash -c 'exec {{hc_fd}}<>/dev/tcp/127.0.0.1/8080 && echo -e "GET / HTTP/1.1\\r\\nHost: 127.0.0.1\\r\\nConnection: close\\r\\n\\r\\n" >&$${{hc_fd}} && cat <&$${{hc_fd}} | grep "HTTP" | grep -q "200"'""",  # noqa
+    ]
+
+
+def test_curl_healthcheck_as_CMD(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.set_test("curl", {"port": 8080, "path": "/health", "data": {"test": "val"}, "exec_type": "CMD"})
+    output = render.render()
+    assert output["services"]["test_container"]["healthcheck"]["test"] == [
+        "CMD",
+        "curl",
+        "--request",
+        "GET",
+        "--silent",
+        "--output",
+        "/dev/null",
+        "--show-error",
+        "--fail",
+        "--data",
+        '{"test": "val"}',
+        "http://127.0.0.1:8080/health",
+    ]
+
+
+def test_curl_healthcheck(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.set_test("curl", {"port": 8080, "path": "/health", "data": {"test": "val"}})
+    output = render.render()
+    assert output["services"]["test_container"]["healthcheck"]["test"] == [
+        "CMD",
+        "curl",
+        "--request",
+        "GET",
+        "--silent",
+        "--output",
+        "/dev/null",
+        "--show-error",
+        "--fail",
+        "--data",
+        '{"test": "val"}',
+        "http://127.0.0.1:8080/health",
+    ]
+
+
+def test_curl_healthcheck_with_headers_and_method_and_data(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.set_test(
+        "curl", {"port": 8080, "path": "/health", "method": "POST", "headers": [("X-Test", "some-value")], "data": {}}
+    )
+    output = render.render()
+    assert output["services"]["test_container"]["healthcheck"]["test"] == [
+        "CMD",
+        "curl",
+        "--request",
+        "POST",
+        "--silent",
+        "--output",
+        "/dev/null",
+        "--show-error",
+        "--fail",
+        "--header",
+        "X-Test: some-value",
+        "--data",
+        "{}",
+        "http://127.0.0.1:8080/health",
+    ]
+
+
+def test_wget_healthcheck(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.set_test("wget", {"port": 8080, "path": "/health"})
+    output = render.render()
+    assert output["services"]["test_container"]["healthcheck"]["test"] == [
+        "CMD",
+        "wget",
+        "--quiet",
+        "--spider",
+        "http://127.0.0.1:8080/health",
+    ]
+
+
+def test_wget_healthcheck_no_spider(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.set_test("wget", {"port": 8080, "path": "/health", "spider": False})
+    output = render.render()
+    assert output["services"]["test_container"]["healthcheck"]["test"] == [
+        "CMD",
+        "wget",
+        "--quiet",
+        "-O",
+        "/dev/null",
+        "http://127.0.0.1:8080/health",
+    ]
+
+
+def test_netcat_healthcheck(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.set_test("netcat", {"port": 8080})
+    output = render.render()
+    assert output["services"]["test_container"]["healthcheck"]["test"] == [
+        "CMD",
+        "nc",
+        "-z",
+        "-w",
+        "1",
+        "127.0.0.1",
+        "8080",
+    ]
+
+
+def test_netcat_udp_healthcheck(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.set_test("netcat", {"port": 8080, "udp": True})
+    output = render.render()
+    assert output["services"]["test_container"]["healthcheck"]["test"] == [
+        "CMD",
+        "nc",
+        "-z",
+        "-w",
+        "1",
+        "-u",
+        "127.0.0.1",
+        "8080",
+    ]
+
+
+def test_tcp_healthcheck(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.set_test("tcp", {"port": 8080})
+    output = render.render()
+    assert output["services"]["test_container"]["healthcheck"]["test"] == [
+        "CMD",
+        "timeout",
+        "1",
+        "bash",
+        "-c",
+        "cat < /dev/null > /dev/tcp/127.0.0.1/8080",
+    ]
+
+
+def test_redis_healthcheck(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.set_test("redis", {"password": "test"})
+    output = render.render()
+    assert output["services"]["test_container"]["healthcheck"]["test"] == [
+        "CMD",
+        "redis-cli",
+        "-h",
+        "127.0.0.1",
+        "-p",
+        "6379",
+        "-a",
+        "test",
+        "ping",
+    ]
+
+
+def test_redis_healthcheck_no_password(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.set_test("redis", {"password": ""})
+    output = render.render()
+    assert output["services"]["test_container"]["healthcheck"]["test"] == [
+        "CMD",
+        "redis-cli",
+        "-h",
+        "127.0.0.1",
+        "-p",
+        "6379",
+        "ping",
+    ]
+
+
+def test_postgres_healthcheck(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.set_test("postgres", {"user": "test-user", "db": "test-db"})
+    output = render.render()
+    assert output["services"]["test_container"]["healthcheck"]["test"] == [
+        "CMD",
+        "pg_isready",
+        "-h",
+        "127.0.0.1",
+        "-p",
+        "5432",
+        "-U",
+        "test-user",
+        "-d",
+        "test-db",
+    ]
+
+
+def test_mariadb_healthcheck(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.set_test("mariadb", {"password": "test-pass"})
+    output = render.render()
+    assert output["services"]["test_container"]["healthcheck"]["test"] == [
+        "CMD",
+        "mariadb-admin",
+        "--user=root",
+        "--host=127.0.0.1",
+        "--port=3306",
+        "--password=test-pass",
+        "ping",
+    ]
+
+
+def test_mongodb_healthcheck(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.set_test("mongodb", {"db": "test-db"})
+    output = render.render()
+    assert output["services"]["test_container"]["healthcheck"]["test"] == [
+        "CMD",
+        "mongosh",
+        "--host",
+        "127.0.0.1",
+        "--port",
+        "27017",
+        "test-db",
+        "--eval",
+        'db.adminCommand("ping")',
+        "--quiet",
+    ]

+ 88 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_labels.py

@@ -0,0 +1,88 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+    return {
+        "images": {
+            "test_image": {
+                "repository": "nginx",
+                "tag": "latest",
+            }
+        },
+    }
+
+
+def test_add_disallowed_label(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.labels.add_label("com.docker.compose.service", "test_service")
+
+
+def test_add_duplicate_label(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.labels.add_label("my.custom.label", "test_value")
+    with pytest.raises(Exception):
+        c1.labels.add_label("my.custom.label", "test_value1")
+
+
+def test_add_label_on_non_existing_container(mock_values):
+    mock_values["labels"] = [
+        {
+            "key": "my.custom.label1",
+            "value": "test_value1",
+            "containers": ["test_container", "test_container2"],
+        },
+    ]
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        render.render()
+
+
+def test_add_label(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.labels.add_label("my.custom.label1", "test_value1")
+    c1.labels.add_label("my.custom.label2", "test_value2")
+    output = render.render()
+    assert output["services"]["test_container"]["labels"] == {
+        "my.custom.label1": "test_value1",
+        "my.custom.label2": "test_value2",
+    }
+
+
+def test_auto_add_labels(mock_values):
+    mock_values["labels"] = [
+        {
+            "key": "my.custom.label1",
+            "value": "test_value1",
+            "containers": ["test_container", "test_container2"],
+        },
+        {
+            "key": "my.custom.label2",
+            "value": "test_value2",
+            "containers": ["test_container"],
+        },
+    ]
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c2 = render.add_container("test_container2", "test_image")
+    c1.healthcheck.disable()
+    c2.healthcheck.disable()
+    output = render.render()
+    assert output["services"]["test_container"]["labels"] == {
+        "my.custom.label1": "test_value1",
+        "my.custom.label2": "test_value2",
+    }
+    assert output["services"]["test_container2"]["labels"] == {
+        "my.custom.label1": "test_value1",
+    }

+ 230 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_notes.py

@@ -0,0 +1,230 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+    return {
+        "ix_context": {
+            "app_metadata": {
+                "name": "test_app",
+                "title": "Test App",
+                "train": "enterprise",
+            }
+        },
+        "images": {
+            "test_image": {
+                "repository": "nginx",
+                "tag": "latest",
+            }
+        },
+    }
+
+
+def test_notes(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    output = render.render()
+    assert (
+        output["x-notes"]
+        == """# Test App
+
+## Security
+
+### Container: [test_container]
+
+- Is running as unknown user
+- Is running as unknown group
+
+## 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://ixsystems.atlassian.net
+
+"""
+    )
+
+
+def test_notes_on_non_enterprise_train(mock_values):
+    mock_values["ix_context"]["app_metadata"]["train"] = "community"
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.set_user(568, 568)
+    c1.healthcheck.disable()
+    output = render.render()
+    assert (
+        output["x-notes"]
+        == """# Test App
+
+## 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
+
+"""
+    )
+
+
+def test_notes_with_warnings(mock_values):
+    render = Render(mock_values)
+    render.notes.add_warning("this is not properly configured. fix it now!")
+    render.notes.add_warning("that is not properly configured. fix it later!")
+    c1 = render.add_container("test_container", "test_image")
+    c1.set_user(568, 568)
+    c1.healthcheck.disable()
+    output = render.render()
+    assert (
+        output["x-notes"]
+        == """# Test App
+
+## Warnings
+
+- this is not properly configured. fix it now!
+- that is not properly configured. fix it later!
+
+## 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://ixsystems.atlassian.net
+
+"""
+    )
+
+
+def test_notes_with_deprecations(mock_values):
+    render = Render(mock_values)
+    render.notes.add_deprecation("this is will be removed later. fix it now!")
+    render.notes.add_deprecation("that is will be removed later. fix it later!")
+    c1 = render.add_container("test_container", "test_image")
+    c1.set_user(568, 568)
+    c1.healthcheck.disable()
+    output = render.render()
+    assert (
+        output["x-notes"]
+        == """# Test App
+
+## Deprecations
+
+- this is will be removed later. fix it now!
+- that is will be removed later. fix it later!
+
+## 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://ixsystems.atlassian.net
+
+"""
+    )
+
+
+def test_notes_with_body(mock_values):
+    render = Render(mock_values)
+    render.notes.set_body(
+        """## Additional info
+
+Some info
+some other info.
+"""
+    )
+    c1 = render.add_container("test_container", "test_image")
+    c1.set_user(568, 568)
+    c1.healthcheck.disable()
+    output = render.render()
+    assert (
+        output["x-notes"]
+        == """# Test App
+
+## Additional info
+
+Some info
+some other info.
+
+## 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://ixsystems.atlassian.net
+
+"""
+    )
+
+
+def test_notes_all(mock_values):
+    render = Render(mock_values)
+    render.notes.add_warning("this is not properly configured. fix it now!")
+    render.notes.add_warning("that is not properly configured. fix it later!")
+    render.notes.add_deprecation("this is will be removed later. fix it now!")
+    render.notes.add_deprecation("that is will be removed later. fix it later!")
+    render.notes.set_body(
+        """## Additional info
+
+Some info
+some other info.
+"""
+    )
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.set_privileged(True)
+    c1.set_user(0, 0)
+    c1.add_group(0)
+    c1.set_ipc_mode("host")
+    c1.set_pid_mode("host")
+    c1.set_cgroup("host")
+    c1.set_tty(True)
+    c1.remove_security_opt("no-new-privileges")
+    c1.restart.set_policy("on-failure", 1)
+
+    c2 = render.add_container("test_container2", "test_image")
+    c2.healthcheck.disable()
+    c2.set_user(568, 568)
+
+    c3 = render.add_container("test_container3", "test_image")
+    c3.healthcheck.disable()
+    c3.restart.set_policy("on-failure", 1)
+    c3.set_user(568, 568)
+
+    output = render.render()
+    assert (
+        output["x-notes"]
+        == """# Test App
+
+## Warnings
+
+- Container [test_container] is running with a TTY, Logs will not appear correctly in the UI due to an [upstream bug](https://github.com/docker/docker-py/issues/1394)
+- this is not properly configured. fix it now!
+- that is not properly configured. fix it later!
+
+## Deprecations
+
+- this is will be removed later. fix it now!
+- that is will be removed later. fix it later!
+
+## Security
+
+### Container: [test_container]
+
+**This container is short-lived.**
+
+- Is running with privileged mode enabled
+- Is running as root user
+- Is running as root group
+- Is running with supplementary root group
+- Is running with host IPC namespace
+- Is running with host PID namespace
+- Is running with host cgroup namespace
+- Is running without [no-new-privileges] security option
+
+## Additional info
+
+Some info
+some other info.
+
+## 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://ixsystems.atlassian.net
+
+"""  # noqa
+    )

+ 93 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_portal.py

@@ -0,0 +1,93 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+    return {
+        "images": {
+            "test_image": {
+                "repository": "nginx",
+                "tag": "latest",
+            }
+        },
+    }
+
+
+def test_no_portals(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    output = render.render()
+    assert output["x-portals"] == []
+
+
+def test_add_portal_with_host_ips(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    port1 = {"bind_mode": "published", "port_number": 8080, "host_ips": ["1.2.3.4", "5.6.7.8"]}
+    port2 = {"bind_mode": "published", "port_number": 8081, "host_ips": ["::", "0.0.0.0"]}
+    port3 = {"bind_mode": "published", "port_number": 8081, "host_ips": ["1.2.3.4"]}
+    port3 = {"bind_mode": "published", "port_number": 8081, "host_ips": ["1.2.3.4"]}
+    port4 = {"bind_mode": "exposed", "port_number": 1234, "host_ips": ["1.2.3.4"]}
+    render.portals.add(port1)
+    render.portals.add(port1, {"name": "test1", "host": "my-host.com"})
+    render.portals.add(port2, {"name": "test2"})
+    render.portals.add(port3, {"name": "test3", "port": None})
+    render.portals.add(port3, {"name": "test4", "port": 1234})
+    render.portals.add(port4, {"name": "test5", "port": 1234})
+    output = render.render()
+    assert output["x-portals"] == [
+        {"name": "Web UI", "scheme": "http", "host": "1.2.3.4", "port": 8080, "path": "/"},
+        {"name": "test1", "scheme": "http", "host": "my-host.com", "port": 8080, "path": "/"},
+        {"name": "test2", "scheme": "http", "host": "0.0.0.0", "port": 8081, "path": "/"},
+        {"name": "test3", "scheme": "http", "host": "1.2.3.4", "port": 8081, "path": "/"},
+        {"name": "test4", "scheme": "http", "host": "1.2.3.4", "port": 1234, "path": "/"},
+    ]
+
+
+def test_add_duplicate_portal(mock_values):
+    render = Render(mock_values)
+    port = {"bind_mode": "published", "port_number": 8080, "host_ips": ["1.2.3.4", "5.6.7.8"]}
+    render.portals.add(port)
+    with pytest.raises(Exception):
+        render.portals.add(port)
+
+
+def test_add_duplicate_portal_with_explicit_name(mock_values):
+    render = Render(mock_values)
+    port = {"bind_mode": "published", "port_number": 8080, "host_ips": ["1.2.3.4", "5.6.7.8"]}
+    render.portals.add(port, {"name": "Some Portal"})
+    with pytest.raises(Exception):
+        render.portals.add(port, {"name": "Some Portal"})
+
+
+def test_add_portal_with_invalid_scheme(mock_values):
+    render = Render(mock_values)
+    port = {"bind_mode": "published", "port_number": 8080, "host_ips": ["1.2.3.4", "5.6.7.8"]}
+    with pytest.raises(Exception):
+        render.portals.add(port, {"scheme": "invalid_scheme"})
+
+
+def test_add_portal_with_invalid_path(mock_values):
+    render = Render(mock_values)
+    port = {"bind_mode": "published", "port_number": 8080, "host_ips": ["1.2.3.4", "5.6.7.8"]}
+    with pytest.raises(Exception):
+        render.portals.add(port, {"path": "invalid_path"})
+
+
+def test_add_portal_with_invalid_path_double_slash(mock_values):
+    render = Render(mock_values)
+    port = {"bind_mode": "published", "port_number": 8080, "host_ips": ["1.2.3.4", "5.6.7.8"]}
+    with pytest.raises(Exception):
+        render.portals.add(port, {"path": "/some//path"})
+
+
+def test_add_portal_with_invalid_port(mock_values):
+    render = Render(mock_values)
+    port = {"bind_mode": "published", "port_number": 8080, "host_ips": ["1.2.3.4", "5.6.7.8"]}
+    with pytest.raises(Exception):
+        render.portals.add(port, {"port": -1})

+ 383 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_ports.py

@@ -0,0 +1,383 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+    return {
+        "images": {
+            "test_image": {
+                "repository": "nginx",
+                "tag": "latest",
+            }
+        },
+    }
+
+
+tests = [
+    {
+        "name": "add_ports_should_work",
+        "inputs": [
+            {
+                "values": ({"bind_mode": "published", "port_number": 8081}, {"container_port": 8080}),
+                "expect_error": False,
+            },
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8082},
+                    {"container_port": 8080, "protocol": "udp"},
+                ),
+                "expect_error": False,
+            },
+        ],
+        "expected": [
+            {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"},
+            {"published": 8082, "target": 8080, "protocol": "udp", "mode": "ingress"},
+        ],
+    },
+    {
+        "name": "add_duplicate_ports_should_fail",
+        "inputs": [
+            {
+                "values": ({"bind_mode": "published", "port_number": 8081}, {"container_port": 8080}),
+                "expect_error": False,
+            },
+            {
+                "values": ({"bind_mode": "published", "port_number": 8081}, {"container_port": 8080}),
+                "expect_error": True,
+            },
+        ],
+    },
+    {
+        "name": "adding_duplicate_port_different_protocol_should_work",
+        "inputs": [
+            {
+                "values": ({"bind_mode": "published", "port_number": 8081}, {"container_port": 8080}),
+                "expect_error": False,
+            },
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "protocol": "udp"},
+                ),
+                "expect_error": False,
+            },
+        ],
+        "expected": [
+            {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"},
+            {"published": 8081, "target": 8080, "protocol": "udp", "mode": "ingress"},
+        ],
+    },
+    {
+        "name": "adding_same_port_for_both_wildcard_families_should_work",
+        "inputs": [
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "host_ips": ["0.0.0.0"]},
+                ),
+                "expect_error": False,
+            },
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "host_ips": ["::"]},
+                ),
+                "expect_error": False,
+            },
+        ],
+        "expected": [
+            {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"},
+        ],
+    },
+    {
+        "name": "adding_duplicate_port_for_v4_ip_and_v4_wildcard_should_fail",
+        "inputs": [
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "host_ips": ["192.168.1.10"]},
+                ),
+                "expect_error": False,
+            },
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "host_ips": ["0.0.0.0"]},
+                ),
+                "expect_error": True,
+            },
+        ],
+    },
+    {
+        "name": "adding_duplicate_port_for_v4_wildcard_and_v4_ip_should_fail",
+        "inputs": [
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "host_ips": ["0.0.0.0"]},
+                ),
+                "expect_error": False,
+            },
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "host_ips": ["192.168.1.10"]},
+                ),
+                "expect_error": True,
+            },
+        ],
+    },
+    {
+        "name": "adding_duplicate_port_for_v4_wildcard_and_v6_ip_should_work",
+        "inputs": [
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "host_ips": ["192.168.1.10"]},
+                ),
+                "expect_error": False,
+            },
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "host_ips": ["fd00:1234:5678:abcd::10"]},
+                ),
+                "expect_error": False,
+            },
+        ],
+        "expected": [
+            {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"},
+            {
+                "published": 8081,
+                "target": 8080,
+                "protocol": "tcp",
+                "mode": "ingress",
+                "host_ip": "fd00:1234:5678:abcd::10",
+            },
+        ],
+    },
+    {
+        "name": "adding_duplicate_port_for_v6_wildcard_and_v4_ip_should_work",
+        "inputs": [
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "host_ips": ["::"]},
+                ),
+                "expect_error": False,
+            },
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "host_ips": ["192.168.1.10"]},
+                ),
+                "expect_error": False,
+            },
+        ],
+        "expected": [
+            {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"},
+            {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "::"},
+        ],
+    },
+    {
+        "name": "adding_duplicate_port_for_v6_wildcard_and_v6_ip_should_fail",
+        "inputs": [
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "host_ips": ["::"]},
+                ),
+                "expect_error": False,
+            },
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "host_ips": ["fd00:1234:5678:abcd::10"]},
+                ),
+                "expect_error": True,
+            },
+        ],
+    },
+    {
+        "name": "adding_duplicate_port_for_v6_ip_and_v6_wildcard_should_fail",
+        "inputs": [
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "host_ips": ["fd00:1234:5678:abcd::10"]},
+                ),
+                "expect_error": False,
+            },
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "host_ips": ["::"]},
+                ),
+                "expect_error": True,
+            },
+        ],
+    },
+    {
+        "name": "adding_duplicate_port_with_different_v4_ip_should_work",
+        "inputs": [
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "host_ips": ["192.168.1.10"]},
+                ),
+                "expect_error": False,
+            },
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "host_ips": ["192.168.1.11"]},
+                ),
+                "expect_error": False,
+            },
+        ],
+        "expected": [
+            {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"},
+            {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.11"},
+        ],
+    },
+    {
+        "name": "adding_port_with_invalid_protocol_should_fail",
+        "inputs": [
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "protocol": "invalid_protocol"},
+                ),
+                "expect_error": True,
+            },
+        ],
+    },
+    {
+        "name": "adding_port_with_invalid_mode_should_fail",
+        "inputs": [
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "mode": "invalid_mode"},
+                ),
+                "expect_error": True,
+            },
+        ],
+    },
+    {
+        "name": "adding_port_with_invalid_ip_should_fail",
+        "inputs": [
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "host_ips": ["invalid_ip"]},
+                ),
+                "expect_error": True,
+            },
+        ],
+    },
+    {
+        "name": "adding_port_with_invalid_port_number_should_fail",
+        "inputs": [
+            {"values": ({"bind_mode": "published", "port_number": -1}, {"container_port": 8080}), "expect_error": True},
+        ],
+    },
+    {
+        "name": "adding_port_with_invalid_container_port_should_fail",
+        "inputs": [
+            {"values": ({"bind_mode": "published", "port_number": 8081}, {"container_port": -1}), "expect_error": True},
+        ],
+    },
+    {
+        "name": "adding_duplicate_ports_with_different_host_ip_should_work",
+        "inputs": [
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "host_ips": ["192.168.1.10"]},
+                ),
+                "expect_error": False,
+            },
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "host_ips": ["192.168.1.10"], "protocol": "udp"},
+                ),
+                "expect_error": False,
+            },
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "host_ips": ["192.168.1.11"]},
+                ),
+                "expect_error": False,
+            },
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "host_ips": ["192.168.1.11"], "protocol": "udp"},
+                ),
+                "expect_error": False,
+            },
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "host_ips": ["fd00:1234:5678:abcd::10"]},
+                ),
+                "expect_error": False,
+            },
+            {
+                "values": (
+                    {"bind_mode": "published", "port_number": 8081},
+                    {"container_port": 8080, "host_ips": ["fd00:1234:5678:abcd::11"]},
+                ),
+                "expect_error": False,
+            },
+        ],
+        # fmt: off
+        "expected": [
+            {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"},
+            {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.11"},
+            {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "fd00:1234:5678:abcd::10"}, # noqa
+            {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "fd00:1234:5678:abcd::11"}, # noqa
+            {"published": 8081, "target": 8080, "protocol": "udp", "mode": "ingress", "host_ip": "192.168.1.10"},
+            {"published": 8081, "target": 8080, "protocol": "udp", "mode": "ingress", "host_ip": "192.168.1.11"},
+        ],
+        # fmt: on
+    },
+]
+
+
+@pytest.mark.parametrize("test", tests)
+def test_ports(test):
+    mock_values = {
+        "images": {
+            "test_image": {
+                "repository": "nginx",
+                "tag": "latest",
+            }
+        },
+    }
+
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+
+    errored = False
+    for input in test["inputs"]:
+        if input["expect_error"]:
+            with pytest.raises(Exception):
+                c1.add_port(*input["values"])
+                errored = True
+        else:
+            c1.add_port(*input["values"])
+
+    errored = True if [i["expect_error"] for i in test["inputs"]].count(True) > 0 else False
+    if errored:
+        return
+
+    output = render.render()
+    assert output["services"]["test_container"]["ports"] == test["expected"]

+ 37 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_render.py

@@ -0,0 +1,37 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+    return {
+        "images": {
+            "test_image": {
+                "repository": "nginx",
+                "tag": "latest",
+            }
+        },
+    }
+
+
+def test_values_cannot_be_modified(mock_values):
+    render = Render(mock_values)
+    render.values["test"] = "test"
+    with pytest.raises(Exception):
+        render.render()
+
+
+def test_duplicate_containers(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        render.add_container("test_container", "test_image")
+
+
+def test_no_containers(mock_values):
+    render = Render(mock_values)
+    with pytest.raises(Exception):
+        render.render()

+ 140 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_resources.py

@@ -0,0 +1,140 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+    return {
+        "images": {
+            "test_image": {
+                "repository": "nginx",
+                "tag": "latest",
+            }
+        },
+    }
+
+
+def test_automatically_add_cpu(mock_values):
+    mock_values["resources"] = {"limits": {"cpus": 1.0}}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    output = render.render()
+    assert output["services"]["test_container"]["deploy"]["resources"]["limits"]["cpus"] == "1.0"
+
+
+def test_invalid_cpu(mock_values):
+    mock_values["resources"] = {"limits": {"cpus": "invalid"}}
+    render = Render(mock_values)
+    with pytest.raises(Exception):
+        render.add_container("test_container", "test_image")
+
+
+def test_automatically_add_memory(mock_values):
+    mock_values["resources"] = {"limits": {"memory": 1024}}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    output = render.render()
+    assert output["services"]["test_container"]["deploy"]["resources"]["limits"]["memory"] == "1024M"
+
+
+def test_invalid_memory(mock_values):
+    mock_values["resources"] = {"limits": {"memory": "invalid"}}
+    render = Render(mock_values)
+    with pytest.raises(Exception):
+        render.add_container("test_container", "test_image")
+
+
+def test_automatically_add_gpus(mock_values):
+    mock_values["resources"] = {
+        "gpus": {
+            "nvidia_gpu_selection": {
+                "pci_slot_0": {"uuid": "uuid_0", "use_gpu": True},
+                "pci_slot_1": {"uuid": "uuid_1", "use_gpu": True},
+            },
+        }
+    }
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    output = render.render()
+    devices = output["services"]["test_container"]["deploy"]["resources"]["reservations"]["devices"]
+    assert len(devices) == 1
+    assert devices[0] == {
+        "capabilities": ["gpu"],
+        "driver": "nvidia",
+        "device_ids": ["uuid_0", "uuid_1"],
+    }
+    assert output["services"]["test_container"]["group_add"] == [44, 107, 568]
+
+
+def test_gpu_without_uuid(mock_values):
+    mock_values["resources"] = {
+        "gpus": {
+            "nvidia_gpu_selection": {
+                "pci_slot_0": {"uuid": "", "use_gpu": True},
+                "pci_slot_1": {"uuid": "uuid_1", "use_gpu": True},
+            },
+        }
+    }
+    render = Render(mock_values)
+    with pytest.raises(Exception):
+        render.add_container("test_container", "test_image")
+
+
+def test_remove_cpus_and_memory_with_gpus(mock_values):
+    mock_values["resources"] = {"gpus": {"nvidia_gpu_selection": {"pci_slot_0": {"uuid": "uuid_1", "use_gpu": True}}}}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.deploy.resources.remove_cpus_and_memory()
+    output = render.render()
+    assert "limits" not in output["services"]["test_container"]["deploy"]["resources"]
+    devices = output["services"]["test_container"]["deploy"]["resources"]["reservations"]["devices"]
+    assert len(devices) == 1
+    assert devices[0] == {
+        "capabilities": ["gpu"],
+        "driver": "nvidia",
+        "device_ids": ["uuid_1"],
+    }
+
+
+def test_remove_cpus_and_memory(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.deploy.resources.remove_cpus_and_memory()
+    output = render.render()
+    assert "deploy" not in output["services"]["test_container"]
+
+
+def test_remove_devices(mock_values):
+    mock_values["resources"] = {"gpus": {"nvidia_gpu_selection": {"pci_slot_0": {"uuid": "uuid_0", "use_gpu": True}}}}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.deploy.resources.remove_devices()
+    output = render.render()
+    assert "reservations" not in output["services"]["test_container"]["deploy"]["resources"]
+    assert output["services"]["test_container"]["group_add"] == [568]
+
+
+def test_set_profile(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.deploy.resources.set_profile("low")
+    output = render.render()
+    assert output["services"]["test_container"]["deploy"]["resources"]["limits"]["cpus"] == "1"
+    assert output["services"]["test_container"]["deploy"]["resources"]["limits"]["memory"] == "512M"
+
+
+def test_set_profile_invalid_profile(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.deploy.resources.set_profile("invalid_profile")

+ 57 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_restart.py

@@ -0,0 +1,57 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+    return {
+        "images": {
+            "test_image": {
+                "repository": "nginx",
+                "tag": "latest",
+            }
+        },
+    }
+
+
+def test_invalid_restart_policy(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.restart.set_policy("invalid_policy")
+
+
+def test_valid_restart_policy(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.restart.set_policy("on-failure")
+    output = render.render()
+    assert output["services"]["test_container"]["restart"] == "on-failure"
+
+
+def test_valid_restart_policy_with_maximum_retry_count(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.restart.set_policy("on-failure", 10)
+    output = render.render()
+    assert output["services"]["test_container"]["restart"] == "on-failure:10"
+
+
+def test_invalid_restart_policy_with_maximum_retry_count(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.restart.set_policy("on-failure", maximum_retry_count=-1)
+
+
+def test_invalid_restart_policy_with_maximum_retry_count_and_policy(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.restart.set_policy("always", maximum_retry_count=10)

+ 91 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_security_opts.py

@@ -0,0 +1,91 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+    return {
+        "images": {
+            "test_image": {
+                "repository": "nginx",
+                "tag": "latest",
+            }
+        },
+    }
+
+
+def test_add_security_opt(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.add_security_opt("apparmor", "unconfined")
+    output = render.render()
+    assert output["services"]["test_container"]["security_opt"] == ["apparmor=unconfined", "no-new-privileges=true"]
+
+
+def test_add_duplicate_security_opt(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.add_security_opt("no-new-privileges", True)
+
+
+def test_add_empty_security_opt(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.add_security_opt("", True)
+
+
+def test_remove_security_opt(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.remove_security_opt("no-new-privileges")
+    output = render.render()
+    assert "security_opt" not in output["services"]["test_container"]
+
+
+def test_add_security_opt_boolean(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.remove_security_opt("no-new-privileges")
+    c1.add_security_opt("no-new-privileges", False)
+    output = render.render()
+    assert output["services"]["test_container"]["security_opt"] == ["no-new-privileges=false"]
+
+
+def test_add_security_opt_arg(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.add_security_opt("label", "type", "svirt_apache_t")
+    output = render.render()
+    assert output["services"]["test_container"]["security_opt"] == [
+        "label=type:svirt_apache_t",
+        "no-new-privileges=true",
+    ]
+
+
+def test_add_security_opt_with_invalid_opt(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.add_security_opt("invalid")
+
+
+def test_add_security_opt_with_opt_containing_value(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.remove_security_opt("no-new-privileges")
+    with pytest.raises(Exception):
+        c1.add_security_opt("no-new-privileges=true")
+    with pytest.raises(Exception):
+        c1.add_security_opt("apparmor:unconfined")

+ 62 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_sysctls.py

@@ -0,0 +1,62 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+    return {
+        "images": {
+            "test_image": {
+                "repository": "nginx",
+                "tag": "latest",
+            }
+        },
+    }
+
+
+def test_add_sysctl(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.sysctls.add("net.ipv4.ip_forward", 1)
+    c1.sysctls.add("fs.mqueue.msg_max", 100)
+    output = render.render()
+    assert output["services"]["test_container"]["sysctls"] == {"net.ipv4.ip_forward": "1", "fs.mqueue.msg_max": "100"}
+
+
+def test_add_net_sysctl_with_host_network(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.set_network_mode("host")
+    c1.sysctls.add("net.ipv4.ip_forward", 1)
+    with pytest.raises(Exception):
+        render.render()
+
+
+def test_add_duplicate_sysctl(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.sysctls.add("net.ipv4.ip_forward", 1)
+    with pytest.raises(Exception):
+        c1.sysctls.add("net.ipv4.ip_forward", 0)
+
+
+def test_add_empty_sysctl(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.sysctls.add("", 1)
+
+
+def test_add_sysctl_with_invalid_key(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.sysctls.add("invalid.sysctl", 1)
+    with pytest.raises(Exception):
+        render.render()

+ 132 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_validations.py

@@ -0,0 +1,132 @@
+import pytest
+from unittest.mock import patch
+
+from pathlib import Path
+from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN
+
+
+def mock_resolve(self):
+    # Don't modify paths that are from RESTRICTED list initialization
+    if str(self) in [str(p) for p in RESTRICTED]:
+        return self
+
+    # For symlinks that point to restricted paths, return the target path
+    # without stripping /private/
+    if str(self).endswith("symlink_restricted"):
+        return Path("/home")  # Return the actual restricted target
+
+    # For other paths, strip /private/ if present
+    return Path(str(self).removeprefix("/private/"))
+
+
+@pytest.mark.parametrize(
+    "test_path, expected",
+    [
+        # Non-restricted path (should be valid)
+        ("/tmp/somefile", True),
+        # Exactly /mnt (restricted_in)
+        ("/mnt", False),
+        # Exactly / (restricted_in)
+        ("/", False),
+        # Subdirectory inside /mnt/.ix-apps (restricted)
+        ("/mnt/.ix-apps/something", False),
+        # A path that is a restricted directory exactly
+        ("/home", False),
+        ("/var/log", False),
+        ("/mnt/.ix-apps", False),
+        ("/data", False),
+        # Subdirectory inside e.g. /data
+        ("/data/subdir", False),
+        # Not an obviously restricted path
+        ("/usr/local/share", True),
+        # Another system path likely not in restricted list
+        ("/opt/myapp", True),
+    ],
+)
+@patch.object(Path, "resolve", mock_resolve)
+def test_is_allowed_path_direct(test_path, expected):
+    """Test direct paths against the is_allowed_path function."""
+    assert is_allowed_path(test_path) == expected
+
+
+@patch.object(Path, "resolve", mock_resolve)
+def test_is_allowed_path_ix_volume():
+    """Test that IX volumes are not allowed"""
+    assert is_allowed_path("/mnt/.ix-apps/something", True)
+
+
+@patch.object(Path, "resolve", mock_resolve)
+def test_is_allowed_path_symlink(tmp_path):
+    """
+    Test that a symlink pointing to a restricted directory is detected as invalid,
+    and a symlink pointing to an allowed directory is valid.
+    """
+    # Create a real (allowed) directory and a restricted directory in a temp location
+    allowed_dir = tmp_path / "allowed_dir"
+    allowed_dir.mkdir()
+
+    restricted_dir = tmp_path / "restricted_dir"
+    restricted_dir.mkdir()
+
+    # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log"
+    # or we create a subdir to match the restricted pattern.
+    # For demonstration, let's just patch it to a path in the restricted list.
+    real_restricted_path = Path("/home")  # This is one of the restricted directories
+
+    # Create symlinks to test
+    symlink_allowed = tmp_path / "symlink_allowed"
+    symlink_restricted = tmp_path / "symlink_restricted"
+
+    # Point the symlinks
+    symlink_allowed.symlink_to(allowed_dir)
+    symlink_restricted.symlink_to(real_restricted_path)
+
+    assert is_allowed_path(str(symlink_allowed)) is True
+    assert is_allowed_path(str(symlink_restricted)) is False
+
+
+def test_is_allowed_path_nested_symlink(tmp_path):
+    """
+    Test that even a nested symlink that eventually resolves into restricted
+    directories is seen as invalid.
+    """
+    # e.g., Create 2 symlinks that chain to /root
+    link1 = tmp_path / "link1"
+    link2 = tmp_path / "link2"
+
+    # link2 -> /root
+    link2.symlink_to(Path("/root"))
+    # link1 -> link2
+    link1.symlink_to(link2)
+
+    assert is_allowed_path(str(link1)) is False
+
+
+def test_is_allowed_path_nonexistent(tmp_path):
+    """
+    Test a path that does not exist at all. The code calls .resolve() which will
+    give the absolute path, but if it's not restricted, it should still be valid.
+    """
+    nonexistent = tmp_path / "this_does_not_exist"
+    assert is_allowed_path(str(nonexistent)) is True
+
+
+@pytest.mark.parametrize(
+    "test_path",
+    list(RESTRICTED),
+)
+@patch.object(Path, "resolve", mock_resolve)
+def test_is_allowed_path_restricted_list(test_path):
+    """Test that all items in the RESTRICTED list are invalid."""
+    assert is_allowed_path(test_path) is False
+
+
+@pytest.mark.parametrize(
+    "test_path",
+    list(RESTRICTED_IN),
+)
+def test_is_allowed_path_restricted_in_list(test_path):
+    """
+    Test that items in RESTRICTED_IN are invalid.
+    """
+    assert is_allowed_path(test_path) is False

+ 746 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tests/test_volumes.py

@@ -0,0 +1,746 @@
+import pytest
+
+
+from render import Render
+from formatter import get_hashed_name_for_volume
+
+
+@pytest.fixture
+def mock_values():
+    return {
+        "images": {
+            "test_image": {
+                "repository": "nginx",
+                "tag": "latest",
+            }
+        },
+    }
+
+
+def test_add_volume_invalid_type(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", {"type": "invalid_type"})
+
+
+def test_add_volume_empty_mount_path(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    with pytest.raises(Exception):
+        c1.add_storage("", {"type": "tmpfs"})
+
+
+def test_add_volume_duplicate_mount_path(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.add_storage("/some/path", {"type": "tmpfs"})
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", {"type": "tmpfs"})
+
+
+def test_add_volume_host_path_invalid_propagation(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    host_path_config = {
+        "type": "host_path",
+        "host_path_config": {"path": "/mnt/test", "propagation": "invalid_propagation"},
+    }
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", host_path_config)
+
+
+def test_add_host_path_volume_no_host_path_config(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    host_path_config = {"type": "host_path"}
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", host_path_config)
+
+
+def test_add_host_path_volume_no_path(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    host_path_config = {"type": "host_path", "host_path_config": {"path": ""}}
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", host_path_config)
+
+
+def test_add_host_path_with_acl_no_path(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    host_path_config = {"type": "host_path", "host_path_config": {"acl_enable": True, "acl": {"path": ""}}}
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", host_path_config)
+
+
+def test_add_host_path_volume_mount(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test"}}
+    c1.add_storage("/some/path", host_path_config)
+    output = render.render()
+    assert output["services"]["test_container"]["volumes"] == [
+        {
+            "type": "bind",
+            "source": "/mnt/test",
+            "target": "/some/path",
+            "read_only": False,
+            "bind": {"create_host_path": False, "propagation": "rprivate"},
+        }
+    ]
+
+
+def test_add_host_path_volume_mount_with_acl(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    host_path_config = {
+        "type": "host_path",
+        "host_path_config": {"path": "/mnt/test", "acl_enable": True, "acl": {"path": "/mnt/test/acl"}},
+    }
+    c1.add_storage("/some/path", host_path_config)
+    output = render.render()
+    assert output["services"]["test_container"]["volumes"] == [
+        {
+            "type": "bind",
+            "source": "/mnt/test/acl",
+            "target": "/some/path",
+            "read_only": False,
+            "bind": {"create_host_path": False, "propagation": "rprivate"},
+        }
+    ]
+
+
+def test_add_host_path_volume_mount_with_propagation(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test", "propagation": "slave"}}
+    c1.add_storage("/some/path", host_path_config)
+    output = render.render()
+    assert output["services"]["test_container"]["volumes"] == [
+        {
+            "type": "bind",
+            "source": "/mnt/test",
+            "target": "/some/path",
+            "read_only": False,
+            "bind": {"create_host_path": False, "propagation": "slave"},
+        }
+    ]
+
+
+def test_add_host_path_volume_mount_with_create_host_path(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test", "create_host_path": True}}
+    c1.add_storage("/some/path", host_path_config)
+    output = render.render()
+    assert output["services"]["test_container"]["volumes"] == [
+        {
+            "type": "bind",
+            "source": "/mnt/test",
+            "target": "/some/path",
+            "read_only": False,
+            "bind": {"create_host_path": True, "propagation": "rprivate"},
+        }
+    ]
+
+
+def test_add_host_path_volume_mount_with_read_only(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    host_path_config = {"type": "host_path", "read_only": True, "host_path_config": {"path": "/mnt/test"}}
+    c1.add_storage("/some/path", host_path_config)
+    output = render.render()
+    assert output["services"]["test_container"]["volumes"] == [
+        {
+            "type": "bind",
+            "source": "/mnt/test",
+            "target": "/some/path",
+            "read_only": True,
+            "bind": {"create_host_path": False, "propagation": "rprivate"},
+        }
+    ]
+
+
+def test_add_ix_volume_invalid_dataset_name(mock_values):
+    mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    ix_volume_config = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "invalid_dataset"}}
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", ix_volume_config)
+
+
+def test_add_ix_volume_no_ix_volume_config(mock_values):
+    mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    ix_volume_config = {"type": "ix_volume"}
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", ix_volume_config)
+
+
+def test_add_ix_volume_volume_mount(mock_values):
+    mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    ix_volume_config = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "test_dataset"}}
+    c1.add_storage("/some/path", ix_volume_config)
+    output = render.render()
+    assert output["services"]["test_container"]["volumes"] == [
+        {
+            "type": "bind",
+            "source": "/mnt/test",
+            "target": "/some/path",
+            "read_only": False,
+            "bind": {"create_host_path": False, "propagation": "rprivate"},
+        }
+    ]
+
+
+def test_add_ix_volume_volume_mount_with_options(mock_values):
+    mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    ix_volume_config = {
+        "type": "ix_volume",
+        "ix_volume_config": {"dataset_name": "test_dataset", "propagation": "rslave", "create_host_path": True},
+    }
+    c1.add_storage("/some/path", ix_volume_config)
+    output = render.render()
+    assert output["services"]["test_container"]["volumes"] == [
+        {
+            "type": "bind",
+            "source": "/mnt/test",
+            "target": "/some/path",
+            "read_only": False,
+            "bind": {"create_host_path": True, "propagation": "rslave"},
+        }
+    ]
+
+
+def test_cifs_volume_missing_server(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    cifs_config = {"type": "cifs", "cifs_config": {"path": "/path", "username": "user", "password": "password"}}
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_missing_path(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    cifs_config = {"type": "cifs", "cifs_config": {"server": "server", "username": "user", "password": "password"}}
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_missing_username(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    cifs_config = {"type": "cifs", "cifs_config": {"server": "server", "path": "/path", "password": "password"}}
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_missing_password(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    cifs_config = {"type": "cifs", "cifs_config": {"server": "server", "path": "/path", "username": "user"}}
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_without_cifs_config(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    cifs_config = {"type": "cifs"}
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_duplicate_option(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    cifs_config = {
+        "type": "cifs",
+        "cifs_config": {
+            "server": "server",
+            "path": "/path",
+            "username": "user",
+            "password": "pas$word",
+            "options": ["verbose=true", "verbose=true"],
+        },
+    }
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_disallowed_option(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    cifs_config = {
+        "type": "cifs",
+        "cifs_config": {
+            "server": "server",
+            "path": "/path",
+            "username": "user",
+            "password": "pas$word",
+            "options": ["user=username"],
+        },
+    }
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_invalid_options(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    cifs_config = {
+        "type": "cifs",
+        "cifs_config": {
+            "server": "server",
+            "path": "/path",
+            "username": "user",
+            "password": "pas$word",
+            "options": {"verbose": True},
+        },
+    }
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_invalid_options2(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    cifs_config = {
+        "type": "cifs",
+        "cifs_config": {
+            "server": "server",
+            "path": "/path",
+            "username": "user",
+            "password": "pas$word",
+            "options": [{"verbose": True}],
+        },
+    }
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", cifs_config)
+
+
+def test_add_cifs_volume(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    cifs_inner_config = {"server": "server", "path": "/path", "username": "user", "password": "pas$word"}
+    cifs_config = {"type": "cifs", "cifs_config": cifs_inner_config}
+    c1.add_storage("/some/path", cifs_config)
+    output = render.render()
+    vol_name = get_hashed_name_for_volume("cifs", cifs_inner_config)
+    assert output["volumes"] == {
+        vol_name: {
+            "driver_opts": {"type": "cifs", "device": "//server/path", "o": "noperm,password=pas$$word,user=user"}
+        }
+    }
+    assert output["services"]["test_container"]["volumes"] == [
+        {"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
+    ]
+
+
+def test_cifs_volume_with_options(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    cifs_inner_config = {
+        "server": "server",
+        "path": "/path",
+        "username": "user",
+        "password": "pas$word",
+        "options": ["vers=3.0", "verbose=true"],
+    }
+    cifs_config = {"type": "cifs", "cifs_config": cifs_inner_config}
+    c1.add_storage("/some/path", cifs_config)
+    output = render.render()
+    vol_name = get_hashed_name_for_volume("cifs", cifs_inner_config)
+    assert output["volumes"] == {
+        vol_name: {
+            "driver_opts": {
+                "type": "cifs",
+                "device": "//server/path",
+                "o": "noperm,password=pas$$word,user=user,verbose=true,vers=3.0",
+            }
+        }
+    }
+    assert output["services"]["test_container"]["volumes"] == [
+        {"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
+    ]
+
+
+def test_nfs_volume_missing_server(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    nfs_config = {"type": "nfs", "nfs_config": {"path": "/path"}}
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_missing_path(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    nfs_config = {"type": "nfs", "nfs_config": {"server": "server"}}
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_without_nfs_config(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    nfs_config = {"type": "nfs"}
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_duplicate_option(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    nfs_config = {
+        "type": "nfs",
+        "nfs_config": {"server": "server", "path": "/path", "options": ["verbose=true", "verbose=true"]},
+    }
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_disallowed_option(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    nfs_config = {"type": "nfs", "nfs_config": {"server": "server", "path": "/path", "options": ["addr=server"]}}
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_invalid_options(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    nfs_config = {"type": "nfs", "nfs_config": {"server": "server", "path": "/path", "options": {"verbose": True}}}
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_invalid_options2(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    nfs_config = {"type": "nfs", "nfs_config": {"server": "server", "path": "/path", "options": [{"verbose": True}]}}
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", nfs_config)
+
+
+def test_add_nfs_volume(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    nfs_inner_config = {"server": "server", "path": "/path"}
+    nfs_config = {"type": "nfs", "nfs_config": nfs_inner_config}
+    c1.add_storage("/some/path", nfs_config)
+    output = render.render()
+    vol_name = get_hashed_name_for_volume("nfs", nfs_inner_config)
+    assert output["volumes"] == {vol_name: {"driver_opts": {"type": "nfs", "device": ":/path", "o": "addr=server"}}}
+    assert output["services"]["test_container"]["volumes"] == [
+        {"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
+    ]
+
+
+def test_nfs_volume_with_options(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    nfs_inner_config = {"server": "server", "path": "/path", "options": ["vers=3.0", "verbose=true"]}
+    nfs_config = {"type": "nfs", "nfs_config": nfs_inner_config}
+    c1.add_storage("/some/path", nfs_config)
+    output = render.render()
+    vol_name = get_hashed_name_for_volume("nfs", nfs_inner_config)
+    assert output["volumes"] == {
+        vol_name: {
+            "driver_opts": {
+                "type": "nfs",
+                "device": ":/path",
+                "o": "addr=server,verbose=true,vers=3.0",
+            }
+        }
+    }
+    assert output["services"]["test_container"]["volumes"] == [
+        {"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
+    ]
+
+
+def test_tmpfs_invalid_size(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    vol_config = {"type": "tmpfs", "tmpfs_config": {"size": "2"}}
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", vol_config)
+
+
+def test_tmpfs_zero_size(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    vol_config = {"type": "tmpfs", "tmpfs_config": {"size": 0}}
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", vol_config)
+
+
+def test_tmpfs_invalid_mode(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    vol_config = {"type": "tmpfs", "tmpfs_config": {"mode": "invalid"}}
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", vol_config)
+
+
+def test_tmpfs_volume(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.add_storage("/some/path", {"type": "tmpfs"})
+    c1.add_storage("/some/other/path", {"type": "tmpfs", "tmpfs_config": {"size": 100}})
+    c1.add_storage(
+        "/some/other/path2", {"type": "tmpfs", "tmpfs_config": {"size": 100, "mode": "0777", "uid": 1000, "gid": 1000}}
+    )
+    output = render.render()
+    assert output["services"]["test_container"]["tmpfs"] == [
+        "/some/other/path2:gid=1000,mode=0777,size=104857600,uid=1000",
+        "/some/other/path:size=104857600",
+        "/some/path",
+    ]
+
+
+def test_add_tmpfs_with_existing_volume(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.add_storage("/some/path", {"type": "volume", "volume_config": {"volume_name": "test_volume"}})
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", {"type": "tmpfs", "tmpfs_config": {"size": 100}})
+
+
+def test_add_volume_with_existing_tmpfs(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.add_storage("/some/path", {"type": "tmpfs", "tmpfs_config": {"size": 100}})
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", {"type": "volume", "volume_config": {"volume_name": "test_volume"}})
+
+
+def test_temporary_volume(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    vol_config = {"type": "temporary", "volume_config": {"volume_name": "test_temp_volume"}}
+    c1.add_storage("/some/path", vol_config)
+    output = render.render()
+    assert output["services"]["test_container"]["volumes"] == [
+        {
+            "source": "test_temp_volume",
+            "type": "volume",
+            "target": "/some/path",
+            "read_only": False,
+            "volume": {"nocopy": False},
+        }
+    ]
+
+
+def test_docker_volume_missing_config(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    vol_config = {"type": "volume", "volume_config": {}}
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", vol_config)
+
+
+def test_docker_volume_missing_volume_name(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    vol_config = {"type": "volume", "volume_config": {"volume_name": ""}}
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", vol_config)
+
+
+def test_docker_volume(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    vol_config = {"type": "volume", "volume_config": {"volume_name": "test_volume"}}
+    c1.add_storage("/some/path", vol_config)
+    output = render.render()
+    assert output["services"]["test_container"]["volumes"] == [
+        {
+            "type": "volume",
+            "source": "test_volume",
+            "target": "/some/path",
+            "read_only": False,
+            "volume": {"nocopy": False},
+        }
+    ]
+    assert output["volumes"] == {"test_volume": {}}
+
+
+def test_anonymous_volume(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    vol_config = {"type": "anonymous", "volume_config": {"nocopy": True}}
+    c1.add_storage("/some/path", vol_config)
+    output = render.render()
+    assert output["services"]["test_container"]["volumes"] == [
+        {"type": "volume", "target": "/some/path", "read_only": False, "volume": {"nocopy": True}}
+    ]
+    assert "volumes" not in output
+
+
+def test_add_udev(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.add_udev()
+    output = render.render()
+    assert output["services"]["test_container"]["volumes"] == [
+        {
+            "type": "bind",
+            "source": "/run/udev",
+            "target": "/run/udev",
+            "read_only": True,
+            "bind": {"create_host_path": False, "propagation": "rprivate"},
+        }
+    ]
+
+
+def test_add_udev_not_read_only(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.add_udev(read_only=False)
+    output = render.render()
+    assert output["services"]["test_container"]["volumes"] == [
+        {
+            "type": "bind",
+            "source": "/run/udev",
+            "target": "/run/udev",
+            "read_only": False,
+            "bind": {"create_host_path": False, "propagation": "rprivate"},
+        }
+    ]
+
+
+def test_add_docker_socket(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.storage._add_docker_socket(mount_path="/var/run/docker.sock")
+    output = render.render()
+    assert output["services"]["test_container"]["volumes"] == [
+        {
+            "type": "bind",
+            "source": "/var/run/docker.sock",
+            "target": "/var/run/docker.sock",
+            "read_only": True,
+            "bind": {"create_host_path": False, "propagation": "rprivate"},
+        }
+    ]
+
+
+def test_add_docker_socket_not_read_only(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.storage._add_docker_socket(read_only=False, mount_path="/var/run/docker.sock")
+    output = render.render()
+    assert output["services"]["test_container"]["volumes"] == [
+        {
+            "type": "bind",
+            "source": "/var/run/docker.sock",
+            "target": "/var/run/docker.sock",
+            "read_only": False,
+            "bind": {"create_host_path": False, "propagation": "rprivate"},
+        }
+    ]
+
+
+def test_add_docker_socket_mount_path(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    c1.storage._add_docker_socket(mount_path="/some/path")
+    output = render.render()
+    assert output["services"]["test_container"]["volumes"] == [
+        {
+            "type": "bind",
+            "source": "/var/run/docker.sock",
+            "target": "/some/path",
+            "read_only": True,
+            "bind": {"create_host_path": False, "propagation": "rprivate"},
+        }
+    ]
+
+
+def test_host_path_with_disallowed_path(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt"}}
+    with pytest.raises(Exception):
+        c1.add_storage("/some/path", host_path_config)
+
+
+def test_host_path_without_disallowed_path(mock_values):
+    render = Render(mock_values)
+    c1 = render.add_container("test_container", "test_image")
+    c1.healthcheck.disable()
+    host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test"}}
+    c1.add_storage("/mnt", host_path_config)
+    output = render.render()
+    assert output["services"]["test_container"]["volumes"] == [
+        {
+            "type": "bind",
+            "source": "/mnt/test",
+            "target": "/mnt",
+            "read_only": False,
+            "bind": {"create_host_path": False, "propagation": "rprivate"},
+        }
+    ]

+ 75 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/tmpfs.py

@@ -0,0 +1,75 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from container import Container
+    from render import Render
+    from storage import IxStorage
+
+try:
+    from .error import RenderError
+    from .validations import valid_fs_path_or_raise, valid_octal_mode_or_raise
+except ImportError:
+    from error import RenderError
+    from validations import valid_fs_path_or_raise, valid_octal_mode_or_raise
+
+
+class Tmpfs:
+
+    def __init__(self, render_instance: "Render", container_instance: "Container"):
+        self._render_instance = render_instance
+        self._container_instance = container_instance
+        self._tmpfs: dict = {}
+
+    def add(self, mount_path: str, config: "IxStorage"):
+        mount_path = valid_fs_path_or_raise(mount_path)
+        if self.is_defined(mount_path):
+            raise RenderError(f"Tmpfs mount path [{mount_path}] already added")
+
+        if self._container_instance.storage.is_defined(mount_path):
+            raise RenderError(f"Tmpfs mount path [{mount_path}] already used for another volume mount")
+
+        mount_config = config.get("tmpfs_config", {})
+        size = mount_config.get("size", None)
+        mode = mount_config.get("mode", None)
+        uid = mount_config.get("uid", None)
+        gid = mount_config.get("gid", None)
+
+        if size is not None:
+            if not isinstance(size, int):
+                raise RenderError(f"Expected [size] to be an integer for [tmpfs] type, got [{size}]")
+            if not size > 0:
+                raise RenderError(f"Expected [size] to be greater than 0 for [tmpfs] type, got [{size}]")
+            # Convert Mebibytes to Bytes
+            size = size * 1024 * 1024
+
+        if mode is not None:
+            mode = valid_octal_mode_or_raise(mode)
+
+        if uid is not None and not isinstance(uid, int):
+            raise RenderError(f"Expected [uid] to be an integer for [tmpfs] type, got [{uid}]")
+
+        if gid is not None and not isinstance(gid, int):
+            raise RenderError(f"Expected [gid] to be an integer for [tmpfs] type, got [{gid}]")
+
+        self._tmpfs[mount_path] = {}
+        if size is not None:
+            self._tmpfs[mount_path]["size"] = str(size)
+        if mode is not None:
+            self._tmpfs[mount_path]["mode"] = str(mode)
+        if uid is not None:
+            self._tmpfs[mount_path]["uid"] = str(uid)
+        if gid is not None:
+            self._tmpfs[mount_path]["gid"] = str(gid)
+
+    def is_defined(self, mount_path: str):
+        return mount_path in self._tmpfs
+
+    def has_tmpfs(self):
+        return bool(self._tmpfs)
+
+    def render(self):
+        result = []
+        for mount_path, config in self._tmpfs.items():
+            opts = sorted([f"{k}={v}" for k, v in config.items()])
+            result.append(f"{mount_path}:{','.join(opts)}" if opts else mount_path)
+        return sorted(result)

+ 344 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/validations.py

@@ -0,0 +1,344 @@
+import re
+import ipaddress
+from pathlib import Path
+
+
+try:
+    from .error import RenderError
+except ImportError:
+    from error import RenderError
+
+OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$")
+RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/"))
+RESTRICTED: tuple[Path, ...] = (
+    Path("/mnt/.ix-apps"),
+    Path("/data"),
+    Path("/var/db"),
+    Path("/root"),
+    Path("/conf"),
+    Path("/audit"),
+    Path("/var/run/middleware"),
+    Path("/home"),
+    Path("/boot"),
+    Path("/var/log"),
+)
+
+
+def valid_security_opt_or_raise(opt: str):
+    if ":" in opt or "=" in opt:
+        raise RenderError(f"Security Option [{opt}] cannot contain [:] or [=]. Pass value as an argument")
+    valid_opts = ["apparmor", "no-new-privileges", "seccomp", "systempaths", "label"]
+    if opt not in valid_opts:
+        raise RenderError(f"Security Option [{opt}] is not valid. Valid options are: [{', '.join(valid_opts)}]")
+
+    return opt
+
+
+def valid_port_bind_mode_or_raise(status: str):
+    valid_statuses = ("published", "exposed", "")
+    if status not in valid_statuses:
+        raise RenderError(f"Invalid port status [{status}]")
+    return status
+
+
+def valid_pull_policy_or_raise(pull_policy: str):
+    valid_policies = ("missing", "always", "never", "build")
+    if pull_policy not in valid_policies:
+        raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]")
+    return pull_policy
+
+
+def valid_ipc_mode_or_raise(ipc_mode: str, containers: list[str]):
+    valid_modes = ("", "host", "private", "shareable", "none")
+    if ipc_mode in valid_modes:
+        return ipc_mode
+    if ipc_mode.startswith("container:"):
+        if ipc_mode[10:] not in containers:
+            raise RenderError(f"IPC mode [{ipc_mode}] is not valid. Container [{ipc_mode[10:]}] does not exist")
+        return ipc_mode
+    raise RenderError(f"IPC mode [{ipc_mode}] is not valid. Valid options are: [{', '.join(valid_modes)}]")
+
+
+def valid_pid_mode_or_raise(ipc_mode: str, containers: list[str]):
+    valid_modes = ("", "host")
+    if ipc_mode in valid_modes:
+        return ipc_mode
+    if ipc_mode.startswith("container:"):
+        if ipc_mode[10:] not in containers:
+            raise RenderError(f"PID mode [{ipc_mode}] is not valid. Container [{ipc_mode[10:]}] does not exist")
+        return ipc_mode
+    raise RenderError(f"PID mode [{ipc_mode}] is not valid. Valid options are: [{', '.join(valid_modes)}]")
+
+
+def valid_sysctl_or_raise(sysctl: str, host_network: bool):
+    if not sysctl:
+        raise RenderError("Sysctl cannot be empty")
+    if host_network and sysctl.startswith("net."):
+        raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled")
+
+    valid_sysctls = [
+        "kernel.msgmax",
+        "kernel.msgmnb",
+        "kernel.msgmni",
+        "kernel.sem",
+        "kernel.shmall",
+        "kernel.shmmax",
+        "kernel.shmmni",
+        "kernel.shm_rmid_forced",
+    ]
+    # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls
+    if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls:
+        raise RenderError(
+            f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]"
+        )
+    return sysctl
+
+
+def valid_redis_password_or_raise(password: str):
+    forbidden_chars = [" ", "'", "#"]
+    for char in forbidden_chars:
+        if char in password:
+            raise RenderError(f"Redis password cannot contain [{char}]")
+
+
+def valid_octal_mode_or_raise(mode: str):
+    mode = str(mode)
+    if not OCTAL_MODE_REGEX.match(mode):
+        raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]")
+    return mode
+
+
+def valid_host_path_propagation(propagation: str):
+    valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate")
+    if propagation not in valid_propagations:
+        raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]")
+    return propagation
+
+
+def valid_portal_scheme_or_raise(scheme: str):
+    schemes = ("http", "https")
+    if scheme not in schemes:
+        raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]")
+    return scheme
+
+
+def valid_port_or_raise(port: int):
+    if port < 1 or port > 65535:
+        raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535")
+    return port
+
+
+def valid_ip_or_raise(ip: str):
+    try:
+        ipaddress.ip_address(ip)
+    except ValueError:
+        raise RenderError(f"Invalid IP address [{ip}]")
+    return ip
+
+
+def valid_port_mode_or_raise(mode: str):
+    modes = ("ingress", "host")
+    if mode not in modes:
+        raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]")
+    return mode
+
+
+def valid_port_protocol_or_raise(protocol: str):
+    protocols = ("tcp", "udp")
+    if protocol not in protocols:
+        raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]")
+    return protocol
+
+
+def valid_depend_condition_or_raise(condition: str):
+    valid_conditions = ("service_started", "service_healthy", "service_completed_successfully")
+    if condition not in valid_conditions:
+        raise RenderError(
+            f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]"
+        )
+    return condition
+
+
+def valid_cgroup_perm_or_raise(cgroup_perm: str):
+    valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "")
+    if cgroup_perm not in valid_cgroup_perms:
+        raise RenderError(
+            f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]"
+        )
+    return cgroup_perm
+
+
+def valid_cgroup_or_raise(cgroup: str):
+    valid_cgroup = ("host", "private")
+    if cgroup not in valid_cgroup:
+        raise RenderError(f"Cgroup [{cgroup}] is not valid. Valid options are: [{', '.join(valid_cgroup)}]")
+    return cgroup
+
+
+def valid_device_cgroup_rule_or_raise(dev_grp_rule: str):
+    parts = dev_grp_rule.split(" ")
+    if len(parts) != 3:
+        raise RenderError(
+            f"Device Group Rule [{dev_grp_rule}] is not valid. Expected format is [<type> <major>:<minor> <permission>]"
+        )
+
+    valid_types = ("a", "b", "c")
+    if parts[0] not in valid_types:
+        raise RenderError(
+            f"Device Group Rule [{dev_grp_rule}] is not valid. Expected type to be one of [{', '.join(valid_types)}]"
+            f" but got [{parts[0]}]"
+        )
+
+    major, minor = parts[1].split(":")
+    for part in (major, minor):
+        if part != "*" and not part.isdigit():
+            raise RenderError(
+                f"Device Group Rule [{dev_grp_rule}] is not valid. Expected major and minor to be digits"
+                f" or [*] but got [{major}] and [{minor}]"
+            )
+
+    valid_cgroup_perm_or_raise(parts[2])
+
+    return dev_grp_rule
+
+
+def allowed_dns_opt_or_raise(dns_opt: str):
+    disallowed_dns_opts = []
+    if dns_opt in disallowed_dns_opts:
+        raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.")
+    return dns_opt
+
+
+def valid_http_path_or_raise(path: str):
+    path = _valid_path_or_raise(path)
+    return path
+
+
+def valid_fs_path_or_raise(path: str):
+    # There is no reason to allow / as a path,
+    # either on host or in a container side.
+    if path == "/":
+        raise RenderError(f"Path [{path}] cannot be [/]")
+    path = _valid_path_or_raise(path)
+    return path
+
+
+def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool:
+    """
+    Validates that the given path (after resolving symlinks) is not
+    one of the restricted paths or within those restricted directories.
+
+    Returns True if the path is allowed, False otherwise.
+    """
+    # Resolve the path to avoid symlink bypasses
+    real_path = Path(input_path).resolve()
+    for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]:
+        if real_path.is_relative_to(restricted):
+            return False
+
+    return real_path not in RESTRICTED_IN
+
+
+def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False):
+    if not is_allowed_path(path, is_ix_volume):
+        raise RenderError(f"Path [{path}] is not allowed to be mounted.")
+    return path
+
+
+def _valid_path_or_raise(path: str):
+    if path == "":
+        raise RenderError(f"Path [{path}] cannot be empty")
+    if not path.startswith("/"):
+        raise RenderError(f"Path [{path}] must start with /")
+    if "//" in path:
+        raise RenderError(f"Path [{path}] cannot contain [//]")
+    return path
+
+
+def allowed_device_or_raise(path: str):
+    disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"]
+    if path in disallowed_devices:
+        raise RenderError(f"Device [{path}] is not allowed to be manually added.")
+    return path
+
+
+def valid_network_mode_or_raise(mode: str, containers: list[str]):
+    valid_modes = ("host", "none")
+    if mode in valid_modes:
+        return mode
+
+    if mode.startswith("service:"):
+        if mode[8:] not in containers:
+            raise RenderError(f"Service [{mode[8:]}] not found")
+        return mode
+
+    raise RenderError(
+        f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:<name>]"
+    )
+
+
+def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0):
+    valid_restart_policies = ("always", "on-failure", "unless-stopped", "no")
+    if policy not in valid_restart_policies:
+        raise RenderError(
+            f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]"
+        )
+    if policy != "on-failure" and maximum_retry_count != 0:
+        raise RenderError("Maximum retry count can only be set for [on-failure] restart policy")
+
+    if maximum_retry_count < 0:
+        raise RenderError("Maximum retry count must be a positive integer")
+
+    return policy
+
+
+def valid_cap_or_raise(cap: str):
+    valid_policies = (
+        "ALL",
+        "AUDIT_CONTROL",
+        "AUDIT_READ",
+        "AUDIT_WRITE",
+        "BLOCK_SUSPEND",
+        "BPF",
+        "CHECKPOINT_RESTORE",
+        "CHOWN",
+        "DAC_OVERRIDE",
+        "DAC_READ_SEARCH",
+        "FOWNER",
+        "FSETID",
+        "IPC_LOCK",
+        "IPC_OWNER",
+        "KILL",
+        "LEASE",
+        "LINUX_IMMUTABLE",
+        "MAC_ADMIN",
+        "MAC_OVERRIDE",
+        "MKNOD",
+        "NET_ADMIN",
+        "NET_BIND_SERVICE",
+        "NET_BROADCAST",
+        "NET_RAW",
+        "PERFMON",
+        "SETFCAP",
+        "SETGID",
+        "SETPCAP",
+        "SETUID",
+        "SYS_ADMIN",
+        "SYS_BOOT",
+        "SYS_CHROOT",
+        "SYS_MODULE",
+        "SYS_NICE",
+        "SYS_PACCT",
+        "SYS_PTRACE",
+        "SYS_RAWIO",
+        "SYS_RESOURCE",
+        "SYS_TIME",
+        "SYS_TTY_CONFIG",
+        "SYSLOG",
+        "WAKE_ALARM",
+    )
+
+    if cap not in valid_policies:
+        raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]")
+
+    return cap

+ 87 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/volume_mount.py

@@ -0,0 +1,87 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+    from storage import IxStorage
+
+try:
+    from .error import RenderError
+    from .formatter import merge_dicts_no_overwrite
+    from .volume_mount_types import BindMountType, VolumeMountType
+    from .volume_sources import HostPathSource, IxVolumeSource, CifsSource, NfsSource, VolumeSource
+except ImportError:
+    from error import RenderError
+    from formatter import merge_dicts_no_overwrite
+    from volume_mount_types import BindMountType, VolumeMountType
+    from volume_sources import HostPathSource, IxVolumeSource, CifsSource, NfsSource, VolumeSource
+
+
+class VolumeMount:
+    def __init__(self, render_instance: "Render", mount_path: str, config: "IxStorage"):
+        self._render_instance = render_instance
+        self.mount_path: str = mount_path
+
+        storage_type: str = config.get("type", "")
+        if not storage_type:
+            raise RenderError("Expected [type] to be set for volume mounts.")
+
+        match storage_type:
+            case "host_path":
+                spec_type = "bind"
+                mount_config = config.get("host_path_config")
+                if mount_config is None:
+                    raise RenderError("Expected [host_path_config] to be set for [host_path] type.")
+                mount_type_specific_definition = BindMountType(self._render_instance, mount_config).render()
+                source = HostPathSource(self._render_instance, mount_config).get()
+            case "ix_volume":
+                spec_type = "bind"
+                mount_config = config.get("ix_volume_config")
+                if mount_config is None:
+                    raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.")
+                mount_type_specific_definition = BindMountType(self._render_instance, mount_config).render()
+                source = IxVolumeSource(self._render_instance, mount_config).get()
+            case "nfs":
+                spec_type = "volume"
+                mount_config = config.get("nfs_config")
+                if mount_config is None:
+                    raise RenderError("Expected [nfs_config] to be set for [nfs] type.")
+                mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+                source = NfsSource(self._render_instance, mount_config).get()
+            case "cifs":
+                spec_type = "volume"
+                mount_config = config.get("cifs_config")
+                if mount_config is None:
+                    raise RenderError("Expected [cifs_config] to be set for [cifs] type.")
+                mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+                source = CifsSource(self._render_instance, mount_config).get()
+            case "volume":
+                spec_type = "volume"
+                mount_config = config.get("volume_config")
+                if mount_config is None:
+                    raise RenderError("Expected [volume_config] to be set for [volume] type.")
+                mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+                source = VolumeSource(self._render_instance, mount_config).get()
+            case "temporary":
+                spec_type = "volume"
+                mount_config = config.get("volume_config")
+                if mount_config is None:
+                    raise RenderError("Expected [volume_config] to be set for [temporary] type.")
+                mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+                source = VolumeSource(self._render_instance, mount_config).get()
+            case "anonymous":
+                spec_type = "volume"
+                mount_config = config.get("volume_config") or {}
+                mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+                source = None
+            case _:
+                raise RenderError(f"Storage type [{storage_type}] is not supported for volume mounts.")
+
+        common_spec = {"type": spec_type, "target": self.mount_path, "read_only": config.get("read_only", False)}
+        if source is not None:
+            common_spec["source"] = source
+            self._render_instance.volumes.add_volume(source, storage_type, mount_config)  # type: ignore
+
+        self.volume_mount_spec = merge_dicts_no_overwrite(common_spec, mount_type_specific_definition)
+
+    def render(self) -> dict:
+        return self.volume_mount_spec

+ 43 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/volume_mount_types.py

@@ -0,0 +1,43 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+    from storage import IxStorageVolumeConfig, IxStorageBindLikeConfigs
+
+
+try:
+    from .validations import valid_host_path_propagation
+except ImportError:
+    from validations import valid_host_path_propagation
+
+
+class BindMountType:
+    def __init__(self, render_instance: "Render", config: "IxStorageBindLikeConfigs"):
+        self._render_instance = render_instance
+        self.spec: dict = {}
+
+        propagation = valid_host_path_propagation(config.get("propagation", "rprivate"))
+        create_host_path = config.get("create_host_path", False)
+
+        self.spec: dict = {
+            "bind": {
+                "create_host_path": create_host_path,
+                "propagation": propagation,
+            }
+        }
+
+    def render(self) -> dict:
+        """Render the bind mount specification."""
+        return self.spec
+
+
+class VolumeMountType:
+    def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"):
+        self._render_instance = render_instance
+        self.spec: dict = {}
+
+        self.spec: dict = {"volume": {"nocopy": config.get("nocopy", False)}}
+
+    def render(self) -> dict:
+        """Render the volume mount specification."""
+        return self.spec

+ 108 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/volume_sources.py

@@ -0,0 +1,108 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+    from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig
+
+try:
+    from .error import RenderError
+    from .formatter import get_hashed_name_for_volume
+    from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise
+except ImportError:
+    from error import RenderError
+    from formatter import get_hashed_name_for_volume
+    from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise
+
+
+class HostPathSource:
+    def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"):
+        self._render_instance = render_instance
+        self.source: str = ""
+
+        if not config:
+            raise RenderError("Expected [host_path_config] to be set for [host_path] type.")
+
+        path = ""
+        if config.get("acl_enable", False):
+            acl_path = config.get("acl", {}).get("path")
+            if not acl_path:
+                raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.")
+            path = valid_fs_path_or_raise(acl_path)
+        else:
+            path = valid_fs_path_or_raise(config.get("path", ""))
+
+        path = path.rstrip("/")
+        self.source = allowed_fs_host_path_or_raise(path)
+
+    def get(self):
+        return self.source
+
+
+class IxVolumeSource:
+    def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"):
+        self._render_instance = render_instance
+        self.source: str = ""
+
+        if not config:
+            raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.")
+        dataset_name = config.get("dataset_name")
+        if not dataset_name:
+            raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.")
+
+        ix_volumes = self._render_instance.values.get("ix_volumes", {})
+        if dataset_name not in ix_volumes:
+            available = ", ".join(ix_volumes.keys())
+            raise RenderError(
+                f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. "
+                f"Available keys: [{available}]."
+            )
+
+        path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/"))
+        self.source = allowed_fs_host_path_or_raise(path, True)
+
+    def get(self):
+        return self.source
+
+
+class CifsSource:
+    def __init__(self, render_instance: "Render", config: dict):
+        self._render_instance = render_instance
+        self.source: str = ""
+
+        if not config:
+            raise RenderError("Expected [cifs_config] to be set for [cifs] type.")
+        self.source = get_hashed_name_for_volume("cifs", config)
+
+    def get(self):
+        return self.source
+
+
+class NfsSource:
+    def __init__(self, render_instance: "Render", config: dict):
+        self._render_instance = render_instance
+        self.source: str = ""
+
+        if not config:
+            raise RenderError("Expected [nfs_config] to be set for [nfs] type.")
+        self.source = get_hashed_name_for_volume("nfs", config)
+
+    def get(self):
+        return self.source
+
+
+class VolumeSource:
+    def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"):
+        self._render_instance = render_instance
+        self.source: str = ""
+
+        if not config:
+            raise RenderError("Expected [volume_config] to be set for [volume] type.")
+
+        volume_name: str = config.get("volume_name", "")
+        if not volume_name:
+            raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.")
+
+        self.source = volume_name
+
+    def get(self):
+        return self.source

+ 133 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/volume_types.py

@@ -0,0 +1,133 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+    from storage import IxStorageNfsConfig, IxStorageCifsConfig, IxStorageVolumeConfig
+
+
+try:
+    from .error import RenderError
+    from .formatter import escape_dollar
+    from .validations import valid_fs_path_or_raise
+except ImportError:
+    from error import RenderError
+    from formatter import escape_dollar
+    from validations import valid_fs_path_or_raise
+
+
+class NfsVolume:
+    def __init__(self, render_instance: "Render", config: "IxStorageNfsConfig"):
+        self._render_instance = render_instance
+
+        if not config:
+            raise RenderError("Expected [nfs_config] to be set for [nfs] type")
+
+        required_keys = ["server", "path"]
+        for key in required_keys:
+            if not config.get(key):
+                raise RenderError(f"Expected [{key}] to be set for [nfs] type")
+
+        opts = [f"addr={config['server']}"]
+        cfg_options = config.get("options")
+        if cfg_options:
+            if not isinstance(cfg_options, list):
+                raise RenderError("Expected [nfs_config.options] to be a list for [nfs] type")
+
+            tracked_keys: set[str] = set()
+            disallowed_opts = ["addr"]
+            for opt in cfg_options:
+                if not isinstance(opt, str):
+                    raise RenderError("Options for [nfs] type must be a list of strings.")
+
+                key = opt.split("=")[0]
+                if key in tracked_keys:
+                    raise RenderError(f"Option [{key}] already added for [nfs] type.")
+                if key in disallowed_opts:
+                    raise RenderError(f"Option [{key}] is not allowed for [nfs] type.")
+                opts.append(opt)
+                tracked_keys.add(key)
+
+        opts.sort()
+
+        path = valid_fs_path_or_raise(config["path"].rstrip("/"))
+        self.volume_spec = {
+            "driver_opts": {
+                "type": "nfs",
+                "device": f":{path}",
+                "o": f"{','.join([escape_dollar(opt) for opt in opts])}",
+            },
+        }
+
+    def get(self):
+        return self.volume_spec
+
+
+class CifsVolume:
+    def __init__(self, render_instance: "Render", config: "IxStorageCifsConfig"):
+        self._render_instance = render_instance
+        self.volume_spec: dict = {}
+
+        if not config:
+            raise RenderError("Expected [cifs_config] to be set for [cifs] type")
+
+        required_keys = ["server", "path", "username", "password"]
+        for key in required_keys:
+            if not config.get(key):
+                raise RenderError(f"Expected [{key}] to be set for [cifs] type")
+
+        opts = [
+            "noperm",
+            f"user={config['username']}",
+            f"password={config['password']}",
+        ]
+
+        domain = config.get("domain")
+        if domain:
+            opts.append(f"domain={domain}")
+
+        cfg_options = config.get("options")
+        if cfg_options:
+            if not isinstance(cfg_options, list):
+                raise RenderError("Expected [cifs_config.options] to be a list for [cifs] type")
+
+            tracked_keys: set[str] = set()
+            disallowed_opts = ["user", "password", "domain", "noperm"]
+            for opt in cfg_options:
+                if not isinstance(opt, str):
+                    raise RenderError("Options for [cifs] type must be a list of strings.")
+
+                key = opt.split("=")[0]
+                if key in tracked_keys:
+                    raise RenderError(f"Option [{key}] already added for [cifs] type.")
+                if key in disallowed_opts:
+                    raise RenderError(f"Option [{key}] is not allowed for [cifs] type.")
+                for disallowed in disallowed_opts:
+                    if key == disallowed:
+                        raise RenderError(f"Option [{key}] is not allowed for [cifs] type.")
+                opts.append(opt)
+                tracked_keys.add(key)
+        opts.sort()
+
+        server = config["server"].lstrip("/")
+        path = config["path"].strip("/")
+        path = valid_fs_path_or_raise("/" + path).lstrip("/")
+
+        self.volume_spec = {
+            "driver_opts": {
+                "type": "cifs",
+                "device": f"//{server}/{path}",
+                "o": f"{','.join([escape_dollar(opt) for opt in opts])}",
+            },
+        }
+
+    def get(self):
+        return self.volume_spec
+
+
+class DockerVolume:
+    def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"):
+        self._render_instance = render_instance
+        self.volume_spec: dict = {}
+
+    def get(self):
+        return self.volume_spec

+ 61 - 0
ix-dev/community/actual-budget/templates/library/base_v2_1_49/volumes.py

@@ -0,0 +1,61 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+
+
+try:
+    from .error import RenderError
+    from .storage import IxStorageVolumeLikeConfigs
+    from .volume_types import NfsVolume, CifsVolume, DockerVolume
+except ImportError:
+    from error import RenderError
+    from storage import IxStorageVolumeLikeConfigs
+    from volume_types import NfsVolume, CifsVolume, DockerVolume
+
+
+class Volumes:
+    def __init__(self, render_instance: "Render"):
+        self._render_instance = render_instance
+        self._volumes: dict[str, Volume] = {}
+
+    def add_volume(self, source: str, storage_type: str, config: "IxStorageVolumeLikeConfigs"):
+        # This method can be called many times from the volume mounts
+        # Only add the volume if it is not already added, but dont raise an error
+        if source == "":
+            raise RenderError(f"Volume source [{source}] cannot be empty")
+
+        if source in self._volumes:
+            return
+
+        self._volumes[source] = Volume(self._render_instance, storage_type, config)
+
+    def has_volumes(self) -> bool:
+        return bool(self._volumes)
+
+    def render(self):
+        return {name: v.render() for name, v in sorted(self._volumes.items()) if v.render() is not None}
+
+
+class Volume:
+    def __init__(
+        self,
+        render_instance: "Render",
+        storage_type: str,
+        config: "IxStorageVolumeLikeConfigs",
+    ):
+        self._render_instance = render_instance
+        self.volume_spec: dict | None = {}
+
+        match storage_type:
+            case "nfs":
+                self.volume_spec = NfsVolume(self._render_instance, config).get()  # type: ignore
+            case "cifs":
+                self.volume_spec = CifsVolume(self._render_instance, config).get()  # type: ignore
+            case "volume" | "temporary":
+                self.volume_spec = DockerVolume(self._render_instance, config).get()  # type: ignore
+            case _:
+                self.volume_spec = None
+
+    def render(self):
+        return self.volume_spec

+ 32 - 0
ix-dev/community/actual-budget/templates/test_values/basic-values.yaml

@@ -0,0 +1,32 @@
+resources:
+  limits:
+    cpus: 2.0
+    memory: 4096
+
+actual_budget:
+  additional_envs: []
+network:
+  host_network: false
+  certificate_id: null
+  web_port:
+    bind_mode: published
+    port_number: 8080
+
+run_as:
+  user: 568
+  group: 568
+
+ix_volumes:
+  data: /opt/tests/mnt/data
+
+storage:
+  data:
+    type: ix_volume
+    ix_volume_config:
+      dataset_name: data
+      create_host_path: true
+  additional_storage:
+    - type: anonymous
+      mount_path: /scratchpad
+      volume_config:
+        nocopy: true

+ 31 - 0
ix-dev/community/actual-budget/templates/test_values/hostnet-values.yaml

@@ -0,0 +1,31 @@
+resources:
+  limits:
+    cpus: 2.0
+    memory: 4096
+
+actual_budget:
+  additional_envs: []
+network:
+  host_network: true
+  web_port:
+    bind_mode: published
+    port_number: 8080
+
+run_as:
+  user: 568
+  group: 568
+
+ix_volumes:
+  data: /opt/tests/mnt/data
+
+storage:
+  data:
+    type: ix_volume
+    ix_volume_config:
+      dataset_name: data
+      create_host_path: true
+  additional_storage:
+    - type: anonymous
+      mount_path: /scratchpad
+      volume_config:
+        nocopy: true

+ 118 - 0
ix-dev/community/actual-budget/templates/test_values/https-values.yaml

@@ -0,0 +1,118 @@
+resources:
+  limits:
+    cpus: 2.0
+    memory: 4096
+
+actual_budget:
+  additional_envs: []
+network:
+  host_network: false
+  certificate_id: "1"
+  web_port:
+    bind_mode: published
+    port_number: 8080
+
+run_as:
+  user: 568
+  group: 568
+
+ix_volumes:
+  data: /opt/tests/mnt/data
+
+storage:
+  data:
+    type: ix_volume
+    ix_volume_config:
+      dataset_name: data
+      create_host_path: true
+  additional_storage:
+    - type: anonymous
+      mount_path: /scratchpad
+      volume_config:
+        nocopy: true
+
+ix_certificates:
+  "1":
+    certificate: |
+      -----BEGIN CERTIFICATE-----
+      MIIEdjCCA16gAwIBAgIDYFMYMA0GCSqGSIb3DQEBCwUAMGwxDDAKBgNVBAMMA2Fz
+      ZDELMAkGA1UEBhMCVVMxDTALBgNVBAgMBGFzZGYxCzAJBgNVBAcMAmFmMQ0wCwYD
+      VQQKDARhc2RmMQwwCgYDVQQLDANhc2QxFjAUBgkqhkiG9w0BCQEWB2FAYS5jb20w
+      HhcNMjEwODMwMjMyMzU0WhcNMjMxMjAzMjMyMzU0WjBuMQswCQYDVQQDDAJhZDEL
+      MAkGA1UEBhMCVVMxDTALBgNVBAgMBGFzZGYxDTALBgNVBAcMBGFzZGYxDTALBgNV
+      BAoMBGFkc2YxDTALBgNVBAsMBGFzZGYxFjAUBgkqhkiG9w0BCQEWB2FAYS5jb20w
+      ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7+1xOHRQyOnQTHFcrdasX
+      Zl0gzutVlA890a1wiQpdD5dOtCLo7+eqVYjqVKo9W8RUIArXWmBu/AbkH7oVFWC1
+      P973W1+ArF5sA70f7BZgqRKJTIisuIFIlRETgfnP2pfQmHRZtGaIJRZI4vQCdYgW
+      2g0KOvvNcZJCVq1OrhKiNiY1bWCp66DGg0ic6OEkZFHTm745zUNQaf2dNgsxKU0H
+      PGjVLJI//yrRFAOSBUqgD4c50krnMF7fU/Fqh+UyOu8t6Y/HsySh3urB+Zie331t
+      AzV6QV39KKxRflNx/yuWrtIEslGTm+xHKoCYJEk/nZ3mX8Y5hG6wWAb7A/FuDVg3
+      AgMBAAGjggEdMIIBGTAnBgNVHREEIDAehwTAqAADhwTAqAAFhwTAqAC2hwTAqACB
+      hwTAqACSMB0GA1UdDgQWBBQ4G2ff4tgZl4vmo4xCfqmJhdqShzAMBgNVHRMBAf8E
+      AjAAMIGYBgNVHSMEgZAwgY2AFLlYf9L99nxJDcpCM/LT3V5hQ/a3oXCkbjBsMQww
+      CgYDVQQDDANhc2QxCzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARhc2RmMQswCQYDVQQH
+      DAJhZjENMAsGA1UECgwEYXNkZjEMMAoGA1UECwwDYXNkMRYwFAYJKoZIhvcNAQkB
+      FgdhQGEuY29tggNgUxcwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwEwDgYDVR0PAQH/
+      BAQDAgWgMA0GCSqGSIb3DQEBCwUAA4IBAQA6FpOInEHB5iVk3FP67GybJ29vHZTD
+      KQHbQgmg8s4L7qIsA1HQ+DMCbdylpA11x+t/eL/n48BvGw2FNXpN6uykhLHJjbKR
+      h8yITa2KeD3LjLYhScwIigXmTVYSP3km6s8jRL6UKT9zttnIHyXVpBDya6Q4WTMx
+      fmfC6O7t1PjQ5ZyVtzizIUP8ah9n4TKdXU4A3QIM6WsJXpHb+vqp1WDWJ7mKFtgj
+      x5TKv3wcPnktx0zMPfLb5BTSE9rc9djcBG0eIAsPT4FgiatCUChe7VhuMnqskxEz
+      MymJLoq8+mzucRwFkOkR2EIt1x+Irl2mJVMeBow63rVZfUQBD8h++LqB
+      -----END CERTIFICATE-----
+      -----BEGIN CERTIFICATE-----
+      MIIEhDCCA2ygAwIBAgIDYFMXMA0GCSqGSIb3DQEBCwUAMGwxDDAKBgNVBAMMA2Fz
+      ZDELMAkGA1UEBhMCVVMxDTALBgNVBAgMBGFzZGYxCzAJBgNVBAcMAmFmMQ0wCwYD
+      VQQKDARhc2RmMQwwCgYDVQQLDANhc2QxFjAUBgkqhkiG9w0BCQEWB2FAYS5jb20w
+      HhcNMjEwODMwMjMyMDQ1WhcNMzEwODI4MjMyMDQ1WjBsMQwwCgYDVQQDDANhc2Qx
+      CzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARhc2RmMQswCQYDVQQHDAJhZjENMAsGA1UE
+      CgwEYXNkZjEMMAoGA1UECwwDYXNkMRYwFAYJKoZIhvcNAQkBFgdhQGEuY29tMIIB
+      IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq//c0hEEr83CS1pMgsHX50jt
+      2MqIbcf63UUNJTiYpUUvUQSFJFc7m/dr+RTZvu97eDCnD5K2qkHHvTPaPZwY+Djf
+      iy7N641Sz6u/y3Yo3xxs1Aermsfedh48vusJpjbkT2XS44VjbkrpKcWDNVpp3Evd
+      M7oJotXeUsZ+imiyVCfr4YhoY5gbGh/r+KN9Wf9YKoUyfLLZGwdZkhtX2zIbidsL
+      Thqi9YTaUHttGinjiBBum234u/CfvKXsfG3yP2gvBGnlvZnM9ktv+lVffYNqlf7H
+      VmB1bKKk84HtzuW5X76SGAgOG8eHX4x5ZLI1WQUuoQOVRl1I0UCjBtbz8XhwvQID
+      AQABo4IBLTCCASkwLQYDVR0RBCYwJIcEwKgABYcEwKgAA4cEwKgAkocEwKgAtYcE
+      wKgAgYcEwKgAtjAdBgNVHQ4EFgQUuVh/0v32fEkNykIz8tPdXmFD9rcwDwYDVR0T
+      AQH/BAUwAwEB/zCBmAYDVR0jBIGQMIGNgBS5WH/S/fZ8SQ3KQjPy091eYUP2t6Fw
+      pG4wbDEMMAoGA1UEAwwDYXNkMQswCQYDVQQGEwJVUzENMAsGA1UECAwEYXNkZjEL
+      MAkGA1UEBwwCYWYxDTALBgNVBAoMBGFzZGYxDDAKBgNVBAsMA2FzZDEWMBQGCSqG
+      SIb3DQEJARYHYUBhLmNvbYIDYFMXMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF
+      BQcDAjAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggEBAKEocOmVuWlr
+      zegtKYMe8NhHIkFY9oVn5ym6RHNOJpPH4QF8XYC3Z5+iC5yGh4P/jVe/4I4SF6Ql
+      PtofU0jNq5vzapt/y+m008eXqPQFmoUOvu+JavoRVcRx2LIP5AgBA1mF56CSREsX
+      TkuJAA9IUQ8EjnmAoAeKINuPaKxGDuU8BGCMqr/qd564MKNf9XYL+Fb2rlkA0O2d
+      2No34DQLgqSmST/LAvPM7Cbp6knYgnKmGr1nETCXasg1cueHLnWWTvps2HiPp2D/
+      +Fq0uqcZLu4Mdo0CPs4e5sHRyldEnRSKh0DVLprq9zr/GMipmPLJUsT5Jed3sj0w
+      M7Y3vwxshpo=
+      -----END CERTIFICATE-----
+    privatekey: |
+      -----BEGIN PRIVATE KEY-----
+      MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC7+1xOHRQyOnQT
+      HFcrdasXZl0gzutVlA890a1wiQpdD5dOtCLo7+eqVYjqVKo9W8RUIArXWmBu/Abk
+      H7oVFWC1P973W1+ArF5sA70f7BZgqRKJTIisuIFIlRETgfnP2pfQmHRZtGaIJRZI
+      4vQCdYgW2g0KOvvNcZJCVq1OrhKiNiY1bWCp66DGg0ic6OEkZFHTm745zUNQaf2d
+      NgsxKU0HPGjVLJI//yrRFAOSBUqgD4c50krnMF7fU/Fqh+UyOu8t6Y/HsySh3urB
+      +Zie331tAzV6QV39KKxRflNx/yuWrtIEslGTm+xHKoCYJEk/nZ3mX8Y5hG6wWAb7
+      A/FuDVg3AgMBAAECggEAapt30rj9DitGTtxAt13pJMEhyYxvvD3WkvmJwguF/Bbu
+      eW0Ba1c668fMeRCA54FWi1sMqusPS4HUqqUvk+tmyAOsAF4qgD/A4MMSC7uJSVI5
+      N/JWhJWyhCY94/FPakiO1nbPbVw41bcqtzU2qvparpME2CtxSCbDiqm7aaag3Kqe
+      EF0fGSUdZ+TYl9JM05+eIyiX+UY19Fg0OjTHMn8nGpxcNTfDBdQ68TKvdo/dtIKL
+      PLKzJUNNdM8odC4CvQtfGMqaslwZwXkiOl5VJcW21ncj/Y0ngEMKeD/i65ZoqGdR
+      0FKCQYEAGtM2FvJcZQ92Wsw7yj2bK2MSegVUyLK32QKBgQDe8syVCepPzRsfjfxA
+      6TZlWcGuTZLhwIx97Ktw3VcQ1f4rLoEYlv0xC2VWBORpzIsJo4I/OLmgp8a+Ga8z
+      FkVRnq90dV3t4NP9uJlHgcODHnOardC2UUka4olBSCG6zmK4Jxi34lOxhGRkshOo
+      L4IBeOIB5g+ZrEEXkzfYJHESRQKBgQDX2YhFhGIrT8BAnC5BbXbhm8h6Bhjz8DYL
+      d+qhVJjef7L/aJxViU0hX9Ba2O8CLK3FZeREFE3hJPiJ4TZSlN4evxs5p+bbNDcA
+      0mhRI/o3X4ac6IxdRebyYnCOB/Cu94/MzppcZcotlCekKNike7eorCcX4Qavm7Pu
+      MUuQ+ifmSwKBgEnchoqZzlbBzMqXb4rRuIO7SL9GU/MWp3TQg7vQmJerTZlgvsQ2
+      wYsOC3SECmhCq4117iCj2luvOdihCboTFsQDnn0mpQe6BIF6Ns3J38wAuqv0CcFd
+      DKsrge1uyD3rQilgSoAhKzkUc24o0PpXQurZ8YZPgbuXpbj5vPaOnCdBAoGACYc7
+      wb3XS4wos3FxhUfcwJbM4b4VKeeHqzfu7pI6cU/3ydiHVitKcVe2bdw3qMPqI9Wc
+      nvi6e17Tbdq4OCsEJx1OiVwFD9YdO3cOTc6lw/3+hjypvZBRYo+/4jUthbu96E+S
+      dtOzehGZMmDvN0uSzupSi3ZOgkAAUFpyuIKickMCgYAId0PCRjonO2thn/R0rZ7P
+      //L852uyzYhXKw5/fjFGhQ6LbaLgIRFaCZ0L2809u0HFnNvJjHv4AKP6j+vFQYYY
+      qQ+66XnfsA9G/bu4MDS9AX83iahD9IdLXQAy8I19prAbpVumKegPbMnNYNB/TYEc
+      3G15AKCXo7jjOUtHY01DCQ==
+      -----END PRIVATE KEY-----

+ 11 - 0
ix-dev/community/adguard-home/README.md

@@ -0,0 +1,11 @@
+# AdGuard Home
+
+[AdGuard Home](https://github.com/AdguardTeam/AdGuardHome) is a network-wide ads & trackers blocking DNS server
+
+During the setup wizard, AdGuard Home presents an option to select on which port the web interface will be available.
+(Defaults to 80. Which is a privileged port and also usually the TrueNAS SCALE UI uses that port)
+Because of that, App will force the webUI to listen to port 30000 (or the port selected by user in the TrueNAS SCALE UI).
+
+If you select a different port in the wizard, the Dashboard will not work initially but
+after a couple of minutes container will automatically restart and the Dashboard will
+be available on the port you selected on the TrueNAS SCALE UI.

+ 48 - 0
ix-dev/community/adguard-home/app.yaml

@@ -0,0 +1,48 @@
+annotations:
+  min_scale_version: 24.10.2.2
+app_version: v0.107.65
+capabilities:
+- description: Adguard is able to change file ownership arbitrarily
+  name: CHOWN
+- description: Adguard is able to bypass file permission checks
+  name: DAC_OVERRIDE
+- description: Adguard is able to bypass permission checks for file operations
+  name: FOWNER
+- description: Adguard is able to bind to privileged ports (< 1024)
+  name: NET_BIND_SERVICE
+- description: Adguard is able to use raw and packet sockets
+  name: NET_RAW
+categories:
+- networking
+changelog_url: https://github.com/AdguardTeam/AdGuardHome/releases
+date_added: '2024-06-20'
+description: Free and open source, powerful network-wide ads & trackers blocking DNS
+  server.
+home: https://github.com/AdguardTeam/AdGuardHome
+host_mounts: []
+icon: https://media.sys.truenas.net/apps/adguard-home/icons/icon.svg
+keywords:
+- dns
+- adblock
+lib_version: 2.1.49
+lib_version_hash: e71e6b0122c9446fa5ea6fb07e7eb01b11fb42d549a19845426bbd7e21a42634
+maintainers:
+- email: dev@ixsystems.com
+  name: truenas
+  url: https://www.truenas.com/
+name: adguard-home
+run_as_context:
+- description: AdGuard Home runs as root user.
+  gid: 0
+  group_name: root
+  uid: 0
+  user_name: root
+screenshots:
+- https://media.sys.truenas.net/apps/adguard-home/screenshots/screenshot1.png
+- https://media.sys.truenas.net/apps/adguard-home/screenshots/screenshot2.png
+sources:
+- https://github.com/AdguardTeam/AdGuardHome
+- https://hub.docker.com/r/adguard/adguardhome
+title: AdGuard Home
+train: community
+version: 1.2.9

+ 6 - 0
ix-dev/community/adguard-home/app_migrations.yaml

@@ -0,0 +1,6 @@
+migrations:
+- file: ip_port_migration
+  from:
+    max_version: 1.1.27
+  target:
+    min_version: 1.2.0

+ 9 - 0
ix-dev/community/adguard-home/item.yaml

@@ -0,0 +1,9 @@
+categories:
+- networking
+icon_url: https://media.sys.truenas.net/apps/adguard-home/icons/icon.svg
+screenshots:
+- https://media.sys.truenas.net/apps/adguard-home/screenshots/screenshot1.png
+- https://media.sys.truenas.net/apps/adguard-home/screenshots/screenshot2.png
+tags:
+- dns
+- adblock

+ 12 - 0
ix-dev/community/adguard-home/ix_values.yaml

@@ -0,0 +1,12 @@
+images:
+  image:
+    repository: adguard/adguardhome
+    tag: v0.107.65
+
+consts:
+  adguard_container_name: adguard
+  config_path: /opt/adguardhome/conf
+  work_path: /opt/adguardhome/work
+  notes_body: |
+    After initial setup, you need to stop and start the app from
+    the TrueNAS SCALE UI for the configuration to take effect.

+ 28 - 0
ix-dev/community/adguard-home/migrations/ip_port_migration

@@ -0,0 +1,28 @@
+#!/usr/bin/python3
+
+import os
+import sys
+import yaml
+
+
+def migrate(values):
+    values["network"]["web_port"] = {
+        "port_number": values["network"]["web_port"],
+        "bind_mode": "published",
+        "host_ips": [],
+    }
+    values["network"]["dns_port"] = {
+        "port_number": values["network"].get("dns_port", 1053),
+        "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()))))

+ 596 - 0
ix-dev/community/adguard-home/questions.yaml

@@ -0,0 +1,596 @@
+groups:
+  - name: AdGuard Home Configuration
+    description: Configure AdGuard Home
+  - name: Network Configuration
+    description: Configure Network for AdGuard Home
+  - name: Storage Configuration
+    description: Configure Storage for AdGuard Home
+  - name: Labels Configuration
+    description: Configure Labels for AdGuard Home
+  - name: Resources Configuration
+    description: Configure Resources for AdGuard Home
+
+questions:
+  - variable: adguard
+    label: ""
+    group: AdGuard Home Configuration
+    schema:
+      type: dict
+      attrs:
+        - variable: use_https_probe
+          label: Use HTTPS Probe
+          description: |
+            Use HTTPS probe for AdGuard Home.</br>
+            Only enable this after you have configured HTTPS
+            in the AdGuard Home web UI.
+          schema:
+            type: boolean
+            default: false
+        - variable: https_port_to_probe
+          label: HTTPS Port to Probe
+          description: |
+            The port to probe for HTTPS.</br>
+            Only enable this after you have configured HTTPS
+            in the AdGuard Home web UI.</br>
+            Note that usually this port is different from the WebUI port.
+          schema:
+            type: int
+            default: 30004
+            required: true
+            show_if: [["use_https_probe", "=", true]]
+        - 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
+
+  - variable: network
+    label: ""
+    group: Network Configuration
+    schema:
+      type: dict
+      attrs:
+        - variable: web_port
+          label: WebUI Port
+          schema:
+            type: dict
+            attrs:
+              - variable: bind_mode
+                label: Port Bind Mode
+                description: |
+                  The port bind mode.</br>
+                  - Publish: The port will be published on the host for external access.</br>
+                  - Expose: The port will be exposed for inter-container communication.</br>
+                  - None: The port will not be exposed or published.</br>
+                  Note: If the Dockerfile defines an EXPOSE directive,
+                  the port will still be exposed for inter-container communication regardless of this setting.
+                schema:
+                  type: string
+                  default: "published"
+                  enum:
+                    - value: "published"
+                      description: Publish port on the host for external access
+                    - value: "exposed"
+                      description: Expose port for inter-container communication
+                    - value: ""
+                      description: None
+              - variable: port_number
+                label: Port Number
+                schema:
+                  type: int
+                  default: 30004
+                  min: 1
+                  max: 65535
+                  required: true
+              - variable: host_ips
+                label: Host IPs
+                description: IPs on the host to bind this port
+                schema:
+                  type: list
+                  show_if: [["bind_mode", "=", "published"]]
+                  default: []
+                  items:
+                    - variable: host_ip
+                      label: Host IP
+                      schema:
+                        type: string
+                        required: true
+                        $ref:
+                          - definitions/node_bind_ip
+        - variable: dns_port
+          label: DNS Port
+          schema:
+            type: dict
+            attrs:
+              - variable: bind_mode
+                label: Port Bind Mode
+                description: |
+                  The port bind mode.</br>
+                  - Publish: The port will be published on the host for external access.</br>
+                  - Expose: The port will be exposed for inter-container communication.</br>
+                  - None: The port will not be exposed or published.</br>
+                  Note: If the Dockerfile defines an EXPOSE directive,
+                  the port will still be exposed for inter-container communication regardless of this setting.
+                schema:
+                  type: string
+                  default: "published"
+                  enum:
+                    - value: "published"
+                      description: Publish port on the host for external access
+                    - value: "exposed"
+                      description: Expose port for inter-container communication
+                    - value: ""
+                      description: None
+              - variable: port_number
+                label: Port Number
+                schema:
+                  type: int
+                  show_if: [["bind_mode", "=", "published"]]
+                  default: 53
+                  min: 1
+                  max: 65535
+                  required: true
+              - variable: host_ips
+                label: Host IPs
+                description: IPs on the host to bind this port
+                schema:
+                  type: list
+                  show_if: [["bind_mode", "=", "published"]]
+                  default: []
+                  items:
+                    - variable: host_ip
+                      label: Host IP
+                      schema:
+                        type: string
+                        required: true
+                        $ref:
+                          - definitions/node_bind_ip
+        - variable: host_network
+          label: Host Network
+          description: |
+            Bind to the host network. It's recommended to keep this disabled.
+          schema:
+            type: boolean
+            show_if: [["dhcp_enabled", "=", false]]
+            default: false
+        - variable: dhcp_enabled
+          label: DHCP Enabled
+          description: |
+            Enable DHCP for the network.</br>
+            This will automatically enable host network.
+          schema:
+            type: boolean
+            default: false
+
+        - variable: additional_ports
+          label: Additional Ports
+          schema:
+            type: list
+            show_if: [["host_network", "=", false]]
+            items:
+              - variable: port
+                label: Port
+                schema:
+                  type: dict
+                  attrs:
+                    - variable: bind_mode
+                      label: Port Bind Mode
+                      description: |
+                        The port bind mode.</br>
+                        - Publish: The port will be published on the host for external access.</br>
+                        - Expose: The port will be exposed for inter-container communication.</br>
+                      schema:
+                        type: string
+                        default: "published"
+                        enum:
+                          - value: "published"
+                            description: Publish port on the host for external access
+                          - value: "exposed"
+                            description: Expose port for inter-container communication
+                    - variable: port_number
+                      label: Port Number
+                      schema:
+                        type: int
+                        min: 1
+                        max: 65535
+                        required: true
+                    - variable: container_port
+                      label: Container Port
+                      schema:
+                        type: int
+                        min: 1
+                        max: 65535
+                        required: true
+                    - variable: protocol
+                      label: Protocol
+                      schema:
+                        type: string
+                        required: true
+                        default: "tcp"
+                        enum:
+                          - value: "tcp"
+                            description: TCP
+                          - value: "udp"
+                            description: UDP
+                    - variable: host_ips
+                      label: Host IPs
+                      description: IPs on the host to bind this port
+                      schema:
+                        type: list
+                        show_if: [["bind_mode", "=", "published"]]
+                        default: []
+                        items:
+                          - variable: host_ip
+                            label: Host IP
+                            schema:
+                              type: string
+                              required: true
+                              $ref:
+                                - definitions/node_bind_ip
+
+  - variable: storage
+    label: ""
+    group: Storage Configuration
+    schema:
+      type: dict
+      attrs:
+        - variable: config
+          label: AdGuard Home Config Storage
+          description: The path to store AdGuard Home Config.
+          schema:
+            type: dict
+            attrs:
+              - variable: type
+                label: Type
+                description: |
+                  ixVolume: Is dataset created automatically by the system.</br>
+                  Host Path: Is a path that already exists on the system.
+                schema:
+                  type: string
+                  required: true
+                  default: "ix_volume"
+                  enum:
+                    - value: "host_path"
+                      description: Host Path (Path that already exists on the system)
+                    - value: "ix_volume"
+                      description: ixVolume (Dataset created automatically by the system)
+              - variable: ix_volume_config
+                label: ixVolume Configuration
+                description: The configuration for the ixVolume dataset.
+                schema:
+                  type: dict
+                  show_if: [["type", "=", "ix_volume"]]
+                  $ref:
+                    - "normalize/ix_volume"
+                  attrs:
+                    - variable: acl_enable
+                      label: Enable ACL
+                      description: Enable ACL for the storage.
+                      schema:
+                        type: boolean
+                        default: false
+                    - variable: dataset_name
+                      label: Dataset Name
+                      description: The name of the dataset to use for storage.
+                      schema:
+                        type: string
+                        required: true
+                        hidden: true
+                        default: "config"
+                    - variable: acl_entries
+                      label: ACL Configuration
+                      schema:
+                        type: dict
+                        show_if: [["acl_enable", "=", true]]
+                        attrs: []
+              - variable: host_path_config
+                label: Host Path Configuration
+                schema:
+                  type: dict
+                  show_if: [["type", "=", "host_path"]]
+                  attrs:
+                    - variable: acl_enable
+                      label: Enable ACL
+                      description: Enable ACL for the storage.
+                      schema:
+                        type: boolean
+                        default: false
+                    - variable: acl
+                      label: ACL Configuration
+                      schema:
+                        type: dict
+                        show_if: [["acl_enable", "=", true]]
+                        attrs: []
+                        $ref:
+                          - "normalize/acl"
+                    - variable: path
+                      label: Host Path
+                      description: The host path to use for storage.
+                      schema:
+                        type: hostpath
+                        show_if: [["acl_enable", "=", false]]
+                        required: true
+        - variable: work
+          label: AdGuard Home WorkDir Storage
+          description: The path to store AdGuard Home WorkDir.
+          schema:
+            type: dict
+            attrs:
+              - variable: type
+                label: Type
+                description: |
+                  ixVolume: Is dataset created automatically by the system.</br>
+                  Host Path: Is a path that already exists on the system.
+                schema:
+                  type: string
+                  required: true
+                  default: "ix_volume"
+                  enum:
+                    - value: "host_path"
+                      description: Host Path (Path that already exists on the system)
+                    - value: "ix_volume"
+                      description: ixVolume (Dataset created automatically by the system)
+              - variable: ix_volume_config
+                label: ixVolume Configuration
+                description: The configuration for the ixVolume dataset.
+                schema:
+                  type: dict
+                  show_if: [["type", "=", "ix_volume"]]
+                  $ref:
+                    - "normalize/ix_volume"
+                  attrs:
+                    - variable: acl_enable
+                      label: Enable ACL
+                      description: Enable ACL for the storage.
+                      schema:
+                        type: boolean
+                        default: false
+                    - variable: dataset_name
+                      label: Dataset Name
+                      description: The name of the dataset to use for storage.
+                      schema:
+                        type: string
+                        required: true
+                        hidden: true
+                        default: "work"
+                    - variable: acl_entries
+                      label: ACL Configuration
+                      schema:
+                        type: dict
+                        show_if: [["acl_enable", "=", true]]
+                        attrs: []
+              - variable: host_path_config
+                label: Host Path Configuration
+                schema:
+                  type: dict
+                  show_if: [["type", "=", "host_path"]]
+                  attrs:
+                    - variable: acl_enable
+                      label: Enable ACL
+                      description: Enable ACL for the storage.
+                      schema:
+                        type: boolean
+                        default: false
+                    - variable: acl
+                      label: ACL Configuration
+                      schema:
+                        type: dict
+                        show_if: [["acl_enable", "=", true]]
+                        attrs: []
+                        $ref:
+                          - "normalize/acl"
+                    - variable: path
+                      label: Host Path
+                      description: The host path to use for storage.
+                      schema:
+                        type: hostpath
+                        show_if: [["acl_enable", "=", false]]
+                        required: true
+        - variable: additional_storage
+          label: Additional Storage
+          schema:
+            type: list
+            default: []
+            items:
+              - variable: storageEntry
+                label: Storage Entry
+                schema:
+                  type: dict
+                  attrs:
+                    - variable: type
+                      label: Type
+                      description: |
+                        ixVolume: Is dataset created automatically by the system.</br>
+                        Host Path: Is a path that already exists on the system.</br>
+                        SMB Share: Is a SMB share that is mounted to as a volume.
+                      schema:
+                        type: string
+                        required: true
+                        default: "ix_volume"
+                        enum:
+                          - value: "host_path"
+                            description: Host Path (Path that already exists on the system)
+                          - value: "ix_volume"
+                            description: ixVolume (Dataset created automatically by the system)
+                          - value: "cifs"
+                            description: SMB/CIFS Share (Mounts a volume to a SMB share)
+                    - variable: read_only
+                      label: Read Only
+                      description: Mount the volume as read only.
+                      schema:
+                        type: boolean
+                        default: false
+                    - variable: mount_path
+                      label: Mount Path
+                      description: The path inside the container to mount the storage.
+                      schema:
+                        type: path
+                        required: true
+                    - variable: host_path_config
+                      label: Host Path Configuration
+                      schema:
+                        type: dict
+                        show_if: [["type", "=", "host_path"]]
+                        attrs:
+                          - variable: acl_enable
+                            label: Enable ACL
+                            description: Enable ACL for the storage.
+                            schema:
+                              type: boolean
+                              default: false
+                          - variable: acl
+                            label: ACL Configuration
+                            schema:
+                              type: dict
+                              show_if: [["acl_enable", "=", true]]
+                              attrs: []
+                              $ref:
+                                - "normalize/acl"
+                          - variable: path
+                            label: Host Path
+                            description: The host path to use for storage.
+                            schema:
+                              type: hostpath
+                              show_if: [["acl_enable", "=", false]]
+                              required: true
+                    - variable: ix_volume_config
+                      label: ixVolume Configuration
+                      description: The configuration for the ixVolume dataset.
+                      schema:
+                        type: dict
+                        show_if: [["type", "=", "ix_volume"]]
+                        $ref:
+                          - "normalize/ix_volume"
+                        attrs:
+                          - variable: acl_enable
+                            label: Enable ACL
+                            description: Enable ACL for the storage.
+                            schema:
+                              type: boolean
+                              default: false
+                          - variable: dataset_name
+                            label: Dataset Name
+                            description: The name of the dataset to use for storage.
+                            schema:
+                              type: string
+                              required: true
+                              default: "storage_entry"
+                          - variable: acl_entries
+                            label: ACL Configuration
+                            schema:
+                              type: dict
+                              show_if: [["acl_enable", "=", true]]
+                              attrs: []
+                    - variable: cifs_config
+                      label: SMB Configuration
+                      description: The configuration for the SMB dataset.
+                      schema:
+                        type: dict
+                        show_if: [["type", "=", "cifs"]]
+                        attrs:
+                          - variable: server
+                            label: Server
+                            description: The server to mount the SMB share.
+                            schema:
+                              type: string
+                              required: true
+                          - variable: path
+                            label: Path
+                            description: The path to mount the SMB share.
+                            schema:
+                              type: string
+                              required: true
+                          - variable: username
+                            label: Username
+                            description: The username to use for the SMB share.
+                            schema:
+                              type: string
+                              required: true
+                          - variable: password
+                            label: Password
+                            description: The password to use for the SMB share.
+                            schema:
+                              type: string
+                              required: true
+                              private: true
+                          - variable: domain
+                            label: Domain
+                            description: The domain to use for the SMB share.
+                            schema:
+                              type: string
+  - variable: labels
+    label: ""
+    group: Labels Configuration
+    schema:
+      type: list
+      default: []
+      items:
+        - variable: label
+          label: Label
+          schema:
+            type: dict
+            attrs:
+              - variable: key
+                label: Key
+                schema:
+                  type: string
+                  required: true
+              - variable: value
+                label: Value
+                schema:
+                  type: string
+                  required: true
+              - variable: containers
+                label: Containers
+                description: Containers where the label should be applied
+                schema:
+                  type: list
+                  items:
+                    - variable: container
+                      label: Container
+                      schema:
+                        type: string
+                        required: true
+                        enum:
+                          - value: adguard
+                            description: adguard
+  - variable: resources
+    label: ""
+    group: Resources Configuration
+    schema:
+      type: dict
+      attrs:
+        - variable: limits
+          label: Limits
+          schema:
+            type: dict
+            attrs:
+              - variable: cpus
+                label: CPUs
+                description: CPUs limit for Adguard Home.
+                schema:
+                  type: int
+                  default: 2
+                  required: true
+              - variable: memory
+                label: Memory (in MB)
+                description: Memory limit for Adguard Home.
+                schema:
+                  type: int
+                  default: 4096
+                  required: true

+ 44 - 0
ix-dev/community/adguard-home/templates/docker-compose.yaml

@@ -0,0 +1,44 @@
+{% set tpl = ix_lib.base.render.Render(values) %}
+
+{% set c1 = tpl.add_container(values.consts.adguard_container_name, "image") %}
+{% do c1.healthcheck.set_test("wget", {
+  "port": values.adguard.https_port_to_probe if values.adguard.use_https_probe else values.network.web_port.port_number,
+  "scheme": "https" if values.adguard.use_https_probe else "http",
+  "path": "/",
+}) %}
+{% do c1.environment.add_user_envs(values.adguard.additional_envs) %}
+
+{% do c1.add_caps(["NET_BIND_SERVICE", "CHOWN", "FOWNER", "DAC_OVERRIDE"]) %}
+{% if values.network.dhcp_enabled %}
+  {% do c1.add_caps(["NET_RAW"]) %}
+  {% do c1.set_network_mode("host") %}
+{% endif %}
+
+{% do c1.set_command([
+  "--no-check-update",
+  "--web-addr",
+  "0.0.0.0:%d"|format(values.network.web_port.port_number),
+  "--config",
+  "%s/AdGuardHome.yaml"|format(values.consts.config_path),
+  "--work-dir",
+  values.consts.work_path,
+]) %}
+
+{% do c1.add_port(values.network.web_port) %}
+{% do c1.add_port(values.network.dns_port, {"container_port": 53}) %}
+{% do c1.add_port(values.network.dns_port, {"container_port": 53, "protocol": "udp"}) %}
+
+{% for port in values.network.additional_ports %}
+  {% do c1.add_port(port) %}
+{% endfor %}
+
+{% do c1.add_storage(values.consts.config_path, values.storage.config) %}
+{% do c1.add_storage(values.consts.work_path, values.storage.work) %}
+{% for store in values.storage.additional_storage %}
+  {% do c1.add_storage(store.mount_path, store) %}
+{% endfor %}
+
+{% do tpl.portals.add(values.network.web_port) %}
+{% do tpl.notes.set_body(values.consts.notes_body) %}
+
+{{ tpl.render() | tojson }}

+ 0 - 0
ix-dev/community/adguard-home/templates/library/base_v2_1_49/__init__.py


+ 70 - 0
ix-dev/community/adguard-home/templates/library/base_v2_1_49/client.py

@@ -0,0 +1,70 @@
+import os
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+
+try:
+    from .error import RenderError
+except ImportError:
+    from error import RenderError
+
+
+def is_truenas_system():
+    """Check if we're running on a TrueNAS system"""
+    return "truenas" in os.uname().release
+
+
+# Import based on system detection
+if is_truenas_system():
+    from truenas_api_client import Client as TrueNASClient
+
+    try:
+        # 25.04 and later
+        from truenas_api_client.exc import ValidationErrors
+    except ImportError:
+        # 24.10 and earlier
+        from truenas_api_client import ValidationErrors
+else:
+    # Mock classes for non-TrueNAS systems
+    class TrueNASClient:
+        def call(self, *args, **kwargs):
+            return None
+
+    class ValidationErrors(Exception):
+        def __init__(self, errors):
+            self.errors = errors
+
+
+class Client:
+    def __init__(self, render_instance: "Render"):
+        self.client = TrueNASClient()
+        self._render_instance = render_instance
+        self._app_name: str = self._render_instance.values.get("ix_context", {}).get("app_name", "") or "unknown"
+
+    def validate_ip_port_combo(self, ip: str, port: int) -> None:
+        # Example of an error messages:
+        # The port is being used by following services: 1) "0.0.0.0:80" used by WebUI Service
+        # The port is being used by following services: 1) "0.0.0.0:9998" used by Applications ('$app_name' application)
+        try:
+            self.client.call("port.validate_port", f"render.{self._app_name}.schema", port, ip, None, True)
+        except ValidationErrors as e:
+            err_str = str(e)
+            # If the IP:port combo appears more than once in the error message,
+            # means that the port is used by more than one service/app.
+            # This shouldn't happen in a well-configured system.
+            # Notice that the ip portion is not included check,
+            # because input might be a specific IP, but another service or app
+            # might be using the same port on a wildcard IP
+            if err_str.count(f':{port}" used by') > 1:
+                raise RenderError(err_str) from None
+
+            # If the error complains about the current app, we ignore it
+            # This is to handle cases where the app is being updated or edited
+            if f"Applications ('{self._app_name}' application)" in err_str:
+                # During upgrade, we want to ignore the error if it is related to the current app
+                return
+
+            raise RenderError(err_str) from None
+        except Exception:
+            pass

+ 86 - 0
ix-dev/community/adguard-home/templates/library/base_v2_1_49/configs.py

@@ -0,0 +1,86 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+
+try:
+    from .error import RenderError
+    from .formatter import escape_dollar
+    from .validations import valid_octal_mode_or_raise, valid_fs_path_or_raise
+except ImportError:
+    from error import RenderError
+    from formatter import escape_dollar
+    from validations import valid_octal_mode_or_raise, valid_fs_path_or_raise
+
+
+class Configs:
+    def __init__(self, render_instance: "Render"):
+        self._render_instance = render_instance
+        self._configs: dict[str, dict] = {}
+
+    def add(self, name: str, data: str):
+        if not isinstance(data, str):
+            raise RenderError(f"Expected [data] to be a string, got [{type(data)}]")
+
+        if name not in self._configs:
+            self._configs[name] = {"name": name, "data": data}
+            return
+
+        if data == self._configs[name]["data"]:
+            return
+
+        raise RenderError(f"Config [{name}] already added with different data")
+
+    def has_configs(self):
+        return bool(self._configs)
+
+    def render(self):
+        return {
+            c["name"]: {"content": escape_dollar(c["data"])}
+            for c in sorted(self._configs.values(), key=lambda c: c["name"])
+        }
+
+
+class ContainerConfigs:
+    def __init__(self, render_instance: "Render", configs: Configs):
+        self._render_instance = render_instance
+        self.top_level_configs: Configs = configs
+        self.container_configs: set[ContainerConfig] = set()
+
+    def add(self, name: str, data: str, target: str, mode: str = ""):
+        self.top_level_configs.add(name, data)
+
+        if target == "":
+            raise RenderError(f"Expected [target] to be set for config [{name}]")
+        if mode != "":
+            mode = valid_octal_mode_or_raise(mode)
+
+        if target in [c.target for c in self.container_configs]:
+            raise RenderError(f"Target [{target}] already used for another config")
+        target = valid_fs_path_or_raise(target)
+        self.container_configs.add(ContainerConfig(self._render_instance, name, target, mode))
+
+    def has_configs(self):
+        return bool(self.container_configs)
+
+    def render(self):
+        return [c.render() for c in sorted(self.container_configs, key=lambda c: c.source)]
+
+
+class ContainerConfig:
+    def __init__(self, render_instance: "Render", source: str, target: str, mode: str):
+        self._render_instance = render_instance
+        self.source = source
+        self.target = target
+        self.mode = mode
+
+    def render(self):
+        result: dict[str, str | int] = {
+            "source": self.source,
+            "target": self.target,
+        }
+
+        if self.mode:
+            result["mode"] = int(self.mode, 8)
+
+        return result

+ 450 - 0
ix-dev/community/adguard-home/templates/library/base_v2_1_49/container.py

@@ -0,0 +1,450 @@
+from typing import Any, TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+    from storage import IxStorage
+
+try:
+    from .configs import ContainerConfigs
+    from .depends import Depends
+    from .deploy import Deploy
+    from .device_cgroup_rules import DeviceCGroupRules
+    from .devices import Devices
+    from .dns import Dns
+    from .environment import Environment
+    from .error import RenderError
+    from .expose import Expose
+    from .extra_hosts import ExtraHosts
+    from .formatter import escape_dollar, get_image_with_hashed_data
+    from .healthcheck import Healthcheck
+    from .labels import Labels
+    from .ports import Ports
+    from .restart import RestartPolicy
+    from .tmpfs import Tmpfs
+    from .validations import (
+        valid_cap_or_raise,
+        valid_cgroup_or_raise,
+        valid_ipc_mode_or_raise,
+        valid_network_mode_or_raise,
+        valid_pid_mode_or_raise,
+        valid_port_bind_mode_or_raise,
+        valid_port_mode_or_raise,
+        valid_pull_policy_or_raise,
+    )
+    from .security_opts import SecurityOpts
+    from .storage import Storage
+    from .sysctls import Sysctls
+except ImportError:
+    from configs import ContainerConfigs
+    from depends import Depends
+    from deploy import Deploy
+    from device_cgroup_rules import DeviceCGroupRules
+    from devices import Devices
+    from dns import Dns
+    from environment import Environment
+    from error import RenderError
+    from expose import Expose
+    from extra_hosts import ExtraHosts
+    from formatter import escape_dollar, get_image_with_hashed_data
+    from healthcheck import Healthcheck
+    from labels import Labels
+    from ports import Ports
+    from restart import RestartPolicy
+    from tmpfs import Tmpfs
+    from validations import (
+        valid_cap_or_raise,
+        valid_cgroup_or_raise,
+        valid_ipc_mode_or_raise,
+        valid_network_mode_or_raise,
+        valid_pid_mode_or_raise,
+        valid_port_bind_mode_or_raise,
+        valid_port_mode_or_raise,
+        valid_pull_policy_or_raise,
+    )
+    from security_opts import SecurityOpts
+    from storage import Storage
+    from sysctls import Sysctls
+
+
+class Container:
+    def __init__(self, render_instance: "Render", name: str, image: str):
+        self._render_instance = render_instance
+
+        self._name: str = name
+        self._image: str = self._resolve_image(image)
+        self._build_image: str = ""
+        self._pull_policy: str = ""
+        self._user: str = ""
+        self._tty: bool = False
+        self._stdin_open: bool = False
+        self._init: bool | None = None
+        self._read_only: bool | None = None
+        self._extra_hosts: ExtraHosts = ExtraHosts(self._render_instance)
+        self._hostname: str = ""
+        self._cap_drop: set[str] = set(["ALL"])  # Drop all capabilities by default and add caps granularly
+        self._cap_add: set[str] = set()
+        self._security_opt: SecurityOpts = SecurityOpts(self._render_instance)
+        self._privileged: bool = False
+        self._group_add: set[int | str] = set()
+        self._network_mode: str = ""
+        self._entrypoint: list[str] = []
+        self._command: list[str] = []
+        self._grace_period: int | None = None
+        self._shm_size: int | None = None
+        self._storage: Storage = Storage(self._render_instance, self)
+        self._tmpfs: Tmpfs = Tmpfs(self._render_instance, self)
+        self._ipc_mode: str | None = None
+        self._pid_mode: str | None = None
+        self._cgroup: str | None = None
+        self._device_cgroup_rules: DeviceCGroupRules = DeviceCGroupRules(self._render_instance)
+        self.sysctls: Sysctls = Sysctls(self._render_instance, self)
+        self.configs: ContainerConfigs = ContainerConfigs(self._render_instance, self._render_instance.configs)
+        self.deploy: Deploy = Deploy(self._render_instance)
+        self.networks: set[str] = set()
+        self.devices: Devices = Devices(self._render_instance)
+        self.environment: Environment = Environment(self._render_instance, self.deploy.resources)
+        self.dns: Dns = Dns(self._render_instance)
+        self.depends: Depends = Depends(self._render_instance)
+        self.healthcheck: Healthcheck = Healthcheck(self._render_instance)
+        self.labels: Labels = Labels(self._render_instance)
+        self.restart: RestartPolicy = RestartPolicy(self._render_instance)
+        self.ports: Ports = Ports(self._render_instance)
+        self.expose: Expose = Expose(self._render_instance)
+
+        self._auto_set_network_mode()
+        self._auto_add_labels()
+        self._auto_add_groups()
+
+    def _auto_add_groups(self):
+        self.add_group(568)
+
+    def _auto_set_network_mode(self):
+        if self._render_instance.values.get("network", {}).get("host_network", False):
+            self.set_network_mode("host")
+
+    def _auto_add_labels(self):
+        labels = self._render_instance.values.get("labels", [])
+        if not labels:
+            return
+
+        for label in labels:
+            containers = label.get("containers", [])
+            if not containers:
+                raise RenderError(f'Label [{label.get("key", "")}] must have at least one container')
+
+            if self._name in containers:
+                self.labels.add_label(label["key"], label["value"])
+
+    def _resolve_image(self, image: str):
+        images = self._render_instance.values["images"]
+        if image not in images:
+            raise RenderError(
+                f"Image [{image}] not found in values. " f"Available images: [{', '.join(images.keys())}]"
+            )
+        repo = images[image].get("repository", "")
+        tag = images[image].get("tag", "")
+
+        if not repo:
+            raise RenderError(f"Repository not found for image [{image}]")
+        if not tag:
+            raise RenderError(f"Tag not found for image [{image}]")
+
+        return f"{repo}:{tag}"
+
+    def build_image(self, content: list[str | None]):
+        dockerfile = f"FROM {self._image}\n"
+        for line in content:
+            line = line.strip() if line else ""
+            if not line:
+                continue
+            if line.startswith("FROM"):
+                # TODO: This will also block multi-stage builds
+                # We can revisit this later if we need it
+                raise RenderError(
+                    "FROM cannot be used in build image. Define the base image when creating the container."
+                )
+            dockerfile += line + "\n"
+
+        self._build_image = dockerfile
+        self._image = get_image_with_hashed_data(self._image, dockerfile)
+
+    def set_pull_policy(self, pull_policy: str):
+        self._pull_policy = valid_pull_policy_or_raise(pull_policy)
+
+    def set_user(self, user: int, group: int):
+        for i in (user, group):
+            if not isinstance(i, int) or i < 0:
+                raise RenderError(f"User/Group [{i}] is not valid")
+        self._user = f"{user}:{group}"
+
+    def add_extra_host(self, host: str, ip: str):
+        self._extra_hosts.add_host(host, ip)
+
+    def add_group(self, group: int | str):
+        if isinstance(group, str):
+            group = str(group).strip()
+            if group.isdigit():
+                raise RenderError(f"Group is a number [{group}] but passed as a string")
+
+        if group in self._group_add:
+            raise RenderError(f"Group [{group}] already added")
+        self._group_add.add(group)
+
+    def get_additional_groups(self) -> list[int | str]:
+        result = []
+        if self.deploy.resources.has_gpus() or self.devices.has_gpus():
+            result.append(44)  # video
+            result.append(107)  # render
+        return result
+
+    def get_current_groups(self) -> list[str]:
+        result = [str(g) for g in self._group_add]
+        result.extend([str(g) for g in self.get_additional_groups()])
+        return result
+
+    def set_tty(self, enabled: bool = False):
+        self._tty = enabled
+
+    def set_stdin(self, enabled: bool = False):
+        self._stdin_open = enabled
+
+    def set_ipc_mode(self, ipc_mode: str):
+        self._ipc_mode = valid_ipc_mode_or_raise(ipc_mode, self._render_instance.container_names())
+
+    def set_pid_mode(self, mode: str = ""):
+        self._pid_mode = valid_pid_mode_or_raise(mode, self._render_instance.container_names())
+
+    def add_device_cgroup_rule(self, dev_grp_rule: str):
+        self._device_cgroup_rules.add_rule(dev_grp_rule)
+
+    def set_cgroup(self, cgroup: str):
+        self._cgroup = valid_cgroup_or_raise(cgroup)
+
+    def set_init(self, enabled: bool = False):
+        self._init = enabled
+
+    def set_read_only(self, enabled: bool = False):
+        self._read_only = enabled
+
+    def set_hostname(self, hostname: str):
+        self._hostname = hostname
+
+    def set_grace_period(self, grace_period: int):
+        if grace_period < 0:
+            raise RenderError(f"Grace period [{grace_period}] cannot be negative")
+        self._grace_period = grace_period
+
+    def set_privileged(self, enabled: bool = False):
+        self._privileged = enabled
+
+    def clear_caps(self):
+        self._cap_add.clear()
+        self._cap_drop.clear()
+
+    def add_caps(self, caps: list[str]):
+        for c in caps:
+            if c in self._cap_add:
+                raise RenderError(f"Capability [{c}] already added")
+            self._cap_add.add(valid_cap_or_raise(c))
+
+    def add_security_opt(self, key: str, value: str | bool | None = None, arg: str | None = None):
+        self._security_opt.add_opt(key, value, arg)
+
+    def remove_security_opt(self, key: str):
+        self._security_opt.remove_opt(key)
+
+    def set_network_mode(self, mode: str):
+        self._network_mode = valid_network_mode_or_raise(mode, self._render_instance.container_names())
+
+    def add_port(self, port_config: dict | None = None, dev_config: dict | None = None):
+        port_config = port_config or {}
+        dev_config = dev_config or {}
+        # Merge port_config and dev_config (dev_config has precedence)
+        config = port_config | dev_config
+        bind_mode = valid_port_bind_mode_or_raise(config.get("bind_mode", ""))
+        # Skip port if its neither published nor exposed
+        if not bind_mode:
+            return
+
+        # Collect port config
+        mode = valid_port_mode_or_raise(config.get("mode", "ingress"))
+        host_port = config.get("port_number", 0)
+        container_port = config.get("container_port", 0) or host_port
+        protocol = config.get("protocol", "tcp")
+        host_ips = config.get("host_ips") or ["0.0.0.0", "::"]
+        if not isinstance(host_ips, list):
+            raise RenderError(f"Expected [host_ips] to be a list, got [{host_ips}]")
+
+        if bind_mode == "published":
+            for host_ip in host_ips:
+                self.ports._add_port(
+                    host_port, container_port, {"protocol": protocol, "host_ip": host_ip, "mode": mode}
+                )
+        elif bind_mode == "exposed":
+            self.expose.add_port(container_port, protocol)
+
+    def set_entrypoint(self, entrypoint: list[str]):
+        self._entrypoint = [escape_dollar(str(e)) for e in entrypoint]
+
+    def set_command(self, command: list[str]):
+        self._command = [escape_dollar(str(e)) for e in command]
+
+    def add_storage(self, mount_path: str, config: "IxStorage"):
+        if config.get("type", "") == "tmpfs":
+            self._tmpfs.add(mount_path, config)
+        else:
+            self._storage.add(mount_path, config)
+
+    def add_docker_socket(self, read_only: bool = True, mount_path: str = "/var/run/docker.sock"):
+        self.add_group(999)
+        self._storage._add_docker_socket(read_only, mount_path)
+
+    def add_udev(self, read_only: bool = True, mount_path: str = "/run/udev"):
+        self._storage._add_udev(read_only, mount_path)
+
+    def add_tun_device(self):
+        self.devices._add_tun_device()
+
+    def add_snd_device(self):
+        self.add_group(29)
+        self.devices._add_snd_device()
+
+    def set_shm_size_mb(self, size: int):
+        self._shm_size = size
+
+    # Easily remove devices from the container
+    # Useful in dependencies like postgres and redis
+    # where there is no need to pass devices to them
+    def remove_devices(self):
+        self.deploy.resources.remove_devices()
+        self.devices.remove_devices()
+
+    @property
+    def storage(self):
+        return self._storage
+
+    def render(self) -> dict[str, Any]:
+        if self._network_mode and self.networks:
+            raise RenderError("Cannot set both [network_mode] and [networks]")
+
+        result = {
+            "image": self._image,
+            "platform": "linux/amd64",
+            "tty": self._tty,
+            "stdin_open": self._stdin_open,
+            "restart": self.restart.render(),
+        }
+
+        if self._pull_policy:
+            result["pull_policy"] = self._pull_policy
+
+        if self.healthcheck.has_healthcheck():
+            result["healthcheck"] = self.healthcheck.render()
+
+        if self._hostname:
+            result["hostname"] = self._hostname
+
+        if self._build_image:
+            result["build"] = {"tags": [self._image], "dockerfile_inline": self._build_image}
+
+        if self.configs.has_configs():
+            result["configs"] = self.configs.render()
+
+        if self._ipc_mode is not None:
+            result["ipc"] = self._ipc_mode
+
+        if self._pid_mode is not None:
+            result["pid"] = self._pid_mode
+
+        if self._device_cgroup_rules.has_rules():
+            result["device_cgroup_rules"] = self._device_cgroup_rules.render()
+
+        if self._cgroup is not None:
+            result["cgroup"] = self._cgroup
+
+        if self._extra_hosts.has_hosts():
+            result["extra_hosts"] = self._extra_hosts.render()
+
+        if self._init is not None:
+            result["init"] = self._init
+
+        if self._read_only is not None:
+            result["read_only"] = self._read_only
+
+        if self._grace_period is not None:
+            result["stop_grace_period"] = f"{self._grace_period}s"
+
+        if self._user:
+            result["user"] = self._user
+
+        for g in self.get_additional_groups():
+            self.add_group(g)
+
+        if self._group_add:
+            result["group_add"] = sorted(self._group_add, key=lambda g: (isinstance(g, str), g))
+
+        if self._shm_size is not None:
+            result["shm_size"] = f"{self._shm_size}M"
+
+        if self._privileged is not None:
+            result["privileged"] = self._privileged
+
+        if self._cap_drop:
+            result["cap_drop"] = sorted(self._cap_drop)
+
+        if self._cap_add:
+            result["cap_add"] = sorted(self._cap_add)
+
+        if self._security_opt.has_opts():
+            result["security_opt"] = self._security_opt.render()
+
+        if self._network_mode:
+            result["network_mode"] = self._network_mode
+
+        if self.sysctls.has_sysctls():
+            result["sysctls"] = self.sysctls.render()
+
+        if self._network_mode != "host":
+            if self.ports.has_ports():
+                result["ports"] = self.ports.render()
+
+            if self.expose.has_ports():
+                result["expose"] = self.expose.render()
+
+        if self._entrypoint:
+            result["entrypoint"] = self._entrypoint
+
+        if self._command:
+            result["command"] = self._command
+
+        if self.devices.has_devices():
+            result["devices"] = self.devices.render()
+
+        if self.deploy.has_deploy():
+            result["deploy"] = self.deploy.render()
+
+        if self.environment.has_variables():
+            result["environment"] = self.environment.render()
+
+        if self.labels.has_labels():
+            result["labels"] = self.labels.render()
+
+        if self.dns.has_dns_nameservers():
+            result["dns"] = self.dns.render_dns_nameservers()
+
+        if self.dns.has_dns_searches():
+            result["dns_search"] = self.dns.render_dns_searches()
+
+        if self.dns.has_dns_opts():
+            result["dns_opt"] = self.dns.render_dns_opts()
+
+        if self.depends.has_dependencies():
+            result["depends_on"] = self.depends.render()
+
+        if self._storage.has_mounts():
+            result["volumes"] = self._storage.render()
+
+        if self._tmpfs.has_tmpfs():
+            result["tmpfs"] = self._tmpfs.render()
+
+        return result

+ 34 - 0
ix-dev/community/adguard-home/templates/library/base_v2_1_49/depends.py

@@ -0,0 +1,34 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from render import Render
+
+try:
+    from .error import RenderError
+    from .validations import valid_depend_condition_or_raise
+except ImportError:
+    from error import RenderError
+    from validations import valid_depend_condition_or_raise
+
+
+class Depends:
+    def __init__(self, render_instance: "Render"):
+        self._render_instance = render_instance
+        self._dependencies: dict[str, str] = {}
+
+    def add_dependency(self, name: str, condition: str):
+        condition = valid_depend_condition_or_raise(condition)
+        if name in self._dependencies.keys():
+            raise RenderError(f"Dependency [{name}] already added")
+        if name not in self._render_instance.container_names():
+            raise RenderError(
+                f"Dependency [{name}] not found in defined containers. "
+                f"Available containers: [{', '.join(self._render_instance.container_names())}]"
+            )
+        self._dependencies[name] = condition
+
+    def has_dependencies(self):
+        return len(self._dependencies) > 0
+
+    def render(self):
+        return {d: {"condition": c} for d, c in self._dependencies.items()}

Some files were not shown because too many files changed in this diff