Does anyone who use the OpenWrt container have a script they can share for upgrading OpenWrt that creates a new container, copies over the config/user packages that works well with Incus?
have you tried doing OpenWRT built-in backup and restoring it on a new container?
You could try using a custom volume thats mounted to /etc/config/users to hold the relevant config independently of the root disk and then just use incus rebuild to upgrade to a newer OpenWrt container image. That way you might not even need a script that interacts with OpenWrt guest instance at all.
Create and attach custom Volume:
incus storage volume create default openwrt-config-vol --type=filesystem size=50MiB
incus storage volume attach default custom/openwrt-config-vol openwrt-instance /etc/config/users
Rebuild instance :
incus rebuild images:openwrt/24.10 local:openwrt-instance
Haven’t tested this myself but would expect it to work unless OpenWrt changes it’s config file layout drastically between releases.
I’ve got a few scripts that I use for this process.
To create a backup and restore it:
create_backup.sh
#!/bin/bash
set -eux
NAME=“router1”
REMOTE=“server1”
incus exec $REMOTE:$NAME – sysupgrade -b /tmp/config_backup.tar.gz
incus file pull $REMOTE:$NAME/tmp/config_backup.tar.gz ./
restore_backup.sh
#!/bin/bash
set -eux
NAME=“router1”
REMOTE=“server1”
incus file push ./config_backup.tar.gz $REMOTE:$NAME/tmp/config_backup.tar.gz --uid 0 --gid 0 --mode 644
incus exec $REMOTE:$NAME -- sysupgrade -r /tmp/config_backup.tar.gz
incus restart $REMOTE:$NAME
For packages, I keep a list of what is installed and located in a file on the OpenWRT instance:
/etc/freehand/my_packages
nano
htop
openssh-sftp-server
iperf3
tcpdump
bind-dig
luci-proto-wireguard
luci-app-acme
acme-acmesh-dnsapi
That file gets automatically backed up in the above tar.gz file because I added a line in /etc/sysupgrade.conf:
/etc/freehand/
To reinstall my selection of packages (and upgrade present packages), I have:
install_packages.sh
#!/bin/bash
set -eux
NAME=“router1”
REMOTE=“server1”
# Wait for an ipv6 link to be up
while ! incus exec $REMOTE:$NAME -- ping -6 -c 1 2606:4700:4700::1111; do
sleep 1
done
# Push in a DNS server if the flag --dns is provided
# Also stop dnsmasq from updating and replacing the resolv.conf file (via mechanisms like DHCP).
if [[ "${1:-}" == "--dns" ]]; then
incus exec $REMOTE:$NAME -- /etc/init.d/dnsmasq stop
sleep 1
incus exec $REMOTE:$NAME -- ash -c 'echo "nameserver 2606:4700:4700::1111" > /etc/resolv.conf'
fi
# Wait for ipv6 gateway and DNS to be working
while ! incus exec $REMOTE:$NAME -- ping -6 -c 1 downloads.openwrt.org; do
sleep 1
done
# Progress with installing packages
incus exec $REMOTE:$NAME -- opkg update
incus exec $REMOTE:$NAME -- ash -c 'opkg list-upgradable | cut -f 1 -d " " | xargs opkg upgrade'
incus exec $REMOTE:$NAME -- ash -c 'grep -v "^\(#\|$\)" /etc/freehand/my_packages | xargs opkg install'
sleep 1
incus restart $REMOTE:$NAME
Using these scripts, my general workflow is
incus image refresh./create_backup.shincus stop&&incus deleteincus create&&incus start./restore_backup.sh./install_packages.sh --dns
Thanks I’ll give this a shot. You can use sysupgrade -k to save the list of packages:
-k include in backup a list of current installed packages at
/etc/backup/installed_packages.txt
Unfortunately rebuild won’t work if there are snapshots
I’ve found that -k flag to include a bit more than I would like.
I also treat my routers as ephemeral containers and like being able to blow them back to a defined state, which is why I’ve opted for the custom/manual package tracking.
FYI I ended up writing a script to upgrade openwrt by creating a new instance and automatically doing a backup/restore.
It performs the following general steps:
- Launch new “openwrt-vnext” instance
- Creates a backup via
sysupgradeof the current instance - Re-installs missing packages and config from backup on the new instance (see
internal_restore.sh). Existing packages aren’t upgraded. - Stops both instances
- Move the
openwrtprofile to the new instance - Renames the new instance to have the version name (e.g.
openwrt-24-10-5) - Starts new instance and performs a health check inside the new instance (see
internal_healthcheck.shas not everything may be applicable to your setup). It’ll rollback to the previous instance if there’s an error.
This assumes you have an openwrt and a bridge-network profile (provides LAN). The new instance is created without any special config so it relies on these profiles. Change as necessary:
> incus profile show openwrt
config:
boot.autorestart: "true"
boot.autostart.delay: "30"
boot.autostart.priority: "10"
boot.stop.priority: "0"
limits.memory: 4GiB
linux.kernel_modules: wireguard
security.protection.delete: "true"
description: ""
devices:
eth1:
name: eth1
nictype: physical
parent: enp6s0
type: nic
name: openwrt
> incus profile show bridge-network
config: {}
description: Adds a bridged nic so instance gets IP from lan
devices:
eth0:
nictype: bridged
parent: bridge0
type: nic
name: bridge-network
upgrade.sh
#! /bin/bash
# Usage: upgrade.sh <current instance name> <new openwrt version>
# Example: upgrade.sh openwrt-24-10-1 24.10
set -e -o pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "Starting OpenWrt upgrade process..."
if [ "$(id -u)" -ne 0 ]; then
echo -e "\e[31mThis script must be run by root\e[0m" >&2
exit 1
fi
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <current_instance_name> <new_openwrt_version>"
echo "Example: $0 openwrt-24-10-1 24.10"
exit 1
fi
CURRENT_INSTANCE="$1"
OPENWRT_VERSION="$2"
# Verify current instance exists
if ! incus info "$CURRENT_INSTANCE" &> /dev/null; then
echo -e "\e[31mContainer '$CURRENT_INSTANCE' does not exist. Exiting.\e[0m"
exit 1
fi
# Verify current instance has openwrt profile attached
if ! incus config get "$CURRENT_INSTANCE" -p profiles | grep -qw "openwrt"; then
echo -e "\e[31mContainer '$CURRENT_INSTANCE' does not have the 'openwrt' profile attached so it is not the correct active instance.\e[0m"
exit 1
fi
# Delete "openwrt-vnext" container if it exists
if incus info "openwrt-vnext" &> /dev/null; then
echo -e "\e[33mContainer 'openwrt-vnext' already exists. Deleting container!\e[0m"
echo -e "\e[33mYou have 5 seconds to cancel (CTRL+C) if you do not wish to proceed...\e[0m"
sleep 5
incus delete "openwrt-vnext" --force
fi
# Launch new openwrt-vnext container
incus launch "images:openwrt/${OPENWRT_VERSION}" openwrt-vnext -p default -p bridge-network
# Get new instance OpenWrt version
NEW_VERSION=$(incus exec "openwrt-vnext" -- /bin/sh -c "source /etc/openwrt_release && echo \$DISTRIB_RELEASE")
NEW_INSTANCE="openwrt-${NEW_VERSION//./-}"
if [ -z "$NEW_VERSION" ]; then
echo -e "\e[31mFailed to determine OpenWrt version of new instance. Deleting 'openwrt-vnext' container and exiting.\e[0m"
incus stop "openwrt-vnext" --timeout 10 --force
incus delete "openwrt-vnext"
exit 1
fi
echo "New instance is running OpenWrt version: $NEW_VERSION."
echo "New container will be named '$NEW_INSTANCE'."
# Fail if the new instance container already exists
if incus info "$NEW_INSTANCE" &> /dev/null; then
echo -e "\e[31mContainer '$NEW_INSTANCE' already exists. Deleting 'openwrt-vnext' container and exiting.\e[0m"
incus delete "openwrt-vnext" --force
exit 1
fi
# Snapshot current openwrt instance before proceeding
echo "Creating snapshot of current '$CURRENT_INSTANCE' instance..."
incus snapshot create "$CURRENT_INSTANCE" --expiry 14d
# Create openwrt backup, transfer to new instance, and delete from host
echo "Creating backup on current '$CURRENT_INSTANCE' instance..."
backup_tmp=$(mktemp -u)
incus exec "$CURRENT_INSTANCE" -- /bin/sh -c "sysupgrade -k -b /tmp/backup.tar.gz"
echo "Pulling backup from current instance to $backup_tmp..."
incus file pull "$CURRENT_INSTANCE/tmp/backup.tar.gz" "$backup_tmp"
echo "Copying backup from $backup_tmp to new instance..."
incus file push "$backup_tmp" "openwrt-vnext/tmp/backup.tar.gz"
rm "$backup_tmp"
# Restore backup inside new instance
echo "Copying restore script to new instance..."
incus file push "${SCRIPT_DIR}/internal_restore.sh" "openwrt-vnext/tmp/openwrt_restore.sh"
echo "Running restore script inside new instance..."
echo ""
# incus exec prints out the output, format it so its clear its from the new instance
incus exec "openwrt-vnext" -- /bin/sh -c "chmod +x /tmp/openwrt_restore.sh && /tmp/openwrt_restore.sh" | sed -e 's/^/[openwrt-vnext] /'
echo ""
incus stop "openwrt-vnext" --timeout 60
echo -e "\e[32mRestore completed inside new instance. New instance is stopped.\e[0m"
echo -e "Migrating from '${CURRENT_INSTANCE}' to '${NEW_INSTANCE}'"
echo -e "\e[33mCAUTION: Starting to make irreversible changes. If any step fails from here, manual intervention may be required!\e[0m"
echo -e "\e[33mAre you sure you want to continue? (y/n)\e[0m"
read -r CONFIRM
if [ "$CONFIRM" != "y" ] && [ "$CONFIRM" != "Y" ]; then
echo "Upgrade cancelled by user."
exit 1
fi
echo "Renaming new instance to '$NEW_INSTANCE'..."
incus rename "openwrt-vnext" "$NEW_INSTANCE"
echo -e "\e[32mRenamed new instance to '$NEW_INSTANCE'.\e[0m"
echo "Stopping current instance '$CURRENT_INSTANCE'..."
incus stop "$CURRENT_INSTANCE" --timeout 60
echo "Removing 'openwrt' profile from current instance '$CURRENT_INSTANCE'..."
incus profile remove "$CURRENT_INSTANCE" "openwrt"
echo -e "\e[32mCurrent instance '$CURRENT_INSTANCE' stopped and 'openwrt' profile removed.\e[0m"
incus profile add "$NEW_INSTANCE" "openwrt"
echo -e "\e[32m'openwrt' profile added to new instance '$NEW_INSTANCE'.\e[0m"
echo "Starting new 'openwrt' instance..."
incus start "$NEW_INSTANCE"
sleep 5
# Run healthcheck on new instance, if it fails, rollback to old instance
echo "Running final healthcheck on new instance..."
incus file push "${SCRIPT_DIR}/internal_healthcheck.sh" "$NEW_INSTANCE/tmp/restore_healthcheck.sh"
if ! incus exec "$NEW_INSTANCE" -- /bin/sh -c "chmod +x /tmp/restore_healthcheck.sh && /tmp/restore_healthcheck.sh" 2>&1 | sed -e "s/^/[$NEW_INSTANCE] /"; then
echo -e "\e[31mHealthcheck failed! Rolling back to old instance.\e[0m"
# Stop and remove new instance
incus stop "$NEW_INSTANCE" --timeout 10 --force
incus profile remove "$NEW_INSTANCE" "openwrt"
# Restore old instance
incus profile add "$CURRENT_INSTANCE" "openwrt"
incus start "$CURRENT_INSTANCE"
# Rename new failed instance for inspection
incus rename "$NEW_INSTANCE" "$NEW_INSTANCE-failed"
echo -e "\e[31mRollback completed. Instance '$CURRENT_INSTANCE' is running again. New instance '$NEW_INSTANCE-failed' remains stopped for inspection.\e[0m"
echo -e "\e[31mNew failed instance '$NEW_INSTANCE-failed' will be deleted in 5 seconds unless you cancel (CTRL+C)...\e[0m"
sleep 5
incus delete "$NEW_INSTANCE-failed"
exit 1
fi
# Take initial snapshot of new instance
echo "Creating snapshot of new instance '$NEW_INSTANCE'..."
incus snapshot create "$NEW_INSTANCE"
echo -e "\e[32mUpgrade to OpenWRT ${OPENWRT_VERSION} completed successfully. New instance '$NEW_INSTANCE' is running.\e[0m"
exit 0
internal_restore.sh
#!/bin/sh
# --- Configuration ---
# Add packages here, separated by spaces, to prevent them from being re-installed
# Example: SKIP_PACKAGES="luci luci-ssl"
SKIP_PACKAGES="luci-app-argon-config luci-theme-argon"
# --- End Configuration ---
# Exit immediately if any command fails (-e)
# Exit if any command in a pipeline fails (-o pipefail)
set -e -o pipefail
echo "Waiting for network connectivity (up to 120 seconds)..."
START_TIME=$(date +%s)
END_TIME=$((START_TIME + 120))
CONNECTED=0
while [ $(date +%s) -lt $END_TIME ]; do
sleep 3
if ! ping -c 1 -W 3 8.8.8.8 >/dev/null 2>&1; then
echo -e "\e[33mPing failed, retrying...\e[0m"
continue
fi
if ! wget -s -q -T 3 https://downloads.openwrt.org 2>/dev/null; then
echo -e "\e[33mHTTP connectivity check failed, retrying...\e[0m"
continue
fi
echo -e "\e[32mNetwork connectivity established.\e[0m"
CONNECTED=1
break
done
if [ $CONNECTED -eq 0 ]; then
echo "Failed to get network connectivity after 120 seconds." >&2
exit 1
fi
echo "Updating package lists..."
# Disable signature checking (this shouldnt be needed but openwrt servers seem to have issues)
sed -i 's/option check_signature/# option check_signature/' /etc/opkg.conf
opkg update
echo "Installing packages from backup..."
PACKAGE_LIST=$(tar -Ozxf /tmp/backup.tar.gz etc/backup/installed_packages.txt | awk 'gsub(/unknown/, ""){print $1}')
# Skip packages in SKIP_PACKAGES
if [ -n "$SKIP_PACKAGES" ]; then
for pkg in $SKIP_PACKAGES; do
PACKAGE_LIST=$(echo "$PACKAGE_LIST" | grep -Fvx "$pkg")
done
fi
# Skip packages that are already installed
INSTALLED_PACKAGES=$(opkg list-installed | awk '{print $1}')
SKIPPED_INSTALLED=""
for pkg in $INSTALLED_PACKAGES; do
if echo "$PACKAGE_LIST" | grep -Fxq "$pkg"; then
SKIPPED_INSTALLED="$SKIPPED_INSTALLED $pkg"
fi
PACKAGE_LIST=$(echo "$PACKAGE_LIST" | grep -Fvx "$pkg")
done
echo "The following packages are already installed and will be skipped"
echo $SKIPPED_INSTALLED
# Install remaining packages
if [ -n "$PACKAGE_LIST" ]; then
echo "The following packages are in the backup and not currently installed:"
echo $PACKAGE_LIST
echo "Installing missing packages..."
opkg install $PACKAGE_LIST
echo -e "\e[32mPackage installation complete.\e[0m"
else
echo "No new packages to install."
fi
# Install Argon theme
echo "Installing Argon theme..."
wget -q --no-check-certificate -O /root/luci-theme-argon.ipk https://github.com/jerrykuku/luci-theme-argon/releases/download/v2.3.2/luci-theme-argon_2.3.2-r20250207_all.ipk
wget -q --no-check-certificate -O /root/luci-app-argon-config.ipk https://github.com/jerrykuku/luci-app-argon-config/releases/download/v0.9/luci-app-argon-config_0.9_all.ipk
opkg install /root/luci-theme-argon.ipk
opkg install /root/luci-app-argon-config.ipk
echo "Restoring configuration..."
sysupgrade -r /tmp/backup.tar.gz
echo "Completed restoring from backup."
exit 0
internal_healthcheck.sh
#!/bin/sh
# Exit immediately if any command fails (-e)
# Exit if any command in a pipeline fails (-o pipefail)
set -e -o pipefail
# List of critical services to check
SERVICES_CHECK="firewall dnsmasq crowdsec-firewall-bouncer"
# Check network connectivity
RETRY_SECONDS=180
START_TIME=$(date +%s)
END_TIME=$((START_TIME + RETRY_SECONDS))
echo "Performing health check. Waiting for network connectivity (up to $RETRY_SECONDS seconds)..."
# Get interfaces
LAN_INTERFACE=$(uci get network.lan.device)
if [ -z "$LAN_INTERFACE" ]; then
echo "LAN interface is not set."
exit 1
fi
WAN_INTERFACE=$(uci get network.wan.device)
if [ -z "$WAN_INTERFACE" ]; then
echo -e "\e[33mWAN interface is not set.\e[0m"
continue
fi
LAN_CHECKED=0
WAN_CHECKED=0
PING_CHECKED=0
DNS_CHECKED=0
HTTP_CHECKED=0
CONNECTED=0
while [ $(date +%s) -lt $END_TIME ]; do
sleep 2
# Check LAN
if [ $LAN_CHECKED -eq 0 ]; then
STATE=$(cat /sys/class/net/${LAN_INTERFACE}/operstate)
if [ "$STATE" != "up" ]; then
echo "'${LAN_INTERFACE}' interface is not up."
exit 1
fi
IP_ADDR=$(uci get network.lan.ipaddr)
if [ "$IP_ADDR" != "192.168.0.1" ]; then
echo "LAN interface ${LAN_INTERFACE} has unexpected IP address '${IP_ADDR}'."
exit 1
fi
echo -e "\e[32mLAN interface '${LAN_INTERFACE}' is up.\e[0m"
LAN_CHECKED=1
fi
# Check WAN
if [ $WAN_CHECKED -eq 0 ]; then
STATE=$(cat /sys/class/net/${WAN_INTERFACE}/operstate)
if [ "$STATE" != "up" ]; then
echo -e "\e[33m'${WAN_INTERFACE}' is not up. Current state is ${STATE}.\e[0m"
continue
fi
if ! ip route | grep -q "^default"; then
echo -e "\e[33mNo default route found.\e[0m"
continue
fi
echo -e "\e[32mWAN interface '${WAN_INTERFACE}' is up.\e[0m"
WAN_CHECKED=1
fi
# Check ping
if [ $PING_CHECKED -eq 0 ]; then
if ! ping -c 1 -W 1 8.8.8.8 >/dev/null 2>&1; then
echo -e "\e[33mPing failed, retrying...\e[0m"
continue
fi
PING_CHECKED=1
echo -e "\e[32mPing succeeded.\e[0m"
fi
# Check DNS resolution
if [ $DNS_CHECKED -eq 0 ]; then
if ! nslookup google.com 127.0.0.1 >/dev/null 2>&1; then
echo -e "\e[33mDNS resolution failed, retrying...\e[0m"
continue
fi
DNS_CHECKED=1
echo -e "\e[32mDNS resolution succeeded.\e[0m"
fi
# Check HTTP connectivity
if [ $HTTP_CHECKED -eq 0 ]; then
if ! wget -s -q -T 3 https://downloads.openwrt.org 2>/dev/null; then
echo -e "\e[33mHTTP connectivity check failed, retrying...\e[0m"
continue
fi
HTTP_CHECKED=1
echo -e "\e[32mHTTP connectivity check succeeded.\e[0m"
fi
CONNECTED=1
break
done
if [ $CONNECTED -eq 0 ]; then
echo -e "\e[31mFailed to get network connectivity within $RETRY_SECONDS seconds. Logs:\e[0m" >&2
logread | grep -i "critical\|emergency\|panic\|warning"
exit 1
fi
# Also verify opkg can update package lists to double check connectivity
opkg update --verbosity=0
echo -e "\e[32mPackage manager connectivity verified.\e[0m"
# Check critical services
for SERVICE in $SERVICES_CHECK; do
if ! service "$SERVICE" status >/dev/null 2>&1; then
echo -e "\e[31mService '$SERVICE' is not running.\e[0m"
exit 1
fi
if ! service "$SERVICE" enabled >/dev/null 2>&1; then
echo -e "\e[31mService '$SERVICE' is not enabled to start on boot.\e[0m"
exit 1
fi
echo -e "\e[32mService '$SERVICE' is running.\e[0m"
done
# Check system log for critical errors in last 50 lines
logread | grep -i "critical\|emergency\|panic\|warning\|warn" | grep -v -E "affinity is now unmanaged|Cannot change IRQ"
echo -e "\e[33mIssues found in system log. The upgrade will continue.\e[0m"
echo -e "\e[32mHealth check passed.\e[0m"
exit 0