Skip to content

Instantly share code, notes, and snippets.

@p0358
Last active April 25, 2025 03:06
Show Gist options
  • Save p0358/b75c1d4be5f9aa95222366d74b26c0a4 to your computer and use it in GitHub Desktop.
Save p0358/b75c1d4be5f9aa95222366d74b26c0a4 to your computer and use it in GitHub Desktop.
How to backup your TrueCharts PVC data before moving on to TrueNAS SCALE 24.10 Electric Eel + Docker

So say that you have been running your TrueNAS SCALE 24.04 or 23.10 or older happily with your TrueCharts apps installed (as happy as you could be with that buggy mess of a project).

You either stuck with 23.10 Cobia, because you didn't want to bother doing yet another TrueCharts troublesome migration where they removed did something with PVC rearrangement and expected you to re-setup all your apps yet again (!), or you upgraded to 24.04 Fangtooth either while ignoring these instructions and apps still kept on working or running the PVC migration script as intended.

You kept your apps config storages to PVC as that was a simple default. (You start to grow a certain regret of not using host path mounts for everything from the beginning, but that's all water in drain now.)

Now you want to backup that data so that you can move to Docker-based apps in upcoming 24.10 Electric Eel release flawlessly and gracefully, and be happy everafter.

This tutorial and a bunch of pointers here is for you then! Read and follow it carefully and (hopefully) you won't get lost.

Steps

Do note that these are just steps I figured out with basic research and trial and error. Some things might not be the best practices and I don't take responsibility for some Kubernetes nerds ending up with a heart attack after reading it. The method works though, so in my book it's valid.

Important

Do these steps with all your apps that you want to back up running (and hopefully keep them idle).

My methods here are simple but rely on the apps actually running and taking advantage of an existing runtime PVC mount, without having to bother with complicated commands to do it manually. Just take advantage of what we have already.

So don't shut off your apps before doing this. Start them up.

All commands assume you're running from root shell (do sudo su - or something if not). I also assume you only ran TrueCharts apps, so I don't focus on official TrueNAS apps, but some steps will apply to them too (apart from data backup, I have no idea how iXvolume works, as I never used these apps).

I also logically assume you're doing these steps right before an upgrade to Electric Eel, since otherwise your backed up apps data would get stale if you put it off.

Replace /mnt/tank/mybackup with the path you're backing up to. Ideally have that path be part of a dataset without ACLs enabled (as is by default), otherwise you may be unable to backup ownership information of files+dirs.

0. In-app backups

Before you start, some apps out there with web admin UI panels have some built-in option of exporting and downloading a backup of database/config. If any of your apps do happen to have such a feature, use it and download such a backup as an extra measure as well.

1. App config screenshots

Firstly save screenshots of all your apps' configs in the UI. Yes, we'll save some extra data about your apps to the JSON files later, but looking at the visual configuration will prove to be easier for the most part, so just do it and thank yourself later. Use the extension FireShot to take the screenshots, as it works and you won't be stitching screenshots together manually like a moron.

These will allow you to view things like what you chose for particular app storage type on a glance, including custom mounts that you'll need to re-add later (this tutorial doesn't cover backing up host mounts, as I assume they're safe on your pool somewhere already, just take note of them).

2. Dump some helpful info to text files

mkdir -p /mnt/tank/mybackup/k3s-info/
k3s kubectl get pvc -A | sort -u | awk '{print $1}' | tail -n +2 | sort -u | xargs -I {} sh -c 'k3s kubectl -n {} describe deploy > /mnt/tank/mybackup/k3s-info/{}.txt'

The above will save some per-app information from Kubernetes runtime. Might be a bit verbose and hard to follow, but this contains your full underlying app config, ports (note: only internal ports here, not exposed ones, so keep the screenshots above handy as well), ENV, mounts. This maybe come useful for you, for example to copy-paste ENV vars to new place afterwards.

Now we'll back up a list of our apps together with their PVC mount names and IDs:

k3s kubectl get pvc -A | sort -u | awk '{print "\t" $1 "\t" $2 "\t" $4}' | column -t | tee /mnt/tank/mybackup/truenas_pvc_names.txt

Now in the file /mnt/tank/mybackup/truenas_pvc_names.txt you should have something like this:

#    NAMESPACE            NAME                                                                            VOLUME
1)   ix-firefox           firefox-config                                                                  pvc-8c0f36f9-13f9-4890-a914-660ee7a39f5d
2)   ix-gitea             db-gitea-postgresql-0                                                           pvc-76bf480b-d305-4d77-b2f5-e19d5b1e8b2f
3)   ix-gitea             gitea-data                                                                      pvc-3387d272-6c3a-4411-872a-8ab16cdbb1ea
4)   ix-jellyfin          jellyfin-config                                                                 pvc-a1e4b06e-adec-4085-8f79-0dc42e8771c3
5)   ix-jellyfin          jellyfin-transcode                                                              pvc-afe8dc5f-ac55-45ee-8b65-c22eebe54a13
6)   ix-librespeed        librespeed-config                                                               pvc-90bd4a63-4231-4bac-a247-03ba3900a13d
7)   ix-paperless-ngx     data-paperless-ngx-redis-0                                                      pvc-e4012924-a839-46f1-b1f4-6c21260a238b
8)   ix-paperless-ngx     paperless-ngx-consume                                                           pvc-750af274-5234-49bf-86b9-60b4d2ae9454
9)   ix-paperless-ngx     paperless-ngx-data                                                              pvc-0848c5e0-f6c7-4922-9262-13c965b7e861

