# go-systemd: eight Go packages for the interfaces systemd already exposes

> CoreOS bindings for socket activation, sd_notify, D-Bus unit control, journald read and write, logind, machined and unit files.

**coreos/go-systemd** — Go bindings to systemd socket activation, journal, D-Bus, and unit files

- Repository: https://github.com/coreos/go-systemd
- Stars: 2,716 · Forks: 339
- Language: Go
- License: Apache-2.0
- Published: 2026-10-06 · Updated: 2026-10-06 · Language: en
- Canonical page: https://hysenlabs.com/projects/coreos-go-systemd

## The package list is really an inventory of systemd's interfaces

The README describes the project in one line, Go bindings to systemd, and then enumerates eight packages. Reading that list as a set tells you more about systemd than it does about this library, because each package corresponds to a different interface systemd has decided to expose.

`activation` is for writing and using socket activation from Go. `daemon` is for notifying systemd of service status changes. `dbus` is for starting, stopping and inspecting running services and units. `journal` is for writing to systemd's logging service, journald, while `sdjournal` is for reading from it by wrapping its C API. `login1` integrates with the systemd logind API, `machine1` registers machines and containers with systemd, and `unit` handles serialization and comparison of unit files.

So the inventory runs from the moment a socket is inherited at process start, through the lifetime of the service, into the logging system, out to the machine and session managers, and down to the file format that describes all of it. Each of those is a place where systemd deliberately exposes a contract to a process it is not fully controlling.

The project is Apache-2.0 licensed, has roughly 2,716 stars and 339 forks, 69 open issues, and was last pushed on 2026-07-23. The fork count relative to stars is high for a library of this kind, which fits: most consumers are distributions and platform tools embedding it rather than individuals experimenting.

## Socket activation changes how the server is written, not just how it starts

Socket activation is the package worth understanding first, because it is the one that alters the structure of your program rather than adding a function call to it.

The idea is that systemd owns the listening socket. The service does not bind a port. systemd starts the service and hands it an already-bound file descriptor, which means the listening socket can exist while the service is restarting, and the service can be reloaded or replaced without clients ever seeing a refused connection. Ordering becomes systemd's problem to solve rather than yours.

Go's standard library makes this awkward by design, because `net.Listen` always creates a fresh socket. The `activation` package bridges that by taking inherited descriptors from the environment and handing back ordinary `net.Listener` values, so the rest of your server code is unchanged. The README points at a worked example for exactly this case: an HTTP server using socket activation, set up by following the example directory in the repository on a Linux machine running systemd.

That example path is the entire documentation for the feature, which is a fair reflection of how little code is involved. The environment variables the mechanism relies on, the number of passed file descriptors and their numbering are the substance of the protocol, and the rest is descriptor to listener plumbing.

It is also the package with the most visible failure modes. The v22.7.0 release notes record a fix for an error on a negative value of `LISTEN_FDS`, which is the sort of edge case that only appears once services start being restarted under load.

## sd_notify is how a Go service says it is actually ready

The `daemon` package implements the sd_notify protocol, and the README describes its purpose plainly: it can be used to inform systemd of service start-up completion, watchdog events, and other status changes.

This exists because a process starting and a service being ready are different events. A Go program that has bound nothing and spawned no goroutines has started, but it may still be loading configuration, connecting to a database or warming a cache. systemd's default assumption, that the process starting means the service is up, is wrong often enough to matter, and `Type=notify` services exist to correct it. A service that reports readiness lets systemd start it before its predecessor has fully exited, and lets an orchestrator treat a slow start as a failure rather than a mystery.

The mechanism is small, a datagram to a socket whose path arrives in an environment variable, carrying a line such as a readiness message and an optional status string. The value of a Go binding is not that this is complicated but that the details are handled: the latest release added a `SdNotifyMonotonicUsec` helper to the daemon package, which is the kind of function that only exists because someone got it subtly wrong by hand.

Watchdog events are the other half. A service can be told to send a ping on an interval and restarted if it stops, which turns a silent deadlock into a restart. For long-running Go services that is often more valuable than the readiness signal, and it is the part of this package that tends to be adopted after the first incident rather than the first install.

## Writing to the journal is pure Go, reading it needs cgo

The split between `journal` and `sdjournal` is the single most consequential design decision in the repository, and it is a direct reflection of how systemd exposes the two directions.

Writing is pure Go. The README says the `journal` package lets you submit journal entries directly to systemd's journal, taking advantage of features like indexed key=value pairs for each log entry. In practice that means structured logging where a field is a real field, queryable later, rather than a string someone has to grep. For a Go service that already uses `log/slog`, mapping structured fields onto journal keys is the natural shape, and it needs no C toolchain to build.

Reading is different. The `sdjournal` package provides read access to the journal by wrapping around journald's native C API, and the README states the consequence directly: it consequently requires cgo and the journal headers to be available.

That asymmetry is the practical lesson. Adopting logging through `journal` costs you nothing beyond a dependency-free import. Adopting journal reading means your build now needs a C compiler and development headers, which changes your cross-compilation story, your static linking options and your container image. For a library whose entire selling point is having two dependencies, that is a large step for a reader.

The two packages are named for what they wrap rather than for what they do, which is a small hint about how the project is organized: systemd's own terminology is the organizing principle.

## Two dependencies, and a module path that encodes the version

The dependency list is short enough to read in full, and it is the clearest statement of scope in the repository. `go.mod` declares the module as `github.com/coreos/go-systemd/v22`, requires Go 1.24, and needs exactly two modules:

