Engineer working with cables and boards.

Compose Apps for IoT: Why They Matter and How ComposeCtl Helps

Photo of Mykhaylo Sul

Posted on Mar 26, 2026 by Mykhaylo Sul

9 min read

Building containers for embedded systems, edge, or IoT devices can be tricky.

These days, even small devices are running complex multi-service systems — think databases, inference engines, dashboards, data collectors, message brokers — all while being constrained on storage, network, and power.

The big question is: how do you build, deploy, test, and update these systems in a reliable, security-focused way — without endless frustration?

The answer is surprisingly simple: treat your IoT apps like cloud-native apps and start working with containers on embedded devices. Instead of juggling scripts and containers manually, you describe your whole system in a Compose file using the Compose Specification.

These Compose Apps make life easier: they're reproducible, so your builds are consistent; portable, so they run on different architectures; and maintainable, so you can tweak or extend services without breaking everything. You're bringing cloud-native best practices — declarative definitions, versioned deployments, modular services — to IoT and embedded development. The same reliability and structure we love in cloud apps, but on devices that live out in the field.

The best part? Once your app is defined this way, you've got a solid foundation for deploying it safer, with more reliability and security — including seamless IoT OTA updates.

Why Compose Apps Make Sense for IoT and Embedded Systems

A Compose App is just a system described declaratively, but it makes a huge difference in practice. Let's look at a real-life example: Home Assistant, a smart home automation platform. You can run it on a single board computer, like the Arduino® UNO™ Q board, to streamline your Docker IoT deployments using Docker Compose.

Here's the official Home Assistant Docker Compose example:

version: '3'
services:
  homeassistant:
    container_name: home-assistant
    image: ghcr.io/home-assistant/home-assistant:stable
    volumes:
      - ./config:/config
      - /etc/localtime:/etc/localtime:ro
    restart: unless-stopped
    network_mode: host

Even with a single service like Home Assistant, it's easy to see why containers are the next new thing in the embedded Linux world. This approach offers several advantages relevant to all IoT developers, such as:

  • Real, maintained image: fully deployable on devices like UNO Q, Raspberry Pi or NUC.
  • Persistent configuration: survives reboots through the volume mapping.
  • Host networking: allows Home Assistant to communicate with local devices, sensors, and actuators.

From here, you can extend your Compose App by adding:

  • MQTT brokers for sensor-actuator messaging
  • Node-RED for automation flows
  • Grafana for dashboards

Even with a single container like Home Assistant, you already see the benefits of Compose Apps: declarative, reproducible, and portable.

The Gap: From Compose File to Deployable App

A Compose file alone isn't enough to run apps safely on a fleet of devices. You still need to:

  • Build and package reproducibly
  • Version and sign for security-focused distribution
  • Deploy and update more safely and reliably over unreliable networks

We at Foundries.io came up with the bridge to pass over this gap, meet the composeapp project.

composeapp: From Compose File to OTA-Ready Package

composeapp turns your Compose App into a portable, versioned, and reproducible artifact, ready for deployment on IoT and embedded devices.

Typical workflow looks like the following:

[Define Compose App (docker-compose.yml)]
    --> [Package and Publish App to container registry]
    --> [Device: Fetch App]
    --> [Device: Install and start App]
  1. Define your Compose App in docker-compose.yml
cat docker-compose.yml
services:
  homeassistant:
    container_name: homeassistant
    image: "ghcr.io/home-assistant/home-assistant:2025.10.2"
    volumes:
      - /PATH_TO_YOUR_CONFIG:/config
      - /etc/localtime:/etc/localtime:ro
      - /run/dbus:/run/dbus:ro
    restart: unless-stopped
    privileged: true
    network_mode: host
  1. Package and publish Compose App by using composectl - a CLI utility that can be build out of the composeapp project.
composectl publish ghcr.io/foundriesio/ha:2025.10.2 amd64,arm64

This command bundles the docker-compose.yml and associated project files (e.g., configuration, static assets, etc.) into an OCI-compliant image and publishes it to the target container registry. It also gathers metadata about the app's image layers and adds this metadata into the OCI image representing Compose App.

  1. Once the Compose App is published to container registry it can be pulled, installed, and started on a device.
composectl pull ghcr.io/foundriesio/ha@sha256:c86b2fb49885895595e8b41256234f3b42a7048075dc245ead48227a89bed619
composectl run ha # to run app you just specify its name, no need to specify an app URI with digest

Now you have an immutable, signed Compose App image, safe to deploy and update across devices.

Building container images used in Compose App

composectl is a tool designed specifically for packaging, publishing, pulling, and running Compose Apps, it does not build the container images Compose App consists of.

Therefore, all container images of Compose App must be built and uploaded to some container registry prior to packaging and publishing the App itself. There are many tools out there for image building, it is up to the user to decide which of them to use, e.g. docker | podman build, buildah, buildctl, etc.

For example, let's extend the above mentioned App example with an additional service (e.g. it could be Home Assistant plugin). A dummy container image will be used for simplicity.

cat Dockerfile

FROM alpine
COPY httpd.sh /usr/local/bin/
CMD ["/usr/local/bin/httpd.sh"]
cat httpd.sh
#!/bin/sh -e

PORT="${PORT-8080}"
MSG="${MSG-OK}"

RESPONSE="HTTP/1.1 200 OK\r\n\r\n${MSG}\r\n"

while true; do
	echo -en "$RESPONSE" | nc -l -p "${PORT}" || true
	echo "= $(date) ============================="