Warning

If you have mount names prefixed with db-, such as db-gitea-postgresql-0, it's likely they're TrueCharts' special thing with their special setup/handling of databases. In other words, you may have some extra trouble with trying to restore/deal with this data. If you see it at this point and care about said app, I'd recommend to pause here and figure out what's the best way to backup that separately: some apps have built-in database export feature, use that if available. Otherwise I recommend trying to figure out how to take an SQL dump from within container shell just to be safe.

Thankfully this doesn't apply to most apps, and I didn't really care about this Gitea instance I had, so I'm happily moving on, yolo.

3. Backup the PVC data

Let's create a tree-like directory structure for all of our mounts. Assuming you have a table like above, you'd do:

mkdir /mnt/tank/mybackup/apps-pvc-data
mkdir -p /mnt/tank/mybackup/apps-pvc-data/ix-firefox/firefox-config
mkdir -p /mnt/tank/mybackup/apps-pvc-data/ix-gitea/{db-gitea-postgresql-0,gitea-data}
mkdir -p /mnt/tank/mybackup/apps-pvc-data/ix-jellyfin/{jellyfin-config,jellyfin-transcode}
mkdir -p /mnt/tank/mybackup/apps-pvc-data/ix-librespeed/librespeed-config
mkdir -p /mnt/tank/mybackup/ix-paperless-ngx/{data-paperless-ngx-redis-0,paperless-ngx-consume,paperless-ngx-data}

Now it's time for some manual labour of copying the PVCs one by one.

Repeat the below to copy every single item from the NAME column into your backup. For example, for the two Jellyfin datasets, we'll run these two commands (since Jellyfin has these two mounts on PVC):

rsync -ra $(find /var/lib/kubelet/pods/ -name 'pvc-a1e4b06e-adec-4085-8f79-0dc42e8771c3')/mount/ /mnt/tank/mybackup/apps-pvc-data/ix-jellyfin/jellyfin-config/
rsync -ra $(find /var/lib/kubelet/pods/ -name 'pvc-afe8dc5f-ac55-45ee-8b65-c22eebe54a13')/mount/ /mnt/tank/mybackup/apps-pvc-data/ix-jellyfin/jellyfin-transcode/

Replace ix-jellyfin, jellyfin-config/jellyfin-transcode, pvc-xxx with your values from the table. Run one command for each row. Double-check you replaced the three things in the command properly with your row from the table. Afterwards I recommend sanity-check of ls -la /mnt/tank/mybackup/apps-pvc-data/ix-jellyfin/jellyfin-config/ to ensure your app's mount data is there as it should.

(in rsync, -r stands for recursive, -a ensures that chmod, owner and links will also be copied over)

4. Do full backup of /var/lib/kubelet/pods/

This is where your mounted PVCs are located, as you could judge from the commands we ran above. We want to back that full thing up for future reference just in case we messed up and omitted something.

tar -acvf /mnt/tank/mybackup/var-lib-kubelet-pods.tar.zst /var/lib/kubelet/pods/

Also don't bother trying to back up ix-applications dataset with tar, by default from filesystem you only have access to the binaries of your containers and that's gonna be useless. The underlying PVCs there are not mounted there and thus not accessible. They're in subdirs of /var/lib/kubelet/pods/ (again: only for currently running apps!!!), which we took care of backing up in the previous step.

5. Final look back

