Used AI to generate this brief script to extract all the app data for me to quickly rebuild each app in 25.04, etc.
- log into your truenas and open a shell, elevate to root (sudo su), and change to
/mnt/.ix-apps/app_configs/ or wherever your .ix-apps is located.
- Personally, I zipped up and copied it to my local machine.
7z a app_configs.7z * and then scp app_configs.7z user@laptop-ip:~/.
- Or, just can copy the script to this directory and just run it. Even though it’s read-only, I prefer not to run scripts on my production boxes.
#!/usr/bin/env python3
import os
import yaml
from packaging import version
from jinja2 import Template
# ----------------------------
# CONFIG: Keys to extract
# ----------------------------
KEYS_TO_EXTRACT = [
"run_as/user",
"run_as/group",
"ix_context/app_metadata/run_as_context/0/uid",
"ix_context/app_metadata/run_as_context/0/gid",
"ix_context/app_metadata/run_as_context/1/uid",
"ix_context/app_metadata/run_as_context/1/gid",
"ix_context/app_metadata/run_as_context/2/uid",
"ix_context/app_metadata/run_as_context/2/gid",
"network/peer_port",
"network/web_port",
"storage/config/host_path_config/path",
"resources/limits/cpus",
"resources/limits/memory",
]
# Optional Jinja2 template (can be customized)
OUTPUT_TEMPLATE = """\
{% for app, values in results.items() %}
{{ values["app_name"] }}{% for k, v in values["data"].items() %}
{{ k }}: {{ v }}{% endfor %}{% if values["storage"] %}
storage:{% for sk, sv in values["storage"].items() %}
{{ sk }}: {{ sv }}{% endfor %}{% endif %}
{% endfor %}
"""
# ----------------------------
# Helpers
# ----------------------------
def find_highest_version_dir(versions_path):
"""Return the highest version directory in versions_path."""
dirs = [
d for d in os.listdir(versions_path)
if os.path.isdir(os.path.join(versions_path, d))
]
if not dirs:
return None
dirs.sort(key=version.parse, reverse=True)
return dirs[0]
def deep_get(dictionary, path):
"""Safely get a value from nested dict/list using slash-separated path."""
keys = path.split("/")
current = dictionary
for key in keys:
if isinstance(current, dict):
current = current.get(key)
elif isinstance(current, list):
try:
idx = int(key)
current = current[idx]
except (ValueError, IndexError):
return None
else:
return None
return current
def flatten_storage(storage_dict):
"""Flatten storage section into key:value lines for display."""
results = {}
if not isinstance(storage_dict, dict):
return results
for name, entry in storage_dict.items():
if isinstance(entry, dict):
for subk, subv in entry.items():
if isinstance(subv, dict):
for k2, v2 in subv.items():
results[f"{name}/{subk}/{k2}"] = v2
else:
results[f"{name}/{subk}"] = subv
elif isinstance(entry, list):
# e.g. "additional_storage"
for i, item in enumerate(entry):
if isinstance(item, dict):
for subk, subv in item.items():
if isinstance(subv, dict):
for k2, v2 in subv.items():
results[f"{name}/{i}/{subk}/{k2}"] = v2
else:
results[f"{name}/{i}/{subk}"] = subv
else:
results[f"{name}/{i}"] = item
else:
results[name] = entry
return results
# ----------------------------
# Main
# ----------------------------
def main():
base_dir = "./app_configs"
results = {}
for app in os.listdir(base_dir):
app_path = os.path.join(base_dir, app, "versions")
if not os.path.isdir(app_path):
continue
latest_version = find_highest_version_dir(app_path)
if not latest_version:
continue
yaml_file = os.path.join(app_path, latest_version, "user_config.yaml")
if not os.path.isfile(yaml_file):
continue
with open(yaml_file, "r") as f:
config = yaml.safe_load(f)
app_name = deep_get(config, "ix_context/app_name") or app
data = {}
for key in KEYS_TO_EXTRACT:
value = deep_get(config, key)
data[key] = value
storage_flat = flatten_storage(config.get("storage", {}))
results[app] = {
"app_name": app_name,
"data": data,
"storage": storage_flat,
}
template = Template(OUTPUT_TEMPLATE)
print(template.render(results=results))
if __name__ == "__main__":
main()
Output looks like:
readarr
run_as/user: 4005
run_as/group: 4005
ix_context/app_metadata/run_as_context/0/uid: 568
ix_context/app_metadata/run_as_context/0/gid: 568
ix_context/app_metadata/run_as_context/1/uid: None
ix_context/app_metadata/run_as_context/1/gid: None
ix_context/app_metadata/run_as_context/2/uid: None
ix_context/app_metadata/run_as_context/2/gid: None
network/peer_port: None
network/web_port: 30045
storage/config/host_path_config/path: /mnt/apps/readarr
resources/limits/cpus: 2
resources/limits/memory: 4096
storage:
config/host_path_config/acl_enable: False
config/host_path_config/path: /mnt/apps/readarr
config/type: host_path
audiobookshelf
run_as/user: 4006
run_as/group: 4006
ix_context/app_metadata/run_as_context/0/uid: 568
ix_context/app_metadata/run_as_context/0/gid: 568
ix_context/app_metadata/run_as_context/1/uid: None
ix_context/app_metadata/run_as_context/1/gid: None
ix_context/app_metadata/run_as_context/2/uid: None
ix_context/app_metadata/run_as_context/2/gid: None
network/peer_port: None
network/web_port: 30067
storage/config/host_path_config/path: /mnt/apps/audiobookshelf/config
resources/limits/cpus: 2
resources/limits/memory: 4096
storage:
additional_storage/0/cifs_config/domain:
additional_storage/0/cifs_config/password: (snip)
additional_storage/0/cifs_config/path: media
additional_storage/0/cifs_config/server: 10.10.94.1
additional_storage/0/cifs_config/username: media-ro
additional_storage/0/mount_path: /mnt/media
additional_storage/0/read_only: True
additional_storage/0/type: cifs
config/host_path_config/acl_enable: False
config/host_path_config/path: /mnt/apps/audiobookshelf/config
config/type: host_path
metadata/host_path_config/acl_enable: False
metadata/host_path_config/path: /mnt/apps/audiobookshelf/metadata
metadata/type: host_path
NOTE: It won’t output app specific settings, like “n8n” has a bunch of additional info. For those, I just open the user_config.yaml for that one service and grab the settings. Otherwise, I’m just going down the list pretty fast now.