32blogby Studio Mitsu

Yocto systemd Auto-Start: Setup Guide with 7 Pitfalls

Add systemd services to Yocto with auto-start and timers. Includes 7 common pitfalls, based on Scarthgap 5.0 LTS.

by omitsu10 min read

This article contains affiliate links.

On this page

To auto-start a service in Yocto, set INIT_MANAGER = "systemd" in your distro config, write a recipe that inherits the systemd bbclass, and include WantedBy=multi-user.target in your service file's [Install] section. If any of these three pieces is missing, your service silently fails to start.

But getting it right involves more than those three lines. You need to understand how Yocto's INIT_MANAGER variable interacts with DISTRO_FEATURES, how to write service files for embedded targets, and how to avoid the pitfalls that trip up even experienced developers.

This guide covers everything from adding systemd services to 7 common pitfalls, based on Yocto Scarthgap 5.0 LTS (systemd 255).

Enabling systemd with INIT_MANAGER

Yocto defaults to sysvinit as the init system. You need to explicitly switch to systemd.

INIT_MANAGER Approach (Zeus 3.0+)

Add a single line to conf/local.conf or conf/distro/mydistro.conf.

bash
# conf/local.conf
INIT_MANAGER = "systemd"

This one line loads init-manager-systemd.inc, which automatically configures the following.

bash
# Contents of init-manager-systemd.inc (Scarthgap branch)
DISTRO_FEATURES:append = " systemd usrmerge"
DISTRO_FEATURES_BACKFILL_CONSIDERED:append = " sysvinit"
VIRTUAL-RUNTIME_init_manager ??= "systemd"
VIRTUAL-RUNTIME_initscripts ??= "systemd-compat-units"
VIRTUAL-RUNTIME_login_manager ??= "shadow-base"
VIRTUAL-RUNTIME_dev_manager ??= "systemd"
ROOT_HOME ?= "/root"

The Legacy Approach (Pre-Zeus)

Before Zeus, you had to manually set DISTRO_FEATURES and VIRTUAL-RUNTIME_* variables in local.conf. This still works but the one-line INIT_MANAGER approach is simpler.

Verifying the Configuration

bash
# Check that systemd is in DISTRO_FEATURES
bitbake-getvar DISTRO_FEATURES | grep systemd

Writing a systemd Service Recipe

To add a systemd service in Yocto, write a recipe that inherits the systemd bbclass.

Complete Recipe Example

bash
# meta-mylayer/recipes-apps/myapp/myapp_1.0.bb
SUMMARY = "My custom application"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

SRC_URI = "file://myapp.sh \
           file://myapp.service"

S = "${WORKDIR}"

inherit systemd

SYSTEMD_SERVICE:${PN} = "myapp.service"
SYSTEMD_AUTO_ENABLE:${PN} = "enable"

do_install() {
    install -d ${D}${bindir}
    install -m 0755 ${WORKDIR}/myapp.sh ${D}${bindir}/myapp

    install -d ${D}${systemd_system_unitdir}
    install -m 0644 ${WORKDIR}/myapp.service ${D}${systemd_system_unitdir}/
}

FILES:${PN} += "${systemd_system_unitdir}/myapp.service"

Key Variables

VariablePurposeDefault
SYSTEMD_SERVICE:${PN}Service file name(s) to manage${PN}.service
SYSTEMD_AUTO_ENABLE:${PN}Whether to auto-enableenable
SYSTEMD_PACKAGESPackages to manage with systemd${PN}

What inherit systemd does automatically:

  • Generates a postinst script that runs systemctl enable
  • Creates symlinks under multi-user.target.wants/
  • Generates a prerm script for package removal

Guarding Your Recipe for systemd

To prevent build errors on distros without systemd, use REQUIRED_DISTRO_FEATURES.

bash
REQUIRED_DISTRO_FEATURES = "systemd"

This causes the recipe to be skipped when DISTRO_FEATURES doesn't include systemd. If another recipe depends on it, a build error is raised, making it easy to trace the root cause.

Adding to an Existing Recipe via bbappend

If you need to add a systemd service to an existing recipe, use a bbappend file. See Pattern 3 in the bbappend practical guide for the specifics. Note that inherit systemd cannot be used in .bbappend files — the base .bb recipe must already inherit the systemd class.

