Tradecraft // NO. 014

We deleted it: how to fully remove a self-hosted service

10 August 2026· 7 min read· Case № 014

A self-hosted service is never one thing — it is a systemd unit, a cron job, a data directory, a set of env vars, and a process. Here are the five places it hides and the exact commands to prove each one is gone.

I deleted the same service twice before I deleted it once. The first time I stopped the unit, removed the directory, and called it done. It came back on the next reboot — the cron job I had forgotten about re-enabled it. The second time I stopped the unit, removed the directory, and the cron job, and the env vars that pointed at it. That stuck. The lesson was not that SearXNG is hard to remove. It is that a self-hosted service is not one thing. It is five things, and you have to find all five.

Thesis

When you finish, you can remove a self-hosted service from a Linux box completely — knowing the five places it hides, and using the exact commands that prove each one is gone.

This is the part of self-hosting nobody writes down. Installing a service gets a README. Removing one gets an uninstall line that stops the process and calls it a day — and leaves a trail behind that breaks the next deploy, the next reboot, or the next time your agent reads a stale env var and tries to reach a service that is not there.

The five places a service hides

A self-hosted service is rarely a single binary. By the time it has run for a while, it has spread across five places:

  1. A systemd unit that starts it and keeps it alive.
  2. A cron entry that restarts it or runs its maintenance.
  3. A data directory where it stores its state.
  4. Environment variables in profiles or shells that point at it.
  5. A running process that is holding its sockets and files.

The mistake is treating any one of these as “the service.” It is all five. Removing one leaves the other four behind, and the first one to resurrect the service will win.

Step 1 — Stop and disable the unit

The unit is the obvious one, and it is where every uninstall starts.

sudo systemctl stop searxng
sudo systemctl disable searxng

stop halts the running process. disable removes the boot-time symlink so it does not start on reboot. Wait a beat, then confirm both took:

systemctl is-active searxng   # -> inactive
systemctl is-enabled searxng # -> disabled

Then remove the unit file itself. If you leave it in /etc/systemd/system/, a stray systemctl enable or a reboot with a lingering WantedBy will bring it back.

sudo rm /etc/systemd/system/searxng.service
sudo systemctl daemon-reload

daemon-reload is the step everyone forgets. systemd caches the unit list; without the reload it keeps scheduling against a file that no longer exists. Run it before you move on.

Step 2 — Remove the cron entry

This is the one that got me. My SearXNG setup had a watchdog cron that restarted the service if it died. Stopping the unit did not stop the cron — the cron was a separate thing, and it kept trying to start a service whose unit I had not yet removed.

Find every reference first:

crontab -l | grep -i searxng
ls /etc/cron.d/ | grep -i searxng

Then edit the crontab to drop the matching lines:

crontab -e

and remove the file from /etc/cron.d/ if it lives there:

sudo rm /etc/cron.d/searxng-watchdog

A cron entry is a restarter. It does not care whether the unit still exists. If you remove the unit but leave the cron, the cron fails silently on every schedule and you get a log full of “no such service” errors you will chase later.

Step 3 — Remove the data directory

The service keeps its state somewhere. For SearXNG that was ~/searxng. Remove it and its contents:

rm -rf ~/searxng

This is the one to be careful with. rm -rf does not ask, and it is not reversible. Before you point it at a directory, confirm what is in it — you do not want to delete a data directory that holds something you need to keep.

ls -la ~/searxng

If the directory holds config you want to keep for reference, back it up first:

tar czf ~/searxng-backup.tar.gz ~/searxng

Then delete. The blank ls after the delete is the proof it is gone:

ls ~/searxng   # -> No such file or directory

Step 4 — Strip the environment variables

This is the subtle one, and the one most likely to bite an agent fleet. A service is not just its process — it is every env var that points at it. In Hermes, each profile has its own .env file. If SEARXNG_URL was provisioned into a profile’s .env, that profile still believes the service exists, and the next time it tries to search it will burn an API call or throw an error against a dead endpoint.

Find every reference:

grep -rl "SEARXNG" ~/.hermes/profiles/*/.env

Then remove the matching lines from each file. The same applies to config.yaml — if the service was wired into a tool’s config, remove the stanza there too.

grep -rl "SEARXNG" ~/.hermes/profiles/*/config.yaml

This matters whether or not you use Hermes. Any config that references a service by env var or config key is a living pointer to it. A dead pointer silently degrades whatever reads it — it does not announce itself.

Step 5 — Confirm no process is running

The last check is the process table. A removed unit and a removed directory can still have a straggler process holding a socket.

pgrep -af searxng

If it returns nothing, you are clean. My only “match” when I ran this was the grep command itself — the shell wrapper for the check contained the string I was searching for. Read the output, do not blindly trust it: a search for “searxng” will match the command you are running to search for “searxng.”

The reveal

Here is why I do not trust uninstall scripts anymore. When I ran all five checks against my box, every one of them came back empty — clean on the first pass. But that is only convincing because I had a checklist. The time before, I had stopped the unit and called it done, and the watchdog cron resurrected the service on the next reboot. The difference between “I stopped it” and “it is gone” is the checklist.

The ironic part is that the removal was done by an agent. I told a headless worker to uninstall SearXNG and clean up all traces, and it did — the same worker from the headless-worker article. The agent did not do the five checks because it was clever. It did them because the goal was specific: “clean up all traces.” Vague goals get half-removals; the checklist is what makes a goal specific enough for an agent to actually finish.

The recipe

The full removal, in order, with the verification built into each step:

  1. Stop and disable the unit.
    sudo systemctl stop searxng && sudo systemctl disable searxng
    systemctl is-active searxng  # expect: inactive
    systemctl is-enabled searxng # expect: disabled
    sudo rm /etc/systemd/system/searxng.service
    sudo systemctl daemon-reload
  2. Remove the cron entry.
    crontab -l | grep -i searxng
    sudo rm /etc/cron.d/searxng-watchdog   # if it exists
  3. Remove the data directory.
    ls -la ~/searxng   # confirm before deleting
    rm -rf ~/searxng
    ls ~/searxng       # expect: No such file or directory
  4. Strip the environment variables.
    grep -rl "SEARXNG" ~/.hermes/profiles/*/.env
    grep -rl "SEARXNG" ~/.hermes/profiles/*/config.yaml
    # remove the matching lines from each file
  5. Confirm no process runs.
    pgrep -af searxng   # expect: nothing (ignore the grep itself)

Substitute the service name for searxng throughout. The five places do not change with the service — only the names do.

The takeaway

A self-hosted service is five things, not one: a unit, a cron entry, a data directory, a set of env vars, and a process. Removing one is not removing the service. Removing all five, and verifying each with the check that pairs with it, is.

The two that will bite you are the cron entry that restarts the service and the env var that still points at it. The cron brings it back on reboot; the env var makes your other tools believe it is still there. Both are silent.

The same discipline applies when you hand the job to an agent. “Uninstall X” leaves room for a half-removal. “Uninstall X and clean up all traces” names the outcome — and the checklist is what makes the outcome checkable.

Next up is the other half of operational hygiene: how to decide a self-hosted service is worth keeping in the first place, and the signals that tell you one has become a liability before you have to delete it.

← All transmissions