Signal9Signal9 — home
online
all lessons

Modify a Service the Right Way: Drop-in Overrides

Intermediate30 min6 steps
What you'll learn
  • Explain why editing the vendor unit under /usr/lib/systemd/system is the wrong place to make a change
  • Create a drop-in override at /etc/systemd/system/<unit>.service.d/*.conf and describe how systemd merges it over the base unit
  • Use systemctl edit (and edit --full) to author an override, and know what each writes
  • Reset a list-valued directive such as ExecStart with the empty-then-set idiom before changing it
  • Apply a unit change on a live host with daemon-reload + restart and verify it with systemctl cat / systemctl show — and validate the unit file offline with systemd-analyze verify where there is no service manager
  1. Two directories, and why it matters which one you touch

    Understand that vendor units and your overrides live in different places on purpose.

    Every service is described by a unit file, and those files live in two different trees that systemd treats very differently.

    • `/usr/lib/systemd/system/` (also /lib/...) holds the vendor units — the ones shipped by the package that installed the service. This directory belongs to the package manager. When the package updates, everything here is overwritten.
    • `/etc/systemd/system/` holds the administrator's configuration — your changes. systemd gives this tree higher priority, and the package manager never touches it.

    So the golden rule is: never edit a file under `/usr/lib/systemd/system/`. Two things go wrong if you do. First, the next dnf/apt update silently reverts your change, and you get to debug why a setting you 'definitely made' has vanished. Second, you have now forked the vendor's file, so you will not receive their future fixes to it either. The supported way to change a service is to put your change in /etc/systemd/system/ instead — and the neatest form of that is a drop-in.

    systemd-analyze unit-paths | grep -Ex '/etc/systemd/system|/usr/lib/systemd/system'
    /etc/systemd/system
    /usr/lib/systemd/system
    systemd-analyze unit-paths prints every directory systemd loads units from, highest priority first, and it works without a running service manager. The full list has a dozen entries (runtime and generator directories under /run, and /usr/local/lib/systemd/system for software you install by hand); the grep keeps the two this lesson is about. /etc/systemd/system comes first, so anything there wins over the same name in /usr/lib/systemd/system. That ordering is the whole reason your changes go in /etc.
    cat > /usr/lib/systemd/system/webapp.service <<'EOF'
    [Unit]
    Description=Demo web app (stands in for a packaged service)
    
    [Service]
    ExecStart=/usr/bin/python3 -m http.server 8000
    
    [Install]
    WantedBy=multi-user.target
    EOF
    There is no webapp package on this box, so for one command you play the package manager: this is the file an RPM or a .deb would install into the vendor tree. It is the only write to /usr/lib/systemd/system/ in this lesson, and it exists so there is a vendor file to leave alone; everything from here on goes in /etc. The service runs Python's built-in web server on port 8000. Python is on both lab images, which matters because systemd-analyze verify checks that ExecStart= names a real program.

    Example — not runnable on your lab box

    systemctl cat webapp
    # /usr/lib/systemd/system/webapp.service
    [Unit]
    Description=Demo web app (stands in for a packaged service)
    
    [Service]
    ExecStart=/usr/bin/python3 -m http.server 8000
    
    [Install]
    WantedBy=multi-user.target
    Real-host reference — the lab box can't answer this (see the note above). On a machine that booted systemd, systemctl cat prints the unit systemd is actually using, with a comment header naming which file on disk it came from, and appends every drop-in below it — the whole merged picture in one place. It is the first thing to run before you override anything. Here, cat /usr/lib/systemd/system/webapp.service shows the same vendor file without the header.
  2. The drop-in: a small patch on top of the unit

    See a drop-in directory and understand systemd's merge, so you change one directive without copying the whole unit.

    A drop-in is a partial unit file placed in a directory named after the unit with a .d suffix: /etc/systemd/system/<unit>.service.d/. Any file ending in .conf inside it is a drop-in. You do not repeat the whole unit — you write only the [Section] and the one or two directives you want to change.

    Here is the model to hold in your head: at load time systemd reads the base vendor unit first, then reads every `.conf` in the `.d` directory and layers each directive on top, last value winning. Your drop-in is a patch, not a replacement. That is why it survives package updates cleanly (the vendor file underneath can change and your patch still applies) and why it is the RHCSA/LFCS-preferred way to tune a service. A one-line file that sets Restart=always is a complete, correct override — everything else about the service keeps coming from the vendor unit.

    mkdir -p /etc/systemd/system/webapp.service.d
    cat > /etc/systemd/system/webapp.service.d/override.conf <<'EOF'
    [Service]
    Restart=always
    RestartSec=3
    EOF
    This is a drop-in by hand, and it runs here exactly as written — it only touches files. Make the <unit>.service.d directory, then write a small .conf with just the [Service] section and the two directives we want to add. Note what is absent: no ExecStart, no Description, no [Unit] or [Install]. Those all still come from the vendor unit you installed in step 1; we are only layering Restart=always and RestartSec=3 on top. The filename (override.conf) is conventional — any *.conf in that directory is read.
    cat /etc/systemd/system/webapp.service.d/override.conf
    [Service]
    Restart=always
    RestartSec=3
    Read back what you just wrote. This is the entire override — three lines. Nothing on this box merges it into a running service, but on a live host these three lines are what systemd layers over the vendor unit at its next daemon-reload, and they are what a grader reads.
    systemd-analyze verify /usr/lib/systemd/system/webapp.service && echo 'webapp.service: verify passed'
    webapp.service: verify passed
    systemd-analyze verify loads the unit the way systemd would: the vendor file first, then every .conf in webapp.service.d/ on top. It reports anything in the merged result it can't use, and it needs no service manager, so it runs here. No complaints followed by the echo means vendor unit plus drop-in load cleanly. You give it the vendor file's path; it finds the drop-ins itself, by the unit's name.
  3. systemctl edit does the plumbing for you

    Use the tool that creates the drop-in, and know the difference between edit and edit --full.

    Creating the .d directory by hand works and is worth doing once to see the mechanism, but the everyday tool is `systemctl edit`. Running systemctl edit webapp opens your $EDITOR on a fresh, empty drop-in, and on save it does three chores for you: it creates the <unit>.service.d/ directory, writes your text into override.conf, and runs daemon-reload so the change is picked up. You type only the section and directives — the same small patch as before.

    There is a second form, `systemctl edit --full`. Instead of a drop-in, this copies the entire vendor unit into /etc/systemd/system/ and opens that for editing. The copy shadows the vendor file completely. Reach for --full only when you genuinely need to rewrite most of the unit (or change something a drop-in cannot cleanly patch); for a directive or two, the plain drop-in is lighter and keeps you riding the vendor's future updates. Whichever you use, the change lands under /etc/, never in /usr/lib/.

    On this box systemctl edit cannot run. On Ubuntu it wants a bus and an interactive tty, and answers Cannot edit units if not on a tty.; on Rocky the stand-in systemctl answers unsupported verb 'edit'. That costs you nothing, because what it writes is precisely the file you wrote by hand in the last step — edit is a convenience wrapper, not a different mechanism. Author the drop-in by hand here; reach for edit on a real host.

    Example — not runnable on your lab box

    systemctl edit webapp
    (opens $EDITOR on /etc/systemd/system/webapp.service.d/override.conf;
     saving runs `systemctl daemon-reload` for you)
    Real-host reference — this exits 1 on the lab box (no service manager, and no tty). It is the interactive way to write the same drop-in as the previous step: systemctl edit webapp gives you an empty override to fill in with just [Service] + your directives, then creates the directory, saves override.conf, and runs daemon-reload for you. systemctl edit --full webapp would instead hand you a full copy of the unit at /etc/systemd/system/webapp.service. Both put your change in the administrator tree where package updates cannot clobber it.
  4. The one trap: resetting a list before you set it

    Change ExecStart (or any list-valued directive) correctly with the empty-then-set idiom.

    Most directives are single-valued: put Restart=always in a drop-in and it simply replaces whatever the vendor set. But a handful of directives are list-valued — ExecStart, ExecStartPre, Environment, After, and friends — and these accumulate rather than replace. If the vendor unit has ExecStart=/usr/bin/python3 -m http.server 8000 and your drop-in adds ExecStart=/usr/bin/python3 -m http.server 8080 --bind 127.0.0.1, systemd does not swap one for the other; for ExecStart specifically it now sees two start commands and refuses to load, and for the additive ones you get both entries.

    The idiom that fixes this is reset-then-set: assign the directive empty first to clear the inherited list, then assign the value you actually want. For ExecStart that is two lines — an empty ExecStart= followed by the real one. It looks odd the first time, but it is the documented way to say 'forget what the vendor put here, use this instead.' Whenever an override of a list directive behaves like it was added rather than replaced, this is the reason.

    cat > /etc/systemd/system/webapp.service.d/override.conf <<'EOF'
    [Service]
    ExecStart=/usr/bin/python3 -m http.server 8080 --bind 127.0.0.1
    EOF
    systemd-analyze verify /usr/lib/systemd/system/webapp.service 2>&1 | grep webapp
    webapp.service: Service has more than one ExecStart= setting, which is only allowed for Type=oneshot services. Refusing.
    Unit webapp.service has a bad unit file setting.
    First the mistake, on purpose: a drop-in that moves the server to port 8080 on localhost but sets the new ExecStart= without clearing the old one. verify merges it over the vendor unit, finds two start commands and refuses the unit. A live host refuses it the same way at daemon-reload, and the service will not start. 2>&1 | grep webapp keeps only the lines about this unit: verify also loads every unit webapp depends on, and on a container box those can print notes that are not yours.
    cat > /etc/systemd/system/webapp.service.d/override.conf <<'EOF'
    [Service]
    ExecStart=
    ExecStart=/usr/bin/python3 -m http.server 8080 --bind 127.0.0.1
    EOF
    The fix. The empty ExecStart= line clears the vendor's start command; the second line sets the one you want. Without the empty line first, systemd sees two ExecStart= entries and refuses the unit, as the previous block showed. Single-valued directives like Restart= or MemoryMax= need no such reset — only the list-valued ones do.
    cat /etc/systemd/system/webapp.service.d/override.conf
    [Service]
    ExecStart=
    ExecStart=/usr/bin/python3 -m http.server 8080 --bind 127.0.0.1
    The empty line really does stay in the file — it is a directive, not a typo, and deleting it is how the two-ExecStart failure comes back. Note also that this rewrote the file: the Restart=always from step 2 is gone, because a heredoc > replaces the whole drop-in. To keep both, put them in one file together, or add a second .conf (e.g. 10-restart.conf and 20-exec.conf) — systemd reads them in filename order.
    systemd-analyze verify /usr/lib/systemd/system/webapp.service && echo 'webapp.service: verify passed'
    webapp.service: verify passed
    Same check, same file, and now it passes: one ExecStart, the one from your drop-in. This is the loop to build — write the drop-in, verify, read what it says — and it is the same check the systemd labs run on your work.
  5. Apply it, then prove it took

    Reload, restart, and verify the effective configuration instead of assuming.

    Editing a file on disk changes nothing that is already running — systemd has the old unit cached in memory. Two verbs bridge that gap, and they are not the same thing. `systemctl daemon-reload` re-reads unit files from disk so systemd knows about your edit; you run it after any change to a unit or drop-in (and systemctl edit runs it for you). `systemctl restart <unit>` then stops and starts the service so a changed ExecStart, resource limit, or environment actually takes effect on the running process. (reload — lowercase, no daemon- — is different again: it asks the service to re-read its own config, like nginx re-reading nginx.conf, and only works if the unit defines an ExecReload.)

    Then verify instead of hoping. systemctl cat <unit> shows the base unit with your drop-in appended, so you can confirm the file is where you meant it. systemctl show -p <Directive> <unit> prints the single effective value systemd computed after merging everything — the ground truth. Checking the effective value is how you catch a drop-in that landed in the wrong directory, or a list directive you forgot to reset.

    Neither daemon-reload nor show does anything useful on this box. On Ubuntu both need the bus; on Rocky the stand-in systemctl accepts daemon-reload and does nothing, and does not know show. What you can do here is the check that catches most override mistakes before they ever reach a service manager: parse the unit file offline with systemd-analyze verify. It reads unit files straight from disk, needs no PID 1, and is exactly what this platform's systemd rooms grade you on.

    Example — not runnable on your lab box

    systemctl daemon-reload && systemctl restart webapp
    Real-host reference — not meaningful on the lab box. On Ubuntu it exits 1 (no bus). On Rocky the stand-in systemctl accepts it, but its restart runs the vendor ExecStart= directly and ignores drop-ins, so it proves nothing about your override. On a live host, daemon-reload makes systemd re-read the unit and your drop-in from disk, and restart applies the change to the running process. You run both after writing an override by hand (with systemctl edit, the daemon-reload half is already done for you, but you still restart to apply a runtime change like a new ExecStart).

    Example — not runnable on your lab box

    systemctl show -p Restart -p ExecStart webapp
    Restart=no
    ExecStart={ path=/usr/bin/python3 ; argv[]=/usr/bin/python3 -m http.server 8080 --bind 127.0.0.1 ; … }
    Real-host reference — the lab box can't answer this (no bus on Ubuntu; unsupported verb 'show' on Rocky). On a live host these are the effective, post-merge values, and the real test that your override took. A single ExecStart (not two) confirms the empty-then-set reset worked. Restart=no is the surprise show exists to catch: step 4's > rewrote override.conf and dropped the Restart=always you wrote in step 2. If show disagrees with what you thought you wrote, believe show — then look for a drop-in in the wrong directory, a list directive that was not reset, or a file you overwrote.
    printf '%s\n' '[Service]' 'RestartSecs=3' > /etc/systemd/system/webapp.service.d/restart.conf
    systemd-analyze verify /usr/lib/systemd/system/webapp.service 2>&1 | grep webapp
    /etc/systemd/system/webapp.service.d/restart.conf:2: Unknown key name 'RestartSecs' in section 'Service', ignoring.
    The verification you can run here, and the mistake nothing else on this box will show you. A second file in the .d directory, restart.conf, is read too (drop-ins load in filename order, so after override.conf), and it has a typo: RestartSecs for RestartSec. verify names the file, the line and the key. Note the word ignoring: a misspelt key is not fatal, the unit still loads and verify still exits 0, and the service simply runs without the setting. So read what verify prints instead of trusting its exit code. systemd-service-capstone grades with this exact tool.
    rm -rf /etc/systemd/system/webapp.service.d /usr/lib/systemd/system/webapp.service
    Tidy up: remove the drop-in directory and the demo vendor unit so nothing from this lesson is left on the box. On a live host you remove a drop-in the same way, then daemon-reload so systemd forgets it.

    Check yourself

    You need a service to always restart if it crashes. Where do you put the Restart=always directive?

  6. The two mistakes that cost the most time

    Avoid the vendor-file edit and the forgotten daemon-reload.

    Every drop-in in this lesson changed what a service does. The same mechanism is how you change what a service may touch: the Sandbox a systemd Service lesson (systemd-sandboxing-guide) puts ProtectSystem=, NoNewPrivileges=, CapabilityBoundingSet= and the other confinement directives in a drop-in, and measures the result with systemd-analyze security --offline=true.

Recap
  • Vendor units live in /usr/lib/systemd/system/ and are overwritten by package updates; your changes belong in /etc/systemd/system/, which systemd prioritizes and packages never touch. Never edit the vendor file.
  • A drop-in is a partial unit at /etc/systemd/system/<unit>.service.d/*.conf: write only the [Section] and directives you want, and systemd layers them over the base unit (last value wins). systemctl edit <unit> creates it and runs daemon-reload for you; edit --full copies the whole unit into /etc instead.
  • List-valued directives like ExecStart accumulate rather than replace, so reset them empty first (ExecStart=) then set the value you want; single-valued directives like Restart just replace.
  • A file edit changes nothing live until daemon-reload re-reads it and restart applies it; verify the result with systemctl cat (see the merge) and systemctl show -p <Directive> (the effective value).
  • systemd's runtime verbs (cat, status, edit, daemon-reload, restart, show) need systemd as PID 1 and a bus. A container lab box has neither, so they fail there or are answered by a stand-in that ignores drop-ins. Its file tools still work: systemd-analyze unit-paths shows the search order, and systemd-analyze verify <path> loads a unit with its drop-ins offline. Read its output, because an unknown key is only a warning. Authoring and verifying the drop-in is the portable half of the skill, and it is what the systemd labs grade.