Applications rarely consist of a single file. A typical deployment might include binaries, configuration files, assets, and supporting scripts that must be deployed together to maintain consistency. While Mender provides several ready-made Update Modules for common scenarios, the need for a custom solution arises when deploying application bundles where files have specific destination paths and require atomic rollback.
This tutorial demonstrates how to create a streaming Update Module that accepts multiple payload files, deploys them according to a manifest, and provides automatic rollback on failure.
Prerequisites
- A device running Mender client v4.x or v5.x (for v3.x, replace
mender-updatewithmenderin commands below) mender-artifacttool installed on your development machinejqinstalled on the target device
Understanding the Approach
Mender’s Update Module interface provides a stream-next mechanism for consuming payload files one at a time. Each call to cat stream-next returns the path to the next file in the artifact, or an empty string when no more files remain. Combined with artifact metadata (passed via -m during artifact creation), this allows the Update Module to know what files to expect and where to place them.
The deployment flow operates in distinct phases:
- Download: Stream all files to a staging area
- ArtifactInstall: Backup existing files, deploy staged files to final locations
- ArtifactCommit: Remove backup (update successful)
- ArtifactRollback: Restore backup if installation fails
The Update Module
Install this script as /usr/share/mender/modules/v3/app-bundle on your target device:
#!/bin/sh
set -ue
STATE="$1"
FILES="$2"
WORK_DIR="/tmp/mender-app-bundle"
STAGING_DIR="${WORK_DIR}/staging"
BACKUP_DIR="${WORK_DIR}/backup"
MANIFEST="${FILES}/header/meta-data"
case "$STATE" in
NeedsArtifactReboot)
echo "No"
;;
SupportsRollback)
echo "Yes"
;;
Download)
rm -rf "$STAGING_DIR"
mkdir -p "$STAGING_DIR"
jq -r '.files[].name' "$MANIFEST" | while read -r name; do
file="$(cat stream-next)"
[ -n "$file" ] || { echo "Stream ended early" >&2; exit 1; }
cat "$file" > "${STAGING_DIR}/${name}"
done
[ -z "$(cat stream-next)" ] || { echo "Unexpected extra files" >&2; exit 1; }
;;
ArtifactInstall)
base_path=$(jq -r '.base_path' "$MANIFEST")
# Backup existing files
rm -rf "$BACKUP_DIR"
mkdir -p "$BACKUP_DIR"
jq -r '.files[].dest' "$MANIFEST" | while read -r dest; do
[ -f "${base_path}/${dest}" ] || continue
mkdir -p "$(dirname "${BACKUP_DIR}/${dest}")"
cp -a "${base_path}/${dest}" "${BACKUP_DIR}/${dest}"
done
cp "$MANIFEST" "${BACKUP_DIR}/manifest.json"
echo "$base_path" > "${BACKUP_DIR}/base_path"
# Deploy new files
mkdir -p "$base_path"
jq -r '.files[] | "\(.name)\t\(.dest)\t\(.mode // "")"' "$MANIFEST" | \
while IFS=' ' read -r name dest mode; do
mkdir -p "$(dirname "${base_path}/${dest}")"
cp "${STAGING_DIR}/${name}" "${base_path}/${dest}"
[ -z "$mode" ] || chmod "$mode" "${base_path}/${dest}"
done
;;
ArtifactCommit)
rm -rf "$BACKUP_DIR"
;;
ArtifactRollback)
[ -d "$BACKUP_DIR" ] || exit 0
base_path=$(cat "${BACKUP_DIR}/base_path")
jq -r '.files[].dest' "${BACKUP_DIR}/manifest.json" | while read -r dest; do
if [ -f "${BACKUP_DIR}/${dest}" ]; then
mkdir -p "$(dirname "${base_path}/${dest}")"
cp -a "${BACKUP_DIR}/${dest}" "${base_path}/${dest}"
else
rm -f "${base_path}/${dest}"
fi
done
;;
Cleanup)
rm -rf "$STAGING_DIR"
;;
esac
exit 0
The critical streaming pattern appears in the Download state. The manifest tells us how many files to expect, and stream-next provides them in order:
file="$(cat stream-next)" # Get path to next payload file
cat "$file" > destination # Stream content to staging
The Manifest Format
The manifest defines where files should be installed. Pass it to mender-artifact using the -m flag, which embeds it in the artifact’s metadata:
{
"base_path": "/opt/myapp",
"files": [
{"name": "myapp", "dest": "myapp", "mode": "0755"},
{"name": "myapp.conf", "dest": "myapp.conf", "mode": "0644"}
]
}
The name field identifies the source file in the bundle directory, while dest specifies the path relative to base_path. The optional mode field sets file permissions after deployment.
File order in the manifest must match the order of -f arguments when creating the artifact, as stream-next delivers files in artifact order.
Creating Artifacts
Structure your application bundle (you can use dummy text files as myapp and myapp.conf for testing):
my-bundle/
├── manifest.json
├── myapp
└── myapp.conf
Generate the artifact with mender-artifact:
mender-artifact write module-image \
-T app-bundle \
-n "myapp-v1.0" \
-t <device-type> \
-o myapp-v1.0.mender \
-m my-bundle/manifest.json \
-f my-bundle/myapp \
-f my-bundle/myapp.conf
Deployment
Install the Update Module to /usr/share/mender/modules/v3/app-bundle on your device. From there, standard Mender deployment applies: upload the artifact to Hosted Mender or your self-hosted server and deploy to your device fleet, or use standalone mode with mender-update install.
Adapting for Your Use Case
The manifest-driven approach allows customization without modifying the Update Module itself - simply change base_path and dest fields in the manifest.
For service management, extend ArtifactInstall to stop services before deployment and restart them afterward:
ArtifactInstall)
systemctl stop myapp || true
# ... deployment code ...
systemctl start myapp
;;
Conclusion
This Update Module demonstrates how Mender’s stream-next mechanism handles multi-file payloads while maintaining rollback guarantees. The manifest-driven design keeps the Update Module generic, with deployment specifics defined at artifact creation time.