Modify a Service the Right Way: Drop-in Overrides
- 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
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/aptupdate 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-pathsprints 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/systemfor software you install by hand); thegrepkeeps the two this lesson is about./etc/systemd/systemcomes 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 EOFThere is no webapppackage on this box, so for one command you play the package manager: this is the file an RPM or a.debwould 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 becausesystemd-analyze verifychecks thatExecStart=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 catprints 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.serviceshows the same vendor file without the header.- `/usr/lib/systemd/system/` (also
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
.dsuffix:/etc/systemd/system/<unit>.service.d/. Any file ending in.confinside 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=alwaysis 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 EOFThis is a drop-in by hand, and it runs here exactly as written — it only touches files. Make the <unit>.service.ddirectory, then write a small.confwith just the[Service]section and the two directives we want to add. Note what is absent: noExecStart, noDescription, no[Unit]or[Install]. Those all still come from the vendor unit you installed in step 1; we are only layeringRestart=alwaysandRestartSec=3on top. The filename (override.conf) is conventional — any*.confin 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 verifyloads the unit the way systemd would: the vendor file first, then every.confinwebapp.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 theechomeans 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.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
.ddirectory by hand works and is worth doing once to see the mechanism, but the everyday tool is `systemctl edit`. Runningsystemctl edit webappopens your$EDITORon a fresh, empty drop-in, and on save it does three chores for you: it creates the<unit>.service.d/directory, writes your text intooverride.conf, and runsdaemon-reloadso 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--fullonly 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 editcannot run. On Ubuntu it wants a bus and an interactive tty, and answersCannot edit units if not on a tty.; on Rocky the stand-insystemctlanswersunsupported verb 'edit'. That costs you nothing, because what it writes is precisely the file you wrote by hand in the last step —editis a convenience wrapper, not a different mechanism. Author the drop-in by hand here; reach forediton 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 webappgives you an empty override to fill in with just[Service]+ your directives, then creates the directory, savesoverride.conf, and runsdaemon-reloadfor you.systemctl edit --full webappwould 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.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=alwaysin 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 hasExecStart=/usr/bin/python3 -m http.server 8000and your drop-in addsExecStart=/usr/bin/python3 -m http.server 8080 --bind 127.0.0.1, systemd does not swap one for the other; forExecStartspecifically 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
ExecStartthat is two lines — an emptyExecStart=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 webappwebapp.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.verifymerges it over the vendor unit, finds two start commands and refuses the unit. A live host refuses it the same way atdaemon-reload, and the service will not start.2>&1 | grep webappkeeps only the lines about this unit:verifyalso loads every unitwebappdepends 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 EOFThe 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 twoExecStart=entries and refuses the unit, as the previous block showed. Single-valued directives likeRestart=orMemoryMax=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=alwaysfrom 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.confand20-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.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 editruns it for you). `systemctl restart <unit>` then stops and starts the service so a changedExecStart, resource limit, or environment actually takes effect on the running process. (reload— lowercase, nodaemon-— is different again: it asks the service to re-read its own config, like nginx re-readingnginx.conf, and only works if the unit defines anExecReload.)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-reloadnorshowdoes anything useful on this box. On Ubuntu both need the bus; on Rocky the stand-insystemctlacceptsdaemon-reloadand does nothing, and does not knowshow. 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 withsystemd-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 webappReal-host reference — not meaningful on the lab box. On Ubuntu it exits 1 (no bus). On Rocky the stand-in systemctlaccepts it, but itsrestartruns the vendorExecStart=directly and ignores drop-ins, so it proves nothing about your override. On a live host,daemon-reloadmakes systemd re-read the unit and your drop-in from disk, andrestartapplies the change to the running process. You run both after writing an override by hand (withsystemctl edit, thedaemon-reloadhalf is already done for you, but you still restart to apply a runtime change like a newExecStart).Example — not runnable on your lab box
systemctl show -p Restart -p ExecStart webappRestart=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 singleExecStart(not two) confirms the empty-then-set reset worked.Restart=nois the surpriseshowexists to catch: step 4's>rewroteoverride.confand dropped theRestart=alwaysyou wrote in step 2. Ifshowdisagrees with what you thought you wrote, believeshow— 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 .ddirectory,restart.conf, is read too (drop-ins load in filename order, so afteroverride.conf), and it has a typo:RestartSecsforRestartSec.verifynames the file, the line and the key. Note the word ignoring: a misspelt key is not fatal, the unit still loads andverifystill exits 0, and the service simply runs without the setting. So read whatverifyprints instead of trusting its exit code.systemd-service-capstonegrades with this exact tool.rm -rf /etc/systemd/system/webapp.service.d /usr/lib/systemd/system/webapp.serviceTidy 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-reloadso systemd forgets it.Check yourself
You need a service to always restart if it crashes. Where do you put the Restart=always directive?
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) putsProtectSystem=,NoNewPrivileges=,CapabilityBoundingSet=and the other confinement directives in a drop-in, and measures the result withsystemd-analyze security --offline=true.
- 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 --fullcopies 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-pathsshows the search order, andsystemd-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.