Designing Service Files for Embedded

How you write the service file directly affects startup reliability. Here are the key points for embedded Linux.

Basic Structure

ini
# myapp.service
[Unit]
Description=My Application Service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/bin/myapp
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

[Unit] Section

After= controls startup ordering. A common mistake with network-dependent services is using After=network.target.

  • network.target — indicates network devices have been configured. Doesn't guarantee an IP address has been assigned
  • network-online.target — indicates the network is usable. Use this when your service needs to make API calls or network connections

[Service] Section

Type= specifies how the process starts.

TypeUse Case
simpleForeground process (default)
forkingDaemon that forks into the background
oneshotScript that runs once and exits

For embedded, set Restart=on-failure with RestartSec=5 (retry after 5 seconds) to enable automatic recovery from crashes.

Permissions

Install service files with 0644. Adding execute permissions (0755) can cause systemd to emit warnings.

bash
# Correct
install -m 0644 ${WORKDIR}/myapp.service ${D}${systemd_system_unitdir}/

# Wrong (execute permission is unnecessary)
install -m 0755 ${WORKDIR}/myapp.service ${D}${systemd_system_unitdir}/

Timer and Socket Activation Recipes

systemd's strengths go beyond services. Timers handle periodic tasks, and socket activation enables on-demand startup.

Timer Units

Use systemd timers instead of cron to keep dependencies and logs integrated with systemd.

ini
# myapp.timer
[Unit]
Description=Run myapp periodically

[Timer]
OnBootSec=1min
OnUnitActiveSec=1h
Persistent=true

[Install]
WantedBy=timers.target
ini
# myapp.service (triggered by the timer)
[Unit]
Description=My Application Task

[Service]
Type=oneshot
ExecStart=/usr/bin/myapp --run-task

In the recipe, list both files in SYSTEMD_SERVICE.

bash
SRC_URI = "file://myapp.sh \
           file://myapp.service \
           file://myapp.timer"

SYSTEMD_SERVICE:${PN} = "myapp.service myapp.timer"

do_install() {
    install -d ${D}${bindir}
    install -m 0755 ${WORKDIR}/myapp.sh ${D}${bindir}/myapp

    install -d ${D}${systemd_system_unitdir}
    install -m 0644 ${WORKDIR}/myapp.service ${D}${systemd_system_unitdir}/
    install -m 0644 ${WORKDIR}/myapp.timer ${D}${systemd_system_unitdir}/
}

FILES:${PN} += "${systemd_system_unitdir}/myapp.service \
                ${systemd_system_unitdir}/myapp.timer"

Socket Activation

Socket activation delays service startup until an actual connection arrives. This is effective for resource-constrained embedded devices.

ini
# myapp.socket
[Unit]
Description=My Application Socket

[Socket]
ListenStream=8080
Accept=no

[Install]
WantedBy=sockets.target
ini
# myapp.service (started by the socket)
[Unit]
Description=My Application
Requires=myapp.socket

[Service]
Type=simple
ExecStart=/usr/bin/myapp

The recipe syntax is the same as for timers. List both the service and socket in SYSTEMD_SERVICE.

bash
SYSTEMD_SERVICE:${PN} = "myapp.service myapp.socket"

7 Common Pitfalls and Fixes

These are the most frequent issues reported on Yocto mailing lists and forums.

1. systemd Not in DISTRO_FEATURES

The most common mistake. Even with inherit systemd in your recipe, if your distro configuration doesn't have systemd enabled, nothing works.

bash
# Check
bitbake-getvar DISTRO_FEATURES | grep systemd

# Fix: add to conf/local.conf
INIT_MANAGER = "systemd"

2. Missing WantedBy in [Install] Section

If the service file has no [Install] section or is missing WantedBy=, systemctl enable does nothing. No symlink is created, and the service won't auto-start.

ini
# Required
[Install]
WantedBy=multi-user.target

3. Missing :$ on SYSTEMD_SERVICE

bash
# Wrong (no package qualifier)
SYSTEMD_SERVICE = "myapp.service"

# Correct
SYSTEMD_SERVICE:${PN} = "myapp.service"

SYSTEMD_SERVICE is a per-package variable. Without :${PN}, the systemd bbclass can't correctly generate the postinst script.

4. Installing to /etc/systemd/system/

bash
# Wrong (/etc/ is for user overrides)
install -d ${D}/etc/systemd/system/

# Correct
install -d ${D}${systemd_system_unitdir}
# → /usr/lib/systemd/system/

/etc/systemd/system/ is where users locally override units. Units installed by packages belong in ${systemd_system_unitdir} (/usr/lib/systemd/system/).

5. Expecting Network Availability with network.target

ini
# network.target: indicates network devices are configured
# ≠ network is actually usable

# When you need actual network connectivity
[Unit]
After=network-online.target
Wants=network-online.target

network.target only means interfaces have been configured. It doesn't guarantee IP assignment or DNS resolution.

6. Execute Permissions on Service Files

bash
# Wrong (systemd may emit warnings)
install -m 0755 ${WORKDIR}/myapp.service ${D}${systemd_system_unitdir}/

# Correct
install -m 0644 ${WORKDIR}/myapp.service ${D}${systemd_system_unitdir}/

Service files are configuration files, not executables. 0644 is the correct permission.

7. journald Logs Lost on Reboot

By default, journald stores logs in RAM (/run/log/journal/). Logs disappear on reboot.

To persist logs, create the /var/log/journal/ directory in your recipe. You can also tune retention with journald.conf.

bash
do_install:append() {
    install -d ${D}${localstatedir}/log/journal
}
FILES:${PN} += "${localstatedir}/log/journal"

For embedded devices, consider Flash write frequency. Frequent log writes shorten Flash lifespan. Set limits with SystemMaxUse= and MaxRetentionSec= in journald.conf.

FAQ

Can I use systemd and sysvinit together in Yocto?

Yes. Scarthgap handles sysvinit as a DISTRO_FEATURES_BACKFILL_CONSIDERED item, so both can coexist. However, INIT_MANAGER = "systemd" disables sysvinit initscripts by default. If you need specific sysvinit scripts, add DISTRO_FEATURES:append = " sysvinit" back explicitly.

How do I check if my service is enabled on the target?

Run systemctl is-enabled myapp.service on the target device. If it returns enabled, the symlink under multi-user.target.wants/ exists. If it returns static, the service has no [Install] section and cannot be enabled.

What's the difference between systemd_system_unitdir and /etc/systemd/system/?

${systemd_system_unitdir} resolves to /usr/lib/systemd/system/ — this is where packages install unit files. /etc/systemd/system/ is for local administrator overrides. Units in /etc/ take precedence over those in /usr/lib/. In Yocto recipes, always use ${systemd_system_unitdir}.

Can I use inherit systemd in a bbappend file?

No. The inherit directive only works in .bb files. If the base recipe already has inherit systemd, your bbappend can set SYSTEMD_SERVICE:${PN} and add service files. If the base recipe doesn't inherit systemd, you'll need a different approach — see the bbappend guide for details.

How do I debug a service that fails to start?

On the target, run systemctl status myapp.service and journalctl -u myapp.service to see logs. During the build, check that the postinst script was generated with bitbake -e myapp | grep postinst. The build error debugging guide covers these tools in depth.

Does SYSTEMD_AUTO_ENABLE default to "enable"?

Yes. If you don't set SYSTEMD_AUTO_ENABLE:${PN}, it defaults to enable. Set it to disable if you want the service installed but not automatically started on boot.

How do I add a systemd service to a read-only rootfs?

With a read-only rootfs, systemctl enable during postinst won't work because /etc/ is not writable. Instead, pre-create the symlink in do_install() by linking your service file into ${D}${sysconfdir}/systemd/system/multi-user.target.wants/. This is a common pattern for OTA update targets where the rootfs is immutable.

Wrapping Up

Getting systemd services to work in Yocto requires three things.

  1. Distro configuration: Enable with INIT_MANAGER = "systemd"
  2. Recipe: inherit systemd and set SYSTEMD_SERVICE:${PN}
  3. Service file: Include WantedBy=multi-user.target in the [Install] section

Timer and socket activation let you integrate periodic tasks and on-demand startup into systemd as well.

Most pitfalls come down to missing configuration. When things don't work, start by checking DISTRO_FEATURES with bitbake-getvar and running systemctl status myapp on the target. The build error debugging guide covers these tools in detail.

For a deeper dive into systemd recipes and Yocto recipe development, these books are solid references.

Related articles: