You've built your application with Yocto. Now you need it to start automatically on boot. In embedded Linux, that means creating a systemd service.
But getting systemd services to work correctly in Yocto requires three things to align: the distro configuration, the recipe, and the service file. If any one of them is missing, your service silently fails to start.
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.
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.
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.