SIMATIC IOT2050 Custom Ubuntu Image with Docker and Telegraf

David Krause11 min read
Other TopicSiemensTutorial / How-to
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Overview

The SIMATIC IOT2050 is Siemens' industrial IoT gateway built on the TI ARM64 (Sitara) platform. The device ships in two hardware variants — IOT2050 Basic (1 GB RAM, 4 GB eMMC) and IOT2050 Advanced (2 GB RAM, 16/32 GB eMMC) — and is supported by the open-source meta-iot2050 build layer, which produces a Debian-based Ubuntu ARM64 image using the Isar (Image Creation and Assembly) build framework rather than classic Yocto/BitBake packaging.

This reference covers three production-proven strategies for pre-integrating a metrics stack (Telegraf + InfluxDB + Grafana) and a container runtime (Docker) into a redistributable IOT2050 image so the end customer can flash a single USB stick and run the device with no further shell work:

  1. Golden Image — install on hardware, capture the on-disk state, re-flash it as the shipping image.
  2. Isar Debian Layer — describe every install step in a reproducible recipe using dpkg-prebuilt and IMAGE_INSTALL_append.
  3. First-Boot Service — ship a small bootstrap service that fetches a setup script from a known server and runs it once after the first-boot filesystem resize.

Each method is evaluated against reproducibility, build-cycle time, and operator complexity.

Prerequisites

Item Value / Version
Host OS (build) Debian 11 (bullseye) or Ubuntu 20.04 LTS, 64-bit
Disk on host ≥ 80 GB free (Isar cache + sstate)
RAM on host ≥ 8 GB (16 GB recommended for parallel build)
Docker on host Optional but recommended for host-side testing of the Telegraf container
meta-iot2050 branch jan/ubuntu for the Ubuntu ARM64 image
Isar Submodule pinned to a release tag matching meta-iot2050
Target IOT2050 firmware Example Image ≥ V1.2.x (matches BSP supplied with the layer)
Telegraf 1.27.x ARM64 static binary or upstream telegraf deb from InfluxData repo
Network egress: the Isar build fetches Debian/Ubuntu apt indexes, the InfluxData repository, and Docker CE packages. If the build host is air-gapped, mirror these repositories into a local apt server and override DEPLOY_APT_REPO / APT_REPO in your image recipe.

Method 1: Golden Image (fastest path)

The Golden Image approach is appropriate when reproducibility is secondary to time-to-delivery. You treat one already-flashed IOT2050 as the canonical device, freeze its software state, and re-deploy that exact image to every other unit.

Workflow

  1. Flash the upstream iot2050-image-example-ubuntu.wic from meta-iot2050 to the IOT2050 Advanced using the Siemens DP image tool or balenaEtcher.
  2. Insert the device, expand the rootfs on first boot, and confirm resize2fs ran cleanly:
df -h /
systemctl is-system-running
  1. Install Docker and Telegraf directly on the device as outlined in Section 4.
  2. Enable and start the services, validate metrics ingestion, then power down cleanly.
  3. Pull the eMMC with dd to a remote host (e.g. dd if=/dev/mmcblk0 | ssh user@host "dd of=iot2050-golden-$(date +%F).img").
  4. Shrink the image with pishrink or image-shrink.sh, then re-flash onto other units with the Siemens DP Image tool.
Reproducibility warning: each clone of the golden image will have identical SSH host keys, identical machine-IDs, and identical Telegraf device names. Run truncate -s 0 /etc/machine-id; systemd-machine-id-setup; rm /etc/ssh/ssh_host_* on the target box or use a post-flash hook before declaring the device production-ready.

Method 2: Isar Debian Layer (reproducible, recommended)

Method 2 produces a deterministic image by declaring the desired software state inside BitBake/Isar recipes. The build runs in a chroot, but the recipes are themselves plain Debian packaging, so you can ship pre-built .deb files that you have already validated on a running IOT2050.

Layer layout

meta-mycustomer/
├── recipes-app/
│   ├── telegraf/
│   │   └── telegraf_1.27.4.bb
│   ├── prebuilt-deb/
│   │   └── my-setup_1.0.0.bb
│   └── docker-ce/
│       └── docker-ce_24.0.bb
├── recipes-core/
│   └── images/
│       └── iot2050-image-customer.bbappend
└── conf/
    └── layer.conf

conf/layer.conf