```
require (
	github.com/godbus/dbus/v5 v5.1.0
	golang.org/x/sys v0.1.0
)
```

`godbus/dbus/v5` is the D-Bus client, which is what carries the `dbus`, `login1` and `machine1` packages. `golang.org/x/sys` is the syscall layer, needed for socket activation's descriptor handling and the notification socket. Nothing else. No logging framework, no YAML parser, no serialization library, even though the `unit` package deserializes unit files, which tells you those files are handled with the standard library.

The `/v22` suffix in the module path deserves a note because it looks like a systemd version and is not. It is Go's semantic import versioning, meaning major version 22 of this library. It does correspond loosely to systemd generations, but the correspondence is the maintainers' choice rather than a rule, and the README badge asks for Go 1.24 or newer to match the `go.mod` directive.

The tree shows the same discipline at directory level. Beyond the eight packages there are `examples/`, `internal/`, `fixtures/`, `import1/`, `scripts/`, and `util/`. The presence of `fixtures/` and `internal/` together with 69 open issues suggests a project with real test discipline for a surface area this fiddly, and `scripts/` is where the things that maintain CI live.

## Recent releases are mostly fixes to edge cases

The release history reads as a maintenance log rather than a feature list, which is what a stable bindings library should look like.

v22.7.0, published 2026-01-28, states it fixes an issue with multiple calls to StopUnit, simplifies and improves code and documentation, and adds a few new methods. The individual entries are characteristic: a fix for the negative `LISTEN_FDS` value, miscellaneous error reporting improvements, the new `SdNotifyMonotonicUsec` helper in the daemon package, a test fix for SetUnitProperties with respect to systemd 252 and later, and a simplification of an escape character in the unit package. Several of the rest are Dependabot bumps of GitHub Actions.

Two of those deserve comment. A test fix relative to systemd 252 is a reminder that this library tracks a moving upstream: when systemd changes behavior, the bindings sometimes need to change with it even though the Go API is stable. And a simplification of an escape character in the unit package is exactly the kind of change that matters for anyone diffing or comparing unit files programmatically.

Before that, v22.6.0 from 2025-08-20 bumped the Go requirement to 1.23, modernized code and improved CI. The gap between v22.5.0 in November 2022 and v22.6.0 in August 2025 is the longest in the list, and it is worth knowing about if you are starting from an older pinned version, since the minimum Go version moved in between.

The D-Bus debugging section in the README is also honest about what is involved. To watch D-Bus traffic you create a bus config file that enables eavesdropping for root, which is the kind of change that is trivial to make and important to undo:

```
<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
    <policy user="root">
        <allow eavesdrop="true"/>
        <allow eavesdrop="true" send_destination="*"/>
    </policy>
</busconfig>
```

The filename is given, `/etc/dbus-1/system-local.conf`, but nothing in the README tells you to remove it afterwards. Eavesdropping on the system bus as root is a debugging aid with a security cost, so it belongs in a session you remember to clean up rather than in a permanent configuration file.

## Conclusion

The thing to take from this repository is that none of these integrations require a native library. Except for reading the journal, where journald exposes a C API and the package wraps it, everything goes through D-Bus or the small notification socket, which is why the whole module depends on godbus and golang.org/x/sys and nothing else. That design is why socket activation and sd_notify feel cheap to adopt in an existing service, and why the unit packages are useful even to code that never runs on systemd. The repository does not settle the operational side: the socket activation example lives in the examples directory, the debugging section tells you to write a bus config file but not how to undo it, and the version scheme in the import path is a versioning convention from Go rather than a systemd concept. The sensible starting point is the HTTPS server example, since socket activation is the one integration that changes the shape of your program rather than adding a call to it.

## FAQ

### What does go-systemd provide?

Eight packages covering socket activation, sd_notify service notification, D-Bus control of units, writing to and reading from journald, integration with logind, registering machines with machined, and serializing and comparing unit files.

### Does using go-systemd require cgo?

Only for reading the journal. The journal package writes to journald in pure Go, while sdjournal wraps journald's native C API and therefore requires cgo and the journal headers. The dbus, daemon, activation, login1, machine1 and unit packages need no C toolchain.

### Why is there a v22 in the import path?

It is Go's semantic import versioning, so github.com/coreos/go-systemd/v22 means major version 22 of the library rather than a systemd release number. The correspondence to systemd generations is the maintainers' choice, not a versioning rule.

### What dependencies does the module have?

Two. go.mod requires github.com/godbus/dbus/v5 at v5.1.0 for D-Bus and golang.org/x/sys at v0.1.0 for syscalls, and declares Go 1.24. Unit file handling uses the standard library rather than a separate parser dependency.

### How do I start using socket activation with a Go HTTP server?

The README points at an HTTP server example in the examples directory of the repository, to be followed on a Linux machine running systemd. The daemon package covers the other half, implementing sd_notify so the service can report start-up completion, watchdog events and other status changes.

## Sources

- [coreos/go-systemd on GitHub](https://github.com/coreos/go-systemd)
- [Issues](https://github.com/coreos/go-systemd/issues)
- [License: Apache-2.0](https://github.com/coreos/go-systemd/blob/main/LICENSE)
- [README](https://github.com/coreos/go-systemd/blob/main/README.md)
- [Releases](https://github.com/coreos/go-systemd/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/coreos-go-systemd
