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:

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.

One caveat

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:

FileWhat it containsOwned 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.

Conservative by design

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:

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:

  1. Create build-rootfs/files/etc/podroid/migrations/<versionCode>.sh. Use the app's versionCode (an integer, found in app/build.gradle.kts) as the filename.
  2. Make the script idempotent. It may run more than once if the guest crashes mid-migration; it must be safe to re-run.
  3. Register it in build-rootfs.sh so 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.

No script needed for most changes

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.