Ramblings of an aging IT geek
← Ramblings of an aging IT geek
golang

the smallest useful daemon i've written this year

Shipping a tiny Go daemon that does one thing, with proper signal handling, a systemd unit and graceful shutdown, and why the boring parts are most of the work.

Go code on a terminal screen

The daemon watches a directory, and when a file lands it does a small job and moves it. That's the whole feature. The interesting part isn't the feature, it's that "ship a long-running process" carries a tail of unglamorous work that the feature doesn't hint at, and Go makes most of that tail pleasant.

The core was an afternoon. Go's standard library gives you a file watcher (via a small dependency), a context, and goroutines, and the actual logic fit on a screen. The other day and a half was everything around it: signals, shutdown, logging, and not corrupting a file if I kill it mid-job.

the bit that makes it a daemon, not a script

A script runs and exits. A daemon has to die well. That means catching SIGTERM and SIGINT, stopping cleanly, and finishing the file it's halfway through rather than leaving a half-processed mess.

ctx, stop := signal.NotifyContext(context.Background(),
    syscall.SIGINT, syscall.SIGTERM)
defer stop()

if err := run(ctx); err != nil {
    log.Fatalf("daemon exited: %v", err)
}

signal.NotifyContext is the bit I keep coming back to. The signal cancels the context, the context threads through every goroutine, and shutdown becomes "stop accepting new work, let the current job finish, return". No global flags, no channels I have to remember to close in the right order.

handing it to systemd

A daemon nobody can supervise is just a process that'll die at the worst moment and stay dead. The systemd unit is short and does the heavy lifting:

[Unit]
Description=File watcher daemon
After=network.target

[Service]
ExecStart=/usr/local/bin/watchd
Restart=on-failure
RestartSec=5
User=watchd

[Install]
WantedBy=multi-user.target

Restart=on-failure means it comes back if it crashes. Running as its own unprivileged user means a bug can't wander off and touch things it shouldn't. And because the binary is a single static Go file, deployment is "copy it, drop in the unit, systemctl enable --now". No runtime, no virtualenv, no apt dependencies to drift.

what "shipped" actually meant

The honest breakdown: the feature was maybe twenty percent of the effort. Graceful shutdown, the systemd unit, structured logging that's useful at 3am, and testing that a kill mid-job leaves a consistent state, that was the other eighty. None of it is clever. All of it is the difference between a script I run by hand and something I'm happy to forget about because it just sits there and works. It's been running for a week now without a peep, which is exactly the outcome I wanted: nothing to report.