| |

LFCS 62 ๐Ÿง systemd โ€” Unit Files

โฑ๏ธ Estimated read: 14 min | |

A systemd unit file is not a shell script with a shebang. It is an INI-format configuration file that declares a resource’s metadata, dependencies, execution model, and installation target. The LFCS exam tests this distinction through scenarios that require creating a service from scratch, overriding a package-provided unit without editing the package’s file, and understanding which section controls what behavior. The unit file is the contract between the administrator and systemd: systemd reads the file, builds a dependency graph from it, and executes the directives in the order the sections dictate.

The three sections that matter for service units are [Unit], [Service], and [Install]. [Unit] carries generic metadata and dependenciesโ€”Description, After, Requires, Wants . [Service] carries the execution modelโ€”Type, ExecStart, Restart, User . [Install] carries the installation target that systemctl enable readsโ€”WantedBy, RequiredBy, Alias . The [Install] section is not read during runtime; it is read only by enable and disable to create or remove symlinks .

The exam’s second layer is the override mechanism. Package-installed units live in /usr/lib/systemd/system. Administrator overrides belong in /etc/systemd/system, either as a full unit file or as a drop-in fragment in a .d/ directory . The systemctl edit command creates the drop-in and runs daemon-reload automatically, which is the safe way to change a unit without losing the change to a package update .

This chapter covers the unit file format and search path, the [Unit], [Service], and [Install] sections and their tested directives, the Type= values and when each applies, the drop-in override mechanism, and the exam scenarios that distinguish a working unit from a persistent one. It assumes familiarity with systemctl service control from the previous chapter.

Key point: A unit file is read in sections. [Unit] declares dependencies and ordering, [Service] declares execution, and [Install] declares the boot target that enable uses. Editing a package’s unit in /usr/lib is the wrong answer; systemctl edit creates a drop-in in /etc/systemd/system/<unit>.d/ that survives package updates.


Why unit files are an exam domain

The exam tests creation, not just control. Starting and stopping a service is a systemctl command. Creating a service requires writing a unit file, reloading the daemon, and enabling it. The LFCS performance-based format includes tasks like “configure a service to start automatically” and “create a custom service unit” . The candidate must know where the file goes, which sections are mandatory, and which directive makes enable work.

The [Install] section is the enable contract. systemctl enable reads the [Install] section and creates symlinks based on WantedBy= or RequiredBy= . A unit file without an [Install] section can be started manually but cannot be enabled. The exam may ask why systemctl enable fails on a custom unit, and the answer is almost always a missing [Install] section or a missing WantedBy target.

The drop-in mechanism separates correct administration from risky administration. Directly editing /usr/lib/systemd/system/nginx.service works until the next package update overwrites the file. The correct pattern is systemctl edit nginx.service, which creates /etc/systemd/system/nginx.service.d/override.conf and ensures the override takes precedence . The exam tests this because production administrators must not lose configuration changes to package updates.

The Type= directive determines how systemd tracks the service. Type=simple means the process started by ExecStart is the main process and stays in the foreground. Type=forking means the process forks into the background and systemd must track the child. Type=oneshot is for scripts that run once and exit, with RemainAfterExit=yes to keep the unit active . Choosing the wrong type produces a service that systemd thinks is dead while the process is still running, or vice versa.

The trade-off. A unit file is more verbose than a service command, but it encodes dependencies, restart policy, resource limits, and logging configuration in a declarative format. The exam tests the verbosity because production systems depend on it.


a. The [Unit] section: metadata, dependencies, and ordering

The [Unit] section is generic across unit types. For a service, the tested directives are Description, After, Before, Requires, and Wants. Description is a free-form string shown in status output and UI tools . After and Before control ordering: if foo.service has After=network.target, systemd starts foo after network.target is reached . Ordering does not imply dependency; it only controls sequence.

Requires and Wants control dependency. Requires=bar.service means if bar fails to start, foo is not started, and if bar stops, foo stops . Wants is the weaker form: if bar fails, foo still starts . The recommendation is to use Wants for most dependencies, because Requires creates a hard failure chain that can cascade .

[Unit]
Description=My Web Application
After=network.target
Wants=network-online.target

The After=network.target line is the most commonly tested ordering directive. A service that binds to a network port should not start before networking is available. network.target is reached when the network stack is configured, not necessarily when an address is assigned; network-online.target is the stronger wait, but it requires the network manager’s wait-online service to be enabled .


b. The [Service] section: execution, restart, and user

The [Service] section is where the service’s actual behavior is defined. The mandatory directive is ExecStart, which specifies the command to run. The Type= directive tells systemd how to interpret the process model: simple (default) for foreground processes, forking for daemons that background themselves, oneshot for scripts, notify for services that signal readiness via sd_notify .

[Service]
Type=simple
ExecStart=/usr/local/bin/my_app
Restart=always
RestartSec=5
User=appuser
WorkingDirectory=/opt/my_app

Restart=always configures systemd to restart the process if it exits, regardless of exit code. RestartSec=5 waits five seconds before restarting, preventing a rapid crash loop . User=appuser runs the service as a non-root user, which is a security requirement for most applications . WorkingDirectory sets the process’s working directory before ExecStart runs.

The Type= choice is the most common source of confusion. A Type=simple service that forks into the background will appear to have exited immediately, and systemd will mark the unit as inactive (dead) while the process is still running. The fix is either to use Type=forking with PIDFile= or to configure the application to run in the foreground, which is the preferred approach for systemd .


c. The [Install] section and the drop-in override

The [Install] section is read only by systemctl enable and disable. It does not affect runtime behavior. The WantedBy=multi-user.target directive tells enable to create a symlink in /etc/systemd/system/multi-user.target.wants/ pointing to the unit file . When the system reaches multi-user.target during boot, systemd starts the unit because it is in the target’s wants directory.

[Install]
WantedBy=multi-user.target

Without this section, systemctl enable my_app.service returns an error. The unit can still be started manually with systemctl start, but it will not start at boot. The exam may present a unit file with no [Install] section and ask why the service does not start after reboot.

The drop-in override is the correct way to modify an existing unit. systemctl edit nginx.service opens an editor for /etc/systemd/system/nginx.service.d/override.conf . The drop-in contains only the directives being changed, not the full unit file. systemd merges the drop-in with the original unit, with the drop-in taking precedence.

# /etc/systemd/system/nginx.service.d/override.conf
[Service]
Restart=on-failure
RestartSec=10

The systemctl edit command runs daemon-reload automatically after saving. If the drop-in is created manually, systemctl daemon-reload must be run before the change takes effect. The systemctl cat <unit> command shows the merged configuration, including the base unit and all drop-ins, which is the verification step after an override .


Complete Example Session

# ============================================
# PART 1: A COMPLETE CUSTOM SERVICE UNIT
# ============================================
# File: /etc/systemd/system/my_app.service

[Unit]
Description=My Python Web Application
After=network.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/bin/python3 /opt/my_app/app.py
Restart=always
RestartSec=5
User=appuser
WorkingDirectory=/opt/my_app

[Install]
WantedBy=multi-user.target

# ============================================
# PART 2: SECTION-BY-SECTION BREAKDOWN
# ============================================
# [Unit]
#   Description โ†’ shown in systemctl status.
#   After=network.target โ†’ start after network is up.
#   Wants=network-online.target โ†’ weak dependency.
#
# [Service]
#   Type=simple โ†’ foreground process.
#   ExecStart โ†’ the command to run.
#   Restart=always โ†’ restart on any exit.
#   RestartSec=5 โ†’ wait 5s before restart.
#   User=appuser โ†’ run as non-root.
#   WorkingDirectory โ†’ process CWD.
#
# [Install]
#   WantedBy=multi-user.target โ†’ enable creates symlink.
#   This is what makes boot persistence work.

# ============================================
# PART 3: CREATE, RELOAD, ENABLE, START
# ============================================
# Create the file.
# sudo tee /etc/systemd/system/my_app.service <<'EOF'
# [Unit]
# Description=My App
# After=network.target
#
# [Service]
# ExecStart=/usr/bin/python3 /opt/my_app/app.py
# Restart=always
#
# [Install]
# WantedBy=multi-user.target
# EOF

# Reload systemd's cache to see the new unit.
sudo systemctl daemon-reload

# Enable and start.
sudo systemctl enable --now my_app

# Verify both states.
systemctl is-active my_app    # active
systemctl is-enabled my_app   # enabled

# ============================================
# PART 4: THE DROP-IN OVERRIDE
# ============================================
# Do not edit /usr/lib/systemd/system/nginx.service.
# Use systemctl edit instead.
sudo systemctl edit nginx.service

# The editor opens:
# /etc/systemd/system/nginx.service.d/override.conf
#
# Add only the directives being changed:
# [Service]
# Restart=on-failure
# RestartSec=10
#
# Save and exit. systemctl edit runs daemon-reload
# automatically. Restart the service:
sudo systemctl restart nginx

# ============================================
# PART 5: VERIFY THE MERGED CONFIGURATION
# ============================================
# systemctl cat shows base unit + all drop-ins.
systemctl cat nginx.service

# Output includes:
# # /usr/lib/systemd/system/nginx.service  (base)
# # /etc/systemd/system/nginx.service.d/override.conf  (drop-in)

# ============================================
# PART 6: TYPE= FORKING FOR DAEMONS
# ============================================
# A service that backgrounds itself needs Type=forking.
# [Service]
# Type=forking
# ExecStart=/usr/sbin/mydaemon
# PIDFile=/run/mydaemon.pid
#
# Without Type=forking, systemd thinks the service
# exited when the parent process returns.

# ============================================
# PART 7: TYPE=ONESHOT FOR SCRIPTS
# ============================================
# A script that runs once and exits.
# [Service]
# Type=oneshot
# ExecStart=/usr/local/bin/setup.sh
# RemainAfterExit=yes
#
# RemainAfterExit keeps the unit active after exit,
# so systemctl is-active returns "active".

# ============================================
# PART 8: WHY ENABLE FAILS WITHOUT [INSTALL]
# ============================================
# A unit with no [Install] section:
# sudo systemctl enable my_app
# โ†’ "The unit files have no installation config"
#
# Fix: add [Install] with WantedBy=multi-user.target.

# ============================================
# PART 9: THE UNIT FILE SEARCH PATH
# ============================================
# /etc/systemd/system       โ†’ admin overrides (highest)
# /run/systemd/system       โ†’ runtime, temporary
# /usr/lib/systemd/system   โ†’ package-installed
#
# A unit in /etc overrides one in /usr/lib.
# Drop-ins in /etc/systemd/system/<unit>.d/ merge
# with the base unit, with drop-in values winning.

# ============================================
# PART 10: THE EXAM CHECKLIST
# ============================================
# 1. Unit file in /etc/systemd/system/<name>.service.
# 2. Has [Unit], [Service], [Install].
# 3. ExecStart points to an executable.
# 4. [Install] has WantedBy=multi-user.target.
# 5. sudo systemctl daemon-reload after creation.
# 6. sudo systemctl enable --now <name>.
# 7. Verify: is-active and is-enabled.
# 8. For overrides: systemctl edit, not direct edit.

Quick Reference

Unit File Sections

SectionPurposeKey Directives
[Unit]Metadata, dependenciesDescription, After, Wants, Requires
[Service]Execution modelType, ExecStart, Restart, User
[Install]Enable targetWantedBy, RequiredBy, Alias

Service Type= Values

TypeMeaningUse When
simpleForeground process (default)Most applications
forkingProcess backgrounds itselfLegacy daemons
oneshotRuns once and exitsSetup scripts
notifySignals readiness via sd_notifyModern services

Dependency Directives

DirectiveEffectFailure Behavior
RequiresHard dependencyUnit fails if dependency fails
WantsWeak dependencyUnit starts regardless
AfterOrdering onlyNo dependency implied
BeforeReverse orderingNo dependency implied

Unit File Locations

PathPurposePriority
/etc/systemd/systemAdmin overridesHighest
/run/systemd/systemRuntime, temporaryMiddle
/usr/lib/systemd/systemPackage-installedLowest

Best Practices

โœ… Do This:

# Create custom units in /etc/systemd/system.                  // โœ…
# /etc/systemd/system/my_app.service

# Include [Install] with WantedBy=multi-user.target.           // โœ…
[Install]
WantedBy=multi-user.target

# Use Restart=always with RestartSec for stability.            // โœ…
Restart=always
RestartSec=5

# Override with systemctl edit, not direct edits.              // โœ…
sudo systemctl edit nginx.service

# Run daemon-reload after creating or editing units.           // โœ…
sudo systemctl daemon-reload

โŒ Don’t Do This:

# Edit /usr/lib/systemd/system/nginx.service directly.         // โŒ
# Changes lost on package update.

# Omit [Install] and expect enable to work.                    // โŒ
# enable fails without an install target.

# Use Type=simple for a daemon that backgrounds itself.        // โŒ
# systemd marks the unit dead while the process runs.

# Run services as root without a specific reason.              // โŒ
User=appuser  // Use a non-root user.

Common Pitfalls

PitfallWhy It HappensFix
enable failsMissing [Install] sectionAdd WantedBy=multi-user.target
Service marked dead but runningType=simple with a forking daemonUse Type=forking or run in foreground
Override lost after updateEdited /usr/lib unit directlyUse systemctl edit drop-in
New unit not founddaemon-reload not runsystemctl daemon-reload
Service starts before networkMissing After=network.targetAdd ordering directive
Oneshot unit shows inactiveNo RemainAfterExit=yesAdd it to keep unit active

Real-World Examples

1. Minimal Custom Unit

[Unit]
Description=My App
[Service]
ExecStart=/usr/bin/myapp
[Install]
WantedBy=multi-user.target

2. Dependency on Network

[Unit]
After=network.target
Wants=network-online.target

3. Restart Policy

[Service]
Restart=always
RestartSec=5

4. Non-Root User

[Service]
User=appuser
WorkingDirectory=/opt/app

5. Drop-In Override

sudo systemctl edit nginx.service

6. Verify Merged Config

systemctl cat nginx.service

7. Forking Daemon

[Service]
Type=forking
PIDFile=/run/mydaemon.pid

8. Oneshot Script

[Service]
Type=oneshot
RemainAfterExit=yes

9. Reload After Edit

sudo systemctl daemon-reload

10. Enable and Start

sudo systemctl enable --now my_app

Visual

Unit File Sections and Lifecycle

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  UNIT FILE ANATOMY                           โ”‚
โ”‚                                              โ”‚
โ”‚  [Unit]                                      โ”‚
โ”‚    Description, After, Wants                 โ”‚
โ”‚         โ”‚                                    โ”‚
โ”‚         โ”‚ read at load time                  โ”‚
โ”‚         โ–ผ                                    โ”‚
โ”‚  [Service]                                   โ”‚
โ”‚    Type, ExecStart, Restart, User            โ”‚
โ”‚         โ”‚                                    โ”‚
โ”‚         โ”‚ read at start time                 โ”‚
โ”‚         โ–ผ                                    โ”‚
โ”‚  [Install]                                   โ”‚
โ”‚    WantedBy=multi-user.target                โ”‚
โ”‚         โ”‚                                    โ”‚
โ”‚         โ”‚ read ONLY by enable/disable        โ”‚
โ”‚         โ–ผ                                    โ”‚
โ”‚  systemctl enable creates symlink            โ”‚
โ”‚                                              โ”‚
โ”‚  Runtime never reads [Install].              โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Drop-In Override Precedence

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  MERGE ORDER                                 โ”‚
โ”‚                                              โ”‚
โ”‚  /usr/lib/systemd/system/nginx.service       โ”‚
โ”‚    (package base, lowest priority)           โ”‚
โ”‚         โ”‚                                    โ”‚
โ”‚         โ–ผ                                    โ”‚
โ”‚  /run/systemd/system/nginx.service           โ”‚
โ”‚    (runtime, middle)                         โ”‚
โ”‚         โ”‚                                    โ”‚
โ”‚         โ–ผ                                    โ”‚
โ”‚  /etc/systemd/system/nginx.service           โ”‚
โ”‚    (admin full unit, high)                   โ”‚
โ”‚         โ”‚                                    โ”‚
โ”‚         โ–ผ                                    โ”‚
โ”‚  /etc/systemd/system/nginx.service.d/*.conf  โ”‚
โ”‚    (drop-ins, highest)                       โ”‚
โ”‚                                              โ”‚
โ”‚  systemctl cat shows the merged result.      โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Type= Decision

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  WHICH TYPE?                                 โ”‚
โ”‚                                              โ”‚
โ”‚  Runs in foreground, stays running?          โ”‚
โ”‚       โ””โ”€โ”€ Type=simple (default)              โ”‚
โ”‚                                              โ”‚
โ”‚  Forks into background, writes PID file?     โ”‚
โ”‚       โ””โ”€โ”€ Type=forking + PIDFile=            โ”‚
โ”‚                                              โ”‚
โ”‚  Runs once, exits, should stay "active"?     โ”‚
โ”‚       โ””โ”€โ”€ Type=oneshot + RemainAfterExit=yes โ”‚
โ”‚                                              โ”‚
โ”‚  Signals readiness via sd_notify?            โ”‚
โ”‚       โ””โ”€โ”€ Type=notify                        โ”‚
โ”‚                                              โ”‚
โ”‚  Wrong type โ†’ status lies about state.       โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Why Enable Fails

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  ENABLE REQUIRES [Install]                   โ”‚
โ”‚                                              โ”‚
โ”‚  Unit with [Install]:                        โ”‚
โ”‚    WantedBy=multi-user.target                โ”‚
โ”‚         โ”‚                                    โ”‚
โ”‚         โ–ผ                                    โ”‚
โ”‚    systemctl enable creates:                 โ”‚
โ”‚    /etc/systemd/system/multi-user.target.wants/โ”‚
โ”‚      โ””โ”€โ”€ my_app.service โ†’ /etc/.../my_app.serviceโ”‚
โ”‚                                              โ”‚
โ”‚  Unit without [Install]:                     โ”‚
โ”‚    systemctl enable โ†’ error                  โ”‚
โ”‚    "no installation config"                  โ”‚
โ”‚                                              โ”‚
โ”‚  Fix: add [Install] with WantedBy=target.    โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
Unit file location/etc/systemd/system/<name>.service
FormatINI, sections in brackets
[Unit]Metadata, After, Wants, Requires
[Service]Type, ExecStart, Restart, User
[Install]WantedBy, read only by enable
Type=simpleForeground process (default)
Type=forkingDaemon that backgrounds itself
Type=oneshotScript that runs once
Drop-in overridesystemctl edit, creates .d/override.conf
Verificationsystemctl cat shows merged config
Reloaddaemon-reload after creation or edit

Key takeaways:

  • A unit file is read in sections, and [Install] is read only by enable. The [Unit] and [Service] sections control runtime behavior; the [Install] section controls boot persistence. A unit without [Install] cannot be enabled .
  • Type= determines how systemd tracks the process. simple for foreground, forking for daemons that background, oneshot for scripts. The wrong type produces a status that disagrees with reality .
  • Override package units with systemctl edit, not direct edits. The drop-in in /etc/systemd/system/<unit>.d/ takes precedence and survives package updates. systemctl cat shows the merged result .
  • After= is ordering, not dependency. It controls sequence, not failure propagation. Wants= is the weak dependency for most cases; Requires= is the hard dependency for critical chains .
  • daemon-reload is mandatory after any unit change. systemd caches unit contents. Without the reload, the new or changed unit is invisible to systemctl start and enable .
  • The [Install] target determines when the service starts at boot. WantedBy=multi-user.target is the standard for services that should start in normal non-graphical boot. A different target means a different boot phase.

Remember: A systemd unit file is a declarative contract in three sections. [Unit] says what the service is and what it depends on. [Service] says how to run it. [Install] says when to start it at boot. The exam tests all three: creating the file in the right location, choosing the right Type=, adding the [Install] section that makes enable work, and using systemctl edit instead of editing package files. The drop-in mechanism is the production-safe pattern: it separates administrator changes from package-provided defaults, and it survives the next apt upgrade. A unit file without [Install] runs now and disappears after reboot. A unit file with it persists. The exam grader reboots.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!