done

Let's build the image and push it to ghcr now:

docker buildx build -t ghcr.io/foundriesio/ha-plugin:v1.0 .
docker push ghcr.io/foundriesio/ha-plugin:v1.0

Once the image is published, it can be used in our App, let's extend the app compose definition, and then publish again.

cat docker-compose.yml
services:
  homeassistant:
    container_name: homeassistant
    image: "ghcr.io/home-assistant/home-assistant:2025.10.2"
    volumes:
      - /PATH_TO_YOUR_CONFIG:/config
      - /etc/localtime:/etc/localtime:ro
      - /run/dbus:/run/dbus:ro
    restart: unless-stopped
    privileged: true
    network_mode: host
  plugin:
    image: ghcr.io/foundriesio/ha-plugin:v1.0
    restart: always
    ports:
      - 8080:${PORT-8080}
    environment:
      MSG: "${MSG-Hello world}"
composectl publish ghcr.io/foundriesio/ha:2025.10.2 amd64,arm64

Now, we can pull the new app version ghcr.io/foundriesio/ha@sha256:fa002bce1c14c28a15816641962b37feef275b6d60ffc24b785489a800d56827 that includes the plugin (ghcr.io/foundriesio/ha-plugin:v1.0) we extended the app with, and reinstall and restart our App.

composectl pull ghcr.io/foundriesio/ha@sha256:fa002bce1c14c28a15816641962b37feef275b6d60ffc24b785489a800d56827
composectl run ha

And, finally, make sure our dummy plugin is running:

curl http://localhost:8080

Hello world

Automating with CI/CD

The development flow above can be automated. Any App changes pushed to a source code repository can trigger a CI/CD pipeline that:

  1. Builds and publishes the App's container images,
  2. Packages and publishes a new version of the App,
  3. Triggers automated tests,
  4. Optionally triggers deployment of the new App version to a subset of devices or hosts.

This Github workflow action is an example of such automation.

Why This Matters

Compose Apps + composectl along with container building tools give you a comprehensive developer-to-device workflow:

  • Developers: iterate locally using a Compose file and Dockerfile
  • CI/CD: produce reproducible, versioned artifacts
  • Devices: pull and run atomic, security-focused updates

In short: define once, build more reliably, deploy more securely.

Why Not Use Industry-Standard Tools for It?

It's fair to ask why not just rely on existing, industry-standard tools.

Docker Compose is widely used, and there was even an effort to introduce a publish command (compose-spec#288) with an initial implementation in Docker Compose itself (docker/compose#11008).

However, that proposal was recently closed as "not planned", and the current docker compose publish command only packages the Compose project files (*.yml).

In real-world setups, a Compose App often includes additional assets — configuration files, static data, or other complementary resources — that should be packaged and distributed together with the project.

Even with a Compose project packaged as an OCI image, there's still the question of how to fetch it reliably on devices. Standard OCI tooling isn't designed for edge or IoT environments with flaky connectivity.

Here are the key limitations of relying solely on standard tools at the edge:

  • When a download is interrupted, commands like docker compose pull and docker compose up discard partially pulled blobs and restart from scratch.
  • For devices with large images and unstable networks, this behavior simply isn't practical.

composectl was built to close these gaps — providing an end-to-end, OCI-compliant workflow for packaging, distributing, and updating complete Compose-based applications with more reliable, resumable image fetching.

Next Step: Security-focused OTA and Fleet Management

Packaging your Compose App is only half the story. When it comes to maintaining and securing a fleet of embedded devices, you also need:

  • Security focused OTA updates
  • Version tracking and rollback
  • Fleet-wide app management

For example, the workflow above produces and publishes a Compose App. The questions are:

  1. How can we help ensure that the App cannot be pulled by unauthorized devices?
  2. How can devices be notified when a new App version is available?
  3. How can an App update be triggered remotely if devices are unmanaged (i.e., have no direct user)?
  4. How can a device verify that the provided new App version URI is authentic and trustworthy?

fioup and FoundriesFactory™ platform handle these gaps and addresses these questions. In the next post, we'll show how they enable more reliable and security focused OTA delivery.

Summary

  • Compose Apps bring cloud-native best practices to IoT and embedded devices.
  • composeapp turns Compose Apps into reproducible, OTA-ready artifacts.
  • Together, they form the foundation for safer, more reliable, and security focused app delivery.
  • Next up: fioup and FoundriesFactory for OTA update and fleet management.

Key takeaways for your DevSecOps workflow:

  • Declarative & Reproducible: Define your entire multi-service system once using the Compose Specification, ensuring consistency from local development to production.

  • Edge-Optimized Packaging: Use composectl to bundle your configuration, static assets, and Compose definitions into a single, versioned, OCI-compliant image.

  • Reliable Delivery: Overcome the limitations of standard Docker tools with resumable image fetching designed specifically for the flaky networks common in IoT environments.

Ultimately, packaging your Compose App into a reproducible, OTA-ready artifact is a crucial foundational step. But to truly operate at scale, you need a mechanism for delivering these updates in a security-rich way to an entire fleet.

In our next post, we will explore how fioup and FoundriesFactory platform take these artifacts and provide the security-focused OTA delivery, version tracking, and fleet-wide management required to keep your production devices safer and up to date.

Get Started Today

Whether you're evaluating platforms or ready to build, we recommend starting with our free Community Edition to explore what's possible with the FoundriesFactory platform.

Related posts