It won't hurt now to be safe and once again review your screenshots from step 1. Look over the "Persistence" section and each item. Ensure you have safely backed up every entry there. The PVC entires should have been backed up to the dirs we created and probably non-empty. The host paths should be safe on your pools. Same with network mounts. Everything should be okay, but this is the last chance to catch any mistakes.

6. ☢️ Nuke

Now comes the scary part. When it's time for upgrade to Electric Eel, we want to nuke the old ix-applications dataset. Otherwise post-upgrade the iX scripts will attempt to do their own upgrades/conversions, which were designed FOR OFFICIAL APPS ONLY. There's a chance things will implode if you were to keep TrueCharts apps on there.

Note

Nuking this should be fiiiine at this point, since we already did two methods of backups above. But if you're paranoid, you can always figure out how to replicate/rename the dataset somewhere else for some time instead of junking it. Just make sure to Unset Pool before doing that (Unset Pool doesn't actually remove it, only makes the apps disappear from the UI and Kubernetes to leave it alone).

Alternatively if you want to rely on built-in scripts to convert your official apps, do remove just TrueCharts apps and keep only the official ones, then you might keep the dataset and cross your fingers.

If you only have TrueCharts apps and wish to proceed with the act of destruction:

  1. Shut down all apps
  2. Remove all apps
  3. Press Configuration -> Unset Pool, confirm
  4. Now you should have a screen saying you have no apps installed.
  5. Time to go back to terminal to nuke ix-applications dataset:

Figure out where's your ix-applications:

zfs list | grep ix-applications | head -n 1

For example: tank/ix-applications (assuming your parent dataset is tank and ix-applications was created as its direct child)

Now do:

zfs unmount -f tank/ix-applications
zfs destroy -f tank/ix-applications

Here we go, the junk was purged. Now we can upgrade.

(you can run the listing command again to ensure ix-applications is actually gone)

Life in Electric Eel ⚡🐍

How we'll re-setup our backed up and new apps.

0. Preparation

Structure

Now I'll share my preferred structure for hosting apps data, which will help you keep things organized in one place, in logical structure, and in a way that's easy to backup one way or another (replication, snapshot, tar, you name it).

TrueNAS now creates a dataset ix-apps in the parent dataset that you choose, let's say in our case this will again be tank. We don't touch their dataset, instead we'll create our own one that will keep our app data in a similar tree structure to our backup that we previously took.

So in my case I'll go into the UI -> Datasets, and I'll create a dataset called apps-data under tank dataset. I'll change the Dataset Preset option to Apps. I now end up with my empty folder at /mnt/tank/apps-data.

Here I create a directory for each of my apps. So let's focus on the Jellyfin example from above.

mkdir -p /mnt/tank/apps-data/jellyfin/{cache,config}
chown -R 568:568 /mnt/tank/apps-data/jellyfin/{cache,config}

Jellyfin just so happens to allow configuring arbitrary user ID and GID, so we also set them to 568.

Now I can copy over my data from the backup:

rsync -ra /mnt/tank/mybackup/apps-pvc-data/ix-jellyfin/jellyfin-config/ /mnt/tank/apps-data/jellyfin/config/

Turns out the available mounts are different. It seems "cache" folder wasn't mounted before, so I don't copy anything to it. I don't create a directory for transcode, as I'll want to keep that in a temporary directory instead, available in the UI configuration as Temporary (Temporary directory created on the disk) (or tmpfs (Temporary directory created on the RAM)).

So you end up with a tree like this:

apps-data/
 \-- jellyfin/
      \-- cache/
      \-- config/
 \-- immich/
      \-- postgres/
      \-- compose.yml
      \-- .env

I hope you get the idea. The compose.yml files is what we'll use for Custom App ran with Docker Compose, and we'll put them in root dir of our app's folder.

Of course you don't have to keep all mounts there! If you add extra custom mounts, or some predefined app mounts, that are huge in size, you're free to point those particular mounts elsewhere, for example having apps-data with your configs and caches on SSD pool, and something like actual media storage for Jellyfin on a HDD pool.

So in case of Jellyfin you have to create a new mount with arbitrary mount path in the container such as /medialibrary, then point it to your media library wherever it is, then add /medialibrary as library scan path in Jellyfin admin panel).

Permissions

Note that TrueCharts allowed you to select user, gid and fs id of the apps. It was often a recommendation to use 568 everywhere for apps user and group. Official apps and Docker images you'll use with Custom App don't have a uniform way of doing that. Some apps ask you to input the UID+GID in ENV vars (which may then be exposed in TrueNAS app config, or possible to add manually), with Custom Apps you'll be able to add user: 568:568 line to the YAML file, but that always won't work properly.