BBPATH .= ":${LAYERDIR}"
BBFILE_COLLECTIONS += "mycustomer"
BBFILE_PATTERN_mycustomer = "^${LAYERDIR}/"
BBFILE_PRIORITY_mycustomer = "10"
LAYERSERIES_COMPAT_mycustomer = "kirkstone"
LAYERDEPENDS_mycustomer = "iot2050 isar"

recipes-core/images/iot2050-image-customer.bbappend

inherit image

IMAGE_INSTALL += " \
    telegraf \
    docker-ce \
    my-setup \
    prebuilt-deb-example \
"

IMAGE_INSTALL_append = " curl ca-certificates systemd"

The IMAGE_INSTALL variable is the single source of truth for the runtime package set. The jan/ubuntu branch of meta-iot2050 ships a base recipe at recipes-core/images/iot2050-image-example.bb that you append to rather than fork.

Including a prebuilt .deb with dpkg-prebuilt

Use the dpkg-prebuilt class from Isar to wrap an external Debian package without rebuilding it from source. The following recipe mirrors the pattern in the upstream meta-isar example:

# recipes-app/prebuilt-deb/prebuilt-deb-example_1.0.0.bb
# SPDX-License-Identifier: MIT
inherit dpkg-prebuilt

SRC_URI = "https://artifacts.example.com/debs/example-prebuilt_1.0.0-0_all.deb"
SRC_URI[sha256sum] = "REPLACE_WITH_REAL_SHA256"

PROVIDES += "example-prebuilt"
RPROVIDES_${PN} += "example-prebuilt"

Place the recipe in recipes-app/prebuilt-deb/ and add example-prebuilt to IMAGE_INSTALL. Isar will fetch the deb, place it in the image's apt pool, and install it during image assembly. The build is fully reproducible: every package, every hash, every file is declared in the recipe.

Including systemd-enabled .debs

Isar's image assembly uses a chroot environment, not a running systemd --system instance. A .deb postinst that calls systemctl enable foo.service will fail with Failed to connect to bus during the build. Two robust patterns exist:

  1. Ship a systemd unit but enable it via a recipe hook. Drop the unit file under /lib/systemd/system/foo.service from the .deb, and add the unit to a startup symlink from an Isar recipe:
inherit dpkg-prebuilt

do_install_append() {
    install -d ${D}/etc/systemd/system/multi-user.target.wants
    ln -sf /lib/systemd/system/foo.service \
           ${D}/etc/systemd/system/multi-user.target.wants/foo.service
}
  1. Use an enable-presets file. Isar honors /lib/systemd/system-preset/ and will enable matching units at first boot through the preset mechanism, which is the same code path used by systemctl preset-all.
# In your .deb, ship:
# /lib/systemd/system-preset/90-customer.preset
enable foo.service
enable bar.service

Method 3: First-Boot Service (most flexible)

This method keeps the image lean and pushes configuration to runtime. A small systemd unit, enabled in the base image, runs once on first boot, fetches a setup script from a known URL, and exits. The setup script can install anything you need — including debs that ship their own systemd units.

Drop-in service file (shipped inside the image)

# /lib/systemd/system/iot2050-firstboot.service
[Unit]
Description=IOT2050 First-Boot Provisioning
ConditionPathExists=/var/lib/iot2050-firstboot.lock
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/iot2050-firstboot.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

Bootstrap script (also shipped in the image)

#!/bin/sh
# /usr/local/bin/iot2050-firstboot.sh
set -e
LOCK=/var/lib/iot2050-firstboot.lock
URL="https://setup.example.com/iot2050/setup.sh"
curl -fsSL "$URL" -o /tmp/setup.sh
sh /tmp/setup.sh
touch "$LOCK"
systemctl disable iot2050-firstboot.service
First-boot filesystem resize: the IOT2050 service runs iot2050-resize-fs.service before any network-dependent unit, expanding the rootfs to fill the eMMC. Order your custom service After= iot2050-resize-fs.service and network-online.target to ensure the disk is full size and the network is up before setup runs. Add the dependency in the unit file: After=iot2050-resize-fs.service network-online.target.

Enable the service from the image recipe

# In your b.bbappend or custom image recipe:
SYSTEMD_SERVICE_${PN} += "iot2050-firstboot.service"
SYSTEMD_AUTO_ENABLE_${PN} = "enable"

