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.
# conf/local.conf
INIT_MANAGER = "systemd"
This one line loads init-manager-systemd.inc, which automatically configures the following.
# 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
# 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
# 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
| Variable | Purpose | Default |
|---|---|---|
SYSTEMD_SERVICE:${PN} | Service file name(s) to manage | ${PN}.service |
SYSTEMD_AUTO_ENABLE:${PN} | Whether to auto-enable | enable |
SYSTEMD_PACKAGES | Packages to manage with systemd | ${PN} |
What inherit systemd does automatically:
- Generates a
postinstscript that runssystemctl enable - Creates symlinks under
multi-user.target.wants/ - Generates a
prermscript for package removal
Guarding Your Recipe for systemd
To prevent build errors on distros without systemd, use REQUIRED_DISTRO_FEATURES.
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
# 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.
| Type | Use Case |
|---|---|
simple | Foreground process (default) |
forking | Daemon that forks into the background |
oneshot | Script 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.
# 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.
# myapp.timer
[Unit]
Description=Run myapp periodically
[Timer]
OnBootSec=1min
OnUnitActiveSec=1h
Persistent=true
[Install]
WantedBy=timers.target
# 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.
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.
# myapp.socket
[Unit]
Description=My Application Socket
[Socket]
ListenStream=8080
Accept=no
[Install]
WantedBy=sockets.target
# 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.
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.
# 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.
# Required
[Install]
WantedBy=multi-user.target
3. Missing :$ on SYSTEMD_SERVICE
# 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/
# 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
# 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
# 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.
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.
- Distro configuration: Enable with
INIT_MANAGER = "systemd" - Recipe:
inherit systemdand setSYSTEMD_SERVICE:${PN} - Service file: Include
WantedBy=multi-user.targetin 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:
- How to Write Yocto Recipes: A .bb File Guide
- Yocto bbappend: A Practical Guide with 5 Patterns
- Getting Started with Yocto: Build Your First Linux Image
- Yocto Build Errors: Task-by-Task Debugging Guide
- Raspberry Pi 5 with Yocto Scarthgap: A Practical Guide
- Yocto OTA Update: SWUpdate vs RAUC vs Mender
- Yocto Build Speed Optimization Guide
- Yocto Custom Layer Guide