Updates & migrations
When you update Podroid, the guest system layer refreshes automatically on the next boot. Your installed packages, edited configs, running services, containers, and home directory are all preserved - no VM reset, no reinstall.
The model: squashfs lower + persistent upper
The guest filesystem is a two-layer overlayfs:
- The read-only lower is the Alpine squashfs shipped inside the APK. It is re-extracted into app storage on every app update, so a fresh version of the system layer is always available after an install.
- The persistent upper is an ext4 image on
/dev/vda(thestorage.imgfile in app storage). Everything you write inside the VM lives here:apk addresults, config edits,rc-updatechanges, containers,/root.
Plain overlayfs (without metacopy, index, or redirect_dir) does not bind the upper to a specific lower. Swapping in a new squashfs and rebooting is enough to pick up updated system files, while the upper layer remains untouched. This is the entire mechanism - no migration tooling is needed for ordinary file updates.
If you changed a file that also changed in the new system layer, and your change was metadata-only (permissions, ownership, timestamps) rather than content, the upper's metadata entry shadows the new lower file. This is rare and easy to resolve: delete the upper's copy with rm and the new system version appears immediately. Content changes always win because the upper copy takes precedence over the lower.
The version contract
Two version markers track where the guest stands relative to the app:
| File | What it contains | Owned by |
|---|---|---|
/etc/podroid/system-version |
The versionCode baked into the squashfs at build time |
Read-only squashfs (updates with every app release) |
/mnt/persist/.podroid/applied-version |
The last versionCode that migration scripts have been run for |
Persistent upper (written by podroid-migrate) |
On each boot the podroid-migrate OpenRC service compares the two values. If applied-version is behind system-version, it runs all pending migration scripts in order and advances the marker.
One-time legacy normalization
Earlier releases mounted the overlay with metacopy=on/index=on/redirect_dir=on. These options record inode references from the lower in the upper, so the upper becomes tied to that specific lower image. Replacing the lower without clearing that state corrupts the overlay.
On the first boot after updating to a version that uses the plain overlay, init-podroid runs podroid-overlay-normalize before stacking the overlay. This tool strips the metacopy and redirect xattrs from any upper entries that carry them, making the upper safe to use with the new lower. It runs once - a marker file at /mnt/persist/.podroid/normalized prevents it from running again. On a fresh install there is nothing to normalize and the tool exits immediately.
The normalizer only removes xattrs (trusted.overlay.metacopy, trusted.overlay.redirect, trusted.overlay.impure) that it placed there; it does not touch file content. If something goes wrong, the worst outcome is a file that reads from the lower instead of the upper - the upper content is never deleted.
What is preserved across an update
Everything on the persistent upper survives. Concretely:
- Packages installed with
apk add - Config files you edited under
/etc - Services added with
rc-update add - Containers, images, and volumes managed by Podman, Docker, or LXC
/rootand any other home directories- Any files you created or modified anywhere in the guest filesystem
What changes are the system files that Podroid ships in the squashfs - OpenRC service scripts, binaries like podroid-hostd, kernel modules, base configs - but only where the upper does not already shadow them.
Adding a migration script
Pure file additions and content changes ship automatically with the squashfs - no script needed. A script is only required when a structural change must happen at runtime, such as enabling a newly-added service, creating a device node, or writing a config that depends on the running system state.
To ship a migration for a new release:
- Create
build-rootfs/files/etc/podroid/migrations/<versionCode>.sh. Use the app'sversionCode(an integer, found inapp/build.gradle.kts) as the filename. - Make the script idempotent. It may run more than once if the guest crashes mid-migration; it must be safe to re-run.
- Register it in
build-rootfs.shso it is copied into the squashfs.
Example - enabling a newly-added service:
#!/bin/sh
# migrations/26.sh - enable my-service added in versionCode 26
# idempotent: rc-update -u is a no-op if the link already exists
rc-update -u add my-service default
podroid-migrate runs scripts in numeric order, so a script for versionCode 26 runs before one for 27. After all pending scripts complete, applied-version is updated atomically. A crash before that point means the scripts run again on the next boot - which is why idempotency matters.
If you add a new binary, replace an OpenRC service script, or edit a base config, just update the file in build-rootfs/files/ and rebuild the squashfs. The overlay union surfaces the new lower file automatically on the next boot, as long as the user has not already written a conflicting upper copy at that path.
Podroid is free software (GPL). Docs for v1.2.5. Found something inaccurate? Open an issue.