Sandeep Sidhu

One install script, six Linux distros: what universal agent installation really takes

Every “curl | bash” one-liner you have ever pasted is hiding a hundred if-statements. I know because I maintain one.

AlertKick installs a monitoring and security agent on customer servers. The requirement sounds simple: a customer pastes one command on any mainstream Linux box and ends up with a running, registered agent. No “first check your distro”, no menu of download links, no prerequisites. One command, any box.

Six variants before you even start

The script itself is one file, but behind it sit six package variants: deb and rpm for the package-managed world, and a plain tar.gz for everything else, each in amd64 and arm64. The script’s first real job is working out which of the six this machine needs.

The happy path is /etc/os-release, which nearly every modern distro ships. But “nearly” is doing a lot of work in that sentence. Containers and minimal images sometimes strip it, so there is a fallback: probe for dpkg or rpm directly and infer the family from whichever answers. Architecture comes from uname -m, with the aliasing dance that entails - x86_64, aarch64, arm64, and friends.

I settled on detecting by capability, not by name. Asking “is this Ubuntu or Debian or Mint” is a losing game because the list never ends. Asking “does this box speak deb, rpm, or neither” has three answers, and all three are handled.

The traps

Static linking is the cheat code. The agent is a single static Go binary. This one decision deletes an entire category of installer misery: no glibc version checks, no interpreter, no dependency resolution. A binary that runs on a 2018 CentOS box and a fresh Debian equally well is the difference between six package variants and sixty. If your agent is written in something that needs a runtime, your install script inherits every version conflict your language has ever had.

Init systems. systemd is the default assumption and covers most of the fleet, but “most” is not the requirement. Minimal containers have no init at all. The script has to notice what it is standing on and either register a service or come clean and tell you what it could not do - a silent half-install is worse than a loud failure.

Idempotency. Real users run install scripts twice. Sometimes because the first run failed halfway, sometimes out of habit, sometimes from a config management tool that reruns everything nightly. Re-running must upgrade or repair, never duplicate. That means every step is written as “ensure X is true”, not “do X”. And the uninstall path has to genuinely clean up, because the third run often follows an angry uninstall.

Registration is part of installation. Success is the agent phoning home with valid credentials, not a binary sitting on disk. The script carries a host-specific token, so the moment the service starts, the box appears in the dashboard already authenticated. If a customer has to go copy an API key into a config file after the “one command”, it was never one command.

Testing it honestly

Containers get you 80% of the way: spin up each distro family, run the script, assert the agent registers. Cheap, fast, runs on every change.

The last 20% only shows up on real machines - actual systemd, actual SELinux, actual weird corporate images with /tmp mounted noexec. VMs catch what containers structurally cannot, and the bugs found there are always the embarrassing ones.

What is deliberately out of scope

Linux only, for now. There is a Windows port on the roadmap and I would rather ship nothing there than a script that half-works. The same principle as everywhere else in this post: the installer is the first impression your product makes on an engineer’s actual server. It is either boring and reliable, or it is the reason they never got far enough to see the product.

Boring and reliable takes about a hundred if-statements. Budget for them.