Telegraf Configuration

Telegraf collects system, Docker, and (optionally) Modbus/OPC UA metrics and pushes them to InfluxDB, Prometheus, MQTT, or any other supported output. Two integration paths are common on the IOT2050.

Path A — Native Telegraf binary

Use the ubuntu/telegraf image only as a reference for the upstream configuration schema. On the IOT2050 itself, install the binary from InfluxData's apt repo or from a deb prebuilt on your build host:

wget -qO- https://repos.influxdata.com/influxdb.key | gpg --dearmor \
    -o /usr/share/keyrings/influxdb.gpg
echo "deb [signed-by=/usr/share/keyrings/influxdb.gpg] \
    https://repos.influxdata.com/debian bullseye stable" \
    > /etc/apt/sources.list.d/influxdb.list
apt update
apt install -y telegraf=1.27.*

Path B — Telegraf inside Docker

For deployments that already run Docker workloads, run Telegraf as a container with the host's /proc, /sys, and Docker socket mounted read-only:

docker run -d --name=telegraf --restart=always \
  -v /:/host:ro \
  -v /var/run/docker.sock:/var/run/docker.sock:ro \
  -v /sys:/host/sys:ro \
  -v /proc:/host/proc:ro \
  -v $PWD/telegraf.conf:/etc/telegraf/telegraf.conf:ro \
  telegraf:1.27

telegraf.conf skeleton

[agent]
  interval = "10s"
  flush_interval = "10s"
  hostname = "$HOSTNAME"
  omit_hostname = false

[[inputs.cpu]]
  percpu = true
  totalcpu = true

[[inputs.mem]]

[[inputs.disk]]
  mount_points = ["/"]
  ignore_fs = ["tmpfs", "devtmpfs", "overlay"]

[[inputs.diskio]]

[[inputs.net]]

[[inputs.internal]]

[[inputs.system]]

[[inputs.docker]]
  endpoint = "unix:///var/run/docker.sock"
  container_names = []
  source_tag = true

[[inputs.smart]]
  path = "/usr/sbin/smartctl"
  attributes = ["id", "name", "model"]

[[outputs.influxdb]]
  urls = ["http://127.0.0.1:8086"]
  database = "iot2050"
  retention_policy = ""
  timeout = "5s"
  useragent = "telegraf"

Docker Integration on the IOT2050

The IOT2050 Basic (1 GB RAM) is tight for Docker. The Advanced (2 GB) is the recommended target. Use docker-ce from Docker's official apt repo, not the Debian-packaged docker.io package, to avoid the older containerd shipped with Debian 11.

install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
    | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=arm64 signed-by=/etc/apt/keyrings/docker.gpg] \
    https://download.docker.com/linux/ubuntu jammy stable" \
    > /etc/apt/sources.list.d/docker.list
apt update
apt install -y docker-ce docker-ce-cli containerd.io \
               docker-buildx-plugin docker-compose-plugin
storage driver: the eMMC on the IOT2050 is not a fast SSD. overlay2 is supported, but on the Basic variant the eMMC endurance budget is small — prefer vfs for low-write workloads and configure log-driver=json-file with size limits (--log-opt max-size=5m --log-opt max-file=3) to avoid log-driven wear.

Building the Image

From the Isar build directory (after source setup-environment build-iot2050 and dropping your meta-mycustomer layer into conf/bblayers.conf):

bitbake iot2050-image-customer

Build artefacts land in build-iot2050/tmp/deploy/images/iot2050/:

File Purpose
iot2050-image-customer-iot2050.wic Raw SD/eMMC image, write with dd or the Siemens DP tool
iot2050-image-customer-iot2050.wic.bmap Bmap file for bmaptool copy (faster, validates during write)
iot2050-image-customer-iot2050.tar.gz Rootfs tarball, useful for differential OTA updates

Verification

  1. Write the image to an SD card or directly to eMMC:
bmaptool copy --bmap iot2050-image-customer-iot2050.wic.bmap \
                iot2050-image-customer-iot2050.wic \
                /dev/sdX
  1. Boot the IOT2050, log in over the serial console or SSH (default user root, no password — set one immediately).
  2. Confirm the resize completed:
df -h /
lsblk
  1. Confirm Docker:
docker version
docker run --rm hello-world
  1. Confirm Telegraf is emitting metrics to InfluxDB:
systemctl status telegraf
journalctl -u telegraf -n 50
influx -execute 'SHOW MEASUREMENTS' -database iot2050
  1. Confirm the first-boot service did not run twice:
ls -l /var/lib/iot2050-firstboot.lock
systemctl is-enabled iot2050-firstboot.service

Troubleshooting Matrix

Symptom Likely cause Fix
dpkg-prebuilt build fails with 404 SRC_URI URL or checksum drift Re-host the deb on a stable URL, update SRC_URI[sha256sum], rebuild
Service enabled in image does not start Isar chroot cannot talk to systemd Use systemd-preset or write the symlink in do_install_append (Section 3.5)
First-boot service runs every boot Missing ConditionPathExists or lock file Verify touch /var/lib/iot2050-firstboot.lock in the script
Telegraf inside Docker sees no host metrics /proc and /sys not mounted Add -v /proc:/host/proc:ro -v /sys:/host/sys:ro to the run command
docker run fails with exec format error Pulled an x86_64 image by accident Use the ARM64 manifest, e.g. telegraf:1.27 pulls the linux/arm64 variant on the IOT2050
InfluxDB receives no data Telegraf points at 127.0.0.1 but InfluxDB runs on a different container network Use the container name as hostname, or join both containers to a user-defined bridge
Image build takes hours for a one-line change No sstate cache reuse Mount persistent sstate-cache and download directories; rebuild with bitbake iot2050-image-customer -c rootfs instead of full clean

Reproducibility and Maintenance Trade-offs

Method Reproducibility Build cycle per change Operator skill Best for
Golden Image Low (manual steps) N/A (live device) Low Single pilot, demos
Isar Layer High (declarative) ~5–15 min with sstate, ~hours clean Medium Production fleets > 5 units
First-Boot Service Medium (depends on remote URL) ~5 min (just edit script) Low Frequently changing config, pilot rollouts

A common hybrid pattern: build the Isar image with a minimal set of packages and the first-boot service skeleton, and let the first-boot service install the application-specific debs. This keeps the image artifact small, the build deterministic, and the per-customer logic in a single versioned file on your artifact server.

Operational Notes

  • Reserve at least 200 MB of free eMMC after image install — the IOT2050 firmware updater and apt's downloaded-package cache need headroom.
  • Disable the apt-daily.timer and apt-daily-upgrade.timer in production images; they will trigger dpkg runs at random intervals and can collide with your services.
  • For OTA updates, use swupdate (already pulled in by meta-iot2050) with a signed CPIO image; do not reflash the full .wic in the field.
  • Watch the kernel omap_wdt watchdog — if Telegraf or Docker hangs for > 60 s, the IOT2050 will reboot, which is desirable in a brownout scenario but noisy in logs.

Which Telegraf Docker image should I use on the IOT2050?

Use the official telegraf:1.27 image from Docker Hub. It is a multi-arch manifest that resolves to the linux/arm64 variant on the IOT2050. The ubuntu/telegraf image is an alternative if you need to match the base distribution more tightly.

Why does my .deb's postinst fail during the Isar build?

Isar assembles the image inside a chroot without a running systemd instance. Any postinst step that calls systemctl enable or systemctl start will fail with Failed to connect to bus. Either move the enable step to a systemd preset file in /lib/systemd/system-preset/ or create the symlink manually in a do_install_append function.

How do I speed up incremental Isar builds?

Keep the sstate cache and the apt download cache on a persistent volume (for example, bind-mount build-iot2050/sstate-cache and build-iot2050/tmp/deploy/apt). After editing a recipe, run bitbake iot2050-image-customer -c rootfs to rebuild only the rootfs target, which reuses compiled artefacts and only re-runs the package install step.

Can I run the IOT2050 headless after flashing the custom image?

Yes. Disable the serial-getty and the getty on tty1 in your image recipe, ensure the SSH server is enabled (it is by default in the jan/ubuntu branch), and either bake a known SSH public key into /root/.ssh/authorized_keys or set a root password hash via passwd -u root in a post-install hook.

Does the first-boot filesystem resize run before my custom service?

Only if you order it explicitly. Add After=iot2050-resize-fs.service network-online.target and Wants=network-online.target to your unit file. The default iot2050-resize-fs.service runs resize2fs on the root partition and then touches /var/lib/iot2050-resize.done — gate your provisioning on that file's existence to be safe.

Back to blog