In other words, in some cases your app will end up running on a different user and group than it did before. If it runs on root, then that's not really a problem since root can do anything with files. If it runs under some arbitrary user, such as 1000:0 for ElasticSearch, you may need to chown -R on the mounted paths to the expected owner to let the app run properly. If in doubt, simply refer to the documentation of the app you're trying to run, they'll usually explain the exact expectations they have in this regard.


You now have two paths for figuring out how to run your apps in the new Docker-flavored reality:

1. TrueNAS official apps

Their official catalog is growing and most of the app configurations are sane. They're quickly adding new apps and fixing reported issues with existing ones. It's not a bad option at all for simpler/default setups at all. Do note that if you had advanced configs such as some crazy VPNs and routing and Traefik and whatnot, then maybe not.

Also note that there's no super-obvious ways of backing up your primary app configs with this method, as you set it up in the UI, so maybe write down your custom settings somewhere or take screenshots of config panel from time to time.

But also remember that network-wise you can turn on "host mode" and then just directly instruct your app to bind to some IP or interface right on your host, without all the abstractions.

Otherwise just set your app up, fill in the details correctly. Reference your screenshots or .txt files to figure out what ports and ENV vars you used. Add the same extra mounts you had. Point your correct app storage paths to the dirs you've set up in step 0 (don't use iXvolume, don't repeat the PVC fiasco again, feel free to only use iXvolume or temp dir for ephemeral temporary data that can be junked at any time (but also please double-check that your app's hypothetical /cache directory really can be junked at any time with no consequences, I know instances of apps that keep rather important data there counter-intuitively; if in doubts, err on the safe side)).

Tip

Note that apps in the "stable" train (great minority of them) run on user 473:473. Apps in "enterprise" or "community" trains run on 568:568 aka the apps:apps user, just as TrueCharts app did. You may need to adjust permissions for datasets of apps like Minio.

Now if you didn't mess up mimicking the configuration, finish up creating the app and start it up, and that's it, it should work. The most common issues you'll run into are permission issues, you'll need to take a look at them individually every time.

2. Docker Compose apps

TrueNAS allows you to create a custom Docker Compose app now pretty easily. All you have to do is go to: Apps -> Discover Apps -> Custom App. You then enter a name and paste contents of Compose file.

Unfortunately out-of-the-box, at the moment of writing this article, the custom apps have a few problems:

  • cannot reference complimentary files with relative paths, project work-dir is set arbitrarily for you
  • cannot provide .env file with environmental variables to use
  • in current RC1 a bug: cannot edit app's Compose file after it was already created (this is fixed in stable .0 branch already)
  • cannot easily backup the config file as it's in a different place from your apps dataset
  • cannot easily reference a Dockerfile to build with relative location

But there's a neat trick around all these limitations, courtery of @sfatula that was described here: https://forums.truenas.com/t/electric-eel-how-i-am-using-dockerfile-env-files-compose-files/15252

In short, create your compose.yml file under your apps-data, for example at /mnt/tank/apps-data/immich/compose.yml. Yes, nowadays the new convention is to name these files just compose.yml instead of docker-compose.yml, but that doesn't really matter for us.

Now create a new custom app, enter its name (such as immich), and in the compose contents put:

include:
  - /mnt/tank/apps-data/immich/compose.yml

And that's it! You can use your linked compose.yml file like you normally would (for contents, refer to documentation of the app you want to install obviously). If you update it, simply hit Edit and Save under the app to recreate it (in bugged RC1 you remove the app and re-create it with the same snippet).

For .env files, you may need to use {env.PORKBUN_API_KEY} expansion instead of ${PORKBUN_API_KEY} (TODO: that claim remains to be tested). You may also need to specify env_file: .env explicitly under your containers to let them see it too.

If you need the app to run on a different user and it doesn't have environment variables to control how it runs, you may try adding user: 568:568 directive to the compose file under the containers, but it's not always guaranteed to work!

Note

Just rememer to not run docker compose inside of your dir where you put the compose.yml file. The workdir of your Custom App is different, so messing with docker compose directly in that dir will show you nothing running or spawn duplicates and then spew errors about duplicated ports etc.

You have to remember to not habitually do that if you're coming from a system where you managed the Docker Compose stacks directly from the CLI. For how to use docker compose now, read on below.

Bonus tip

Now I'll sell you my own tip, courtesy of me.

If you want to use the docker compose command to aid you in some tasks:

  1. First check list of projects with docker compose ls. It will print out a table of NAME, STATUS, CONFIG FILES. The names are in format of ix-<HowYouNamedYourAppInUI>
  2. Now run a command like docker compose -p ix-minio ps

Tada 🎉. Very neat. You can use it for some things if you can't be bothered to do them in the UI or factually can't do them there, and it's cleaner than messing up with the containers individually. As for why you can't instead enter the workdir of Compose project like usually with stacks or use the -f option to specify the runtime path of Compose file: don't ask me.

Closing words

I wrote these steps down around a week after a successful migration done in more-less this way, on 24.10 RC-1. Feel free to share any feedback in comments. I found my fair share of useful tips from people in this community, so now it's my time to give back.

@lied
Copy link

lied commented Mar 11, 2025

I wish I had found this guide earlier. Would have saved me a lot of trouble :-) Anyway thanks, the tip with using docker compose ls is perfect. I would be interested if the generated compose.yml (which is json??) could be converted and then reused, so I would not have to reinstall apps, just because I want to switch the volume from ix-volume to host path...

@p0358
Copy link
Author

p0358 commented Mar 11, 2025

@lied I have to admit the contents of those files are pretty interesting, it looks like they're only a textual copy-paste of your contents only if you created a custom app (where it asks to paste Compose file contents), meanwhile the apps from the UI use that JSON-ish format. You can copy-paste the JSON itself to something like https://it-tools.tech/json-to-yaml-converter to get the YAML back. It seems the YAML is just various snippets from https://github.com/truenas/apps slapped together into final production file.

In any case though, if you want to migrate from ix-volume to host path, it might be the easiest to use standard Docker command line tools to figure out where your volume files are actually residing, copy them over to your intended host path, and then just edit your app in the UI, removing/changing the ix-volume option on the same path to Host Path mount, I don't see a reason why that wouldn't work as long as the permissions in the new path aren't wrong.


Btw later I found out that for some commands, docker compose indeed wants -f with specified compose file instead of just -p for project name (either that or cd-ing to where it is): mostly in cases where it operates on said file for example for pulling images, rather than operating on an already running project...

I'll also have to experiment whether with x-notes and x-portals I could add these things to the UI with custom Compose files, like Jellyfin that has this in the root-level of its generated Compose file:

x-notes: >+
  # Jellyfin


  ## Bug Reports and Feature Requests


  If you find a bug in this app or have an idea for a new feature, please file an issue at

  https://github.com/truenas/apps

x-portals:
  - host: 0.0.0.0
    name: Web UI
    path: /
    port: 8096
    scheme: http

@lied
Copy link

lied commented Mar 11, 2025

@p0358 Switching the volumes isn't that straight forward unfortunately. In my paperless-ngx app I can't switch the volumes in the UI once it's setup with ix-Volume it can't be changed... This is why I played around with the docker compose ls , json parsing yesterday to edit the mounts manually. This wasn't working. Copying the data is the easy part :-), i have plenty of experience with that after my "migration" from TrueCharts.

For the parsing of the json to yaml there is an even easier way:

docker compose -f /mnt/.ix-apps/app_configs/compreface/versions/1.1.12/templates/rendered/docker-compose.yaml convert --format yaml
> output.yaml

But this isn't working for all apps, some (e.g. paperless-ngx) have some templating parts in this 'rendered' compose file, which is either a bug of the app or the 'true' compose file is stored some place else.

If you find out how to get to the TrueNAS UI compose file I would be very interested, cause then I could easily just migrate my apps to the - include: compose.yml way you suggested.

@p0358
Copy link
Author

p0358 commented Mar 11, 2025

@lied You could also build up a new Compose file based on the templates on their apps repo, but those are actually somewhat annoying to read, so pulling out the readable JSON part out of the auto-generated yaml file might be the best bet.

I think starting off with that existing auto-generated file should be the best bet (if your command for conversion works to begin with). You will probably see in it some Python code for setting up permissions and also "permissions" container in the "services" section. Perhaps some of that could be gotten rid of, I don't use anything like that in my custom Compose apps, but while YMMV per-app, it might all be possible to be just removed and the whole file simplified

@cjshrader
Copy link

Thank you so much for all of this. I'm not very good with this kind of stuff, I've only ever had a surface level knowledge. Using your guide I was able to move off of my truechart apps but keep my settings. I've got everything running natively or in Docker now, I really appreciate it.

@mgillman
Copy link

Thank you.
This was extremely helpful! I had put off upgrading to Electric Eel for too long... but then I bumped into your perfectly clear guide.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment