In this section, you’ll build the topo-imx93-npu-deployment Topo Template starting from two non-Topo, non-Compose projects:
RPMsgYou’ll combine those sources into one repository, then make the repository a normal Compose project. Then, you’ll add the Topo metadata and Remoteproc Runtime services.
Start by copying the original base projects from the Topo Template.
Clone the Topo Template Format repository for the validation schema, clone the original Topo Template for the source files, and start a new empty repository:
git clone https://github.com/arm/topo-template-format.git
git clone https://github.com/Arm-Examples/topo-imx93-npu-deployment.git
mkdir new-topo-npu-template
cd new-topo-npu-template
Create the project layout:
mkdir -p webapp executorch-runner licenses
Copy the relevant webapp files:
cp -R ../topo-imx93-npu-deployment/webapp/src webapp
Copy the Cortex-M33 runner build inputs from the firmware project:
cp ../topo-imx93-npu-deployment/executorch-runner/build-runner.sh executorch-runner/build-runner.sh
cp ../topo-imx93-npu-deployment/executorch-runner/export_mv2_imx93.py executorch-runner/export_mv2_imx93.py
cp ../topo-imx93-npu-deployment/executorch-runner/docker-entrypoint.sh executorch-runner/docker-entrypoint.sh
cp -R ../topo-imx93-npu-deployment/executorch-runner/patches executorch-runner
Add the licenses and ignore rules used by the source projects:
cp ../topo-imx93-npu-deployment/LICENSE.md .
cp -R ../topo-imx93-npu-deployment/licenses .
cp ../topo-imx93-npu-deployment/.gitignore .
You have two sets of source code combined into one repository. It’s not a Compose project and it’s not a Topo Template.
You’ll now create a Compose project and Topo Template around the source code.
The Compose project provides the container build and runtime structure. A Dockerfile describes how to build one image. A Compose file describes the services that use those images, their build contexts, ports, volumes, dependencies, and runtime settings. In this Template:
webapp/Dockerfile builds the Flask image.webapp/compose.yaml keeps the web app’s build context and Linux runtime settings close to the web app source.executorch-runner/Dockerfile builds the ExecuTorch .pte model and Cortex-M33 runner ELF through multi-stage Docker builds.compose.yaml is the Topo Template entry point. It combines the web app, artifact build services, the Remoteproc Runtime service, and the root-level x-topo metadata.For a general introduction to Compose projects, services, and the compose.yaml file, see Docker’s
How Compose works
documentation.
When you creating a file for any of the following steps, paste the complete file contents as shown.
When you update part of an existing Compose file for a step, merge the YAML into the existing top-level key shown by the snippet. For example, if a snippet starts with services:, add the named service under the existing top-level services: map. Don’t create a second services: block in the same file.
Before adding Topo metadata, make the project work as ordinary Compose. Start by containerizing the Cortex-A web application.
Create webapp/Dockerfile with the following complete contents:
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONUNBUFFERED=1
RUN python -m pip install --no-cache-dir flask==3.0.3
COPY src/data/imagenet_classes.txt /opt/mv2-imx93/imagenet_classes.txt
COPY src/app.py .
COPY src/templates/ templates/
COPY src/static/ static/
EXPOSE 3000
CMD ["python", "app.py"]
Create webapp/compose.yaml with the following complete contents:
services:
webapp:
platform: linux/arm64
build:
context: .
dockerfile: Dockerfile
privileged: true
ports:
- "${WEBAPP_PORT:-3001}:3000"
volumes:
- /sys:/sys
- /dev:/dev
restart: unless-stopped
Create the root compose.yaml with the following complete contents:
services:
webapp:
platform: linux/arm64
extends:
file: webapp/compose.yaml
service: webapp
Check that Compose can read the project:
docker compose config
The output includes the resolved webapp service, and is similar to:
services:
webapp:
build:
context: /path/to/new-topo-npu-template/webapp
dockerfile: Dockerfile
ports:
- mode: ingress
target: 3000
published: "3001"
At this point, Compose can build and run the Cortex-A web application as a normal Linux container. The image runs webapp/src/app.py. It packages the Jinja templates from webapp/src/templates/, the static assets from webapp/src/static/, and the ImageNet labels from webapp/src/data/imagenet_classes.txt. The container listens on port 3000, and Compose publishes it on host port 3001 unless you set WEBAPP_PORT to another value.
The web application needs an ExecuTorch .pte model, and the target needs a Cortex-M33 ELF image. Both artifacts are built by executorch-runner/Dockerfile.
Copy the Dockerfile into the runner build context:
cp ../topo-imx93-npu-deployment/executorch-runner/Dockerfile executorch-runner/
For this multi-stage Dockerfile:
build-base: installs the common Ubuntu build tools.executorch-base: clones ExecuTorch, installs the Arm backend dependencies, and copies export_mv2_imx93.py and docker-entrypoint.sh.pte-builder: exports mv2_ethosu65_256.pte.pte-artifacts: packages the .pte file as a BuildKit artifact context.runner-base: installs the Arm GNU toolchain, MCUX SDK, RPMsg-Lite dependencies, runner sources, and local patches.runner-builder: builds executorch_runner_cm33.elf.runner-artifacts: packages the ELF for inspection or reuse.runner-runtime: produces a scratch image whose entrypoint is the ELF file.The important artifact stages look like this:
FROM busybox:1.36 AS pte-artifacts
COPY --from=pte-builder /workspace/build/mv2-imx93/mv2_ethosu65_256.pte /artifacts/mv2_ethosu65_256.pte
FROM busybox:1.36 AS runner-artifacts
COPY --from=runner-builder /artifacts/executorch_runner_cm33.elf /artifacts/executorch_runner_cm33.elf
FROM scratch AS runner-runtime
COPY --from=runner-builder /artifacts/executorch_runner_cm33.elf /executorch_runner_cm33.elf
ENTRYPOINT ["/executorch_runner_cm33.elf"]
Add pte-artifacts and runner-artifacts as siblings of the existing webapp service in the root compose.yaml:
services:
pte-artifacts:
platform: linux/arm64
scale: 0
build:
context: executorch-runner
dockerfile: Dockerfile
target: pte-artifacts
cache_from:
- ${EXECUTORCH_BASE_CACHE_IMAGE:-ghcr.io/arm-examples/topo-imx93-npu-deployment/executorch-base:et-v1.2.0-ubuntu24.04}
runner-artifacts:
platform: linux/arm64
scale: 0
build:
context: executorch-runner
dockerfile: Dockerfile
target: runner-artifacts
cache_from:
- ${IMX93_RUNNER_BUILD_CACHE_IMAGE:-ghcr.io/arm-examples/topo-imx93-npu-deployment/imx93-runner-build:mcux-v25.09.00-armgcc14.2-ubuntu24.04}
Don’t replace the existing root webapp service with this snippet. The root file should now have three service names under the same top-level services: map: webapp, pte-artifacts, and runner-artifacts.
These services are used only to build artifacts. They don’t run as part of the deployed application. scale: 0 tells Compose not to start containers for them, while still allowing other services to copy files from their build outputs.
Replace webapp/compose.yaml with the following version so the Flask image imports the .pte artifact:
services:
webapp:
platform: linux/arm64
build:
context: .
dockerfile: Dockerfile
additional_contexts:
pte_artifacts: service:pte-artifacts
privileged: true
ports:
- "${WEBAPP_PORT:-3001}:3000"
volumes:
- /sys:/sys
- /dev:/dev
restart: unless-stopped
Then add the .pte copy line to webapp/Dockerfile with the other COPY commands:
COPY --from=pte_artifacts /artifacts/mv2_ethosu65_256.pte /opt/mv2-imx93/mv2_ethosu65_256.pte
The /opt/mv2-imx93/ path is the location the Flask application expects for its MobileNetV2 support files. At run time, the app reads the .pte file from this path before copying it into reserved memory for the Cortex-M33 runner.
Add the Cortex-M33 runner as another sibling under the top-level services: map in the root compose.yaml:
services:
cm33-runner:
platform: linux/arm64
build:
context: executorch-runner
dockerfile: Dockerfile
target: runner-runtime
cache_from:
- ${IMX93_RUNNER_BUILD_CACHE_IMAGE:-ghcr.io/arm-examples/topo-imx93-npu-deployment/imx93-runner-build:mcux-v25.09.00-armgcc14.2-ubuntu24.04}
runtime: io.containerd.remoteproc.v1
annotations:
remoteproc.name: imx-rproc
Keep the existing webapp, pte-artifacts, and runner-artifacts services in the same file. This step adds one more service and doesn’t replace any of the previous services.
This is the heterogeneous deployment hook. Docker still builds an image, but the service doesn’t start as a Linux userspace process. The runtime io.containerd.remoteproc.v1 selects Remoteproc Runtime, and the remoteproc.name annotation tells the shim to use the i.MX remote processor driver.
Update the existing root webapp service so it depends on the CM33 runner and passes the cache image values into the build. Keep the existing extends block, then add depends_on and build.args as shown:
services:
webapp:
platform: linux/arm64
extends:
file: webapp/compose.yaml
service: webapp
depends_on:
- cm33-runner
build:
args:
EXECUTORCH_BASE_CACHE_IMAGE: ghcr.io/arm-examples/topo-imx93-npu-deployment/executorch-base:et-v1.2.0-ubuntu24.04
IMX93_RUNNER_BUILD_CACHE_IMAGE: ghcr.io/arm-examples/topo-imx93-npu-deployment/imx93-runner-build:mcux-v25.09.00-armgcc14.2-ubuntu24.04
The web app is privileged and mounts /sys and /dev because it checks the device tree, reads remoteproc state through /sys/class/remoteproc, talks to /dev/ttyRPMSG*, writes shared memory through /dev/mem, and checks for /dev/ethosu0.
Keep the web app build context in webapp/compose.yaml. The root webapp.build.args block supplies only Topo-collected build arguments. It shouldn’t replace the extended build context and Dockerfile from webapp/compose.yaml.
After the Compose services are complete, add the root-level x-topo block.
Keep it at the root of compose.yaml, as a sibling of services, not under services.
If you want to use an agent skill to perform this step, skip to the optional step.
x-topo:
name: "i.MX93 ExecuTorch runner"
description: "Runs a Cortex-A web application that sends image inference commands to a resident CM33 ExecuTorch runner over RPMsg."
features:
- "remoteproc-runtime"
args:
EXECUTORCH_BASE_CACHE_IMAGE:
description: Optional GHCR image used as a BuildKit cache source for the ExecuTorch PTE build.
required: false
default: ghcr.io/arm-examples/topo-imx93-npu-deployment/executorch-base:et-v1.2.0-ubuntu24.04
IMX93_RUNNER_BUILD_CACHE_IMAGE:
description: Optional GHCR image used as a BuildKit cache source for the CM33 runner build.
required: false
default: ghcr.io/arm-examples/topo-imx93-npu-deployment/imx93-runner-build:mcux-v25.09.00-armgcc14.2-ubuntu24.04
The features value tells Topo that this template requires remoteproc-runtime support on the target. This is useful when checking for project compatibility with the topo templates --target <target> command.
The args entries describe configurable build inputs. Compose consumes those values through the cache_from interpolation that you added earlier:
cache_from:
- ${EXECUTORCH_BASE_CACHE_IMAGE:-ghcr.io/arm-examples/topo-imx93-npu-deployment/executorch-base:et-v1.2.0-ubuntu24.04}
The root webapp.build.args block also makes the Topo-provided values visible in the Compose build model while preserving the webapp/ build context inherited through extends.
Keep runtime settings such as WEBAPP_PORT as normal Compose interpolation unless you intentionally want Topo to collect them as Topo Template setup arguments.
The Topo Template Format repository includes public authoring skills for agents that support skill installation:
topo-template-context: provides Topo and Topo Template reference context for x-topo metadata, schema, docs, and CLI template behavior.topo-template-bootstrap: converts a Compose repository into a Topo Template by adding or improving compose.yaml and x-topo metadata.topo-template-lint: reviews a Topo Template for schema correctness, metadata consistency, deployment success messages, and build argument wiring.Install the skills with npx skills:
npx skills add arm/topo-template-format
If your agent doesn’t use npx skills, manually copy or symlink the directories under ../topo-template-format/skills/ into your agent’s skills directory.
Restart your agent after installing or updating the skills.
From the root of the Compose project, ask your agent to use topo-template-bootstrap:
Use topo-template-bootstrap on this repository.
Treat the root compose.yaml as the Template root.
Preserve plain docker compose behavior.
Add x-topo metadata only where it reflects the actual services, hardware requirements, and build arguments.
After bootstrap, ask the agent to use topo-template-lint:
Use topo-template-lint on this repository.
Validate compose.yaml against the Topo Template Format schema.
Check README alignment, Remoteproc Runtime metadata, and x-topo.args wiring.
Check the Compose model and check that the Topo metadata is present:
docker compose config
In the docker compose config output, check that the resolved webapp service has:
build.context ending in /webappbuild.dockerfile set to Dockerfilebuild.additional_contexts.pte_artifacts set to service:pte-artifactsInstall check-jsonschema if it’s not already available:
brew install check-jsonschema
sudo apt update
sudo apt install -y pipx
pipx ensurepath
pipx install check-jsonschema
export PATH="$HOME/.local/bin:$PATH"
Validate the root Compose file with the schema in the Topo Template Format:
check-jsonschema \
--schemafile ../topo-template-format/schema/topo-template-format.json \
compose.yaml
Review these points:
compose.yaml contains root-level x-topo metadata.x-topo.features includes remoteproc-runtime.platform: linux/arm64.pte-artifacts and runner-artifacts use scale: 0.cm33-runner uses runtime: io.containerd.remoteproc.v1.cm33-runner has remoteproc.name: imx-rproc.webapp depends on cm33-runner.webapp imports the .pte file through additional_contexts.x-topo.args entry is consumed by Compose interpolation.You started with two non-Topo, non-Compose projects, made them a standard Compose project, and then converted that Compose project into a Topo Template. You created the web app image, added artifact builds for the ExecuTorch .pte model and Cortex-M33 ELF, packaged the firmware as a Remoteproc Runtime service, and exposed the build cache inputs as Topo arguments.
Next, you’ll prepare the FRDM i.MX 93 target, deploy the template with Topo, and run the image classification application.