Test Socket Activation with systemd-socket-activate
systemd-socket-activate runs a daemon behind a temporary listening socket so you can watch exactly how a connection gets passed to it. You will send it a real connection and see the handoff happen, before you ever commit to a .socket unit. Allow about fifteen minutes.
The route
Jump straight to the step you need, or tick off Done means at the end.
- You need: the
systemdpackage, a test daemon, and an unused local TCP port. - Scope: the examples are unprivileged and bind only to loopback.
1. Check the installed tool
This guide follows systemd 255, installed here as package version 255.4-1ubuntu8.17; options and diagnostics can differ on older releases. Confirm the binary and read its short option summary.
$ command -v systemd-socket-activate
/usr/bin/systemd-socket-activate
$ systemd-socket-activate --version
systemd 255 (255.4-1ubuntu8.17)
$ systemd-socket-activate --help
systemd-socket-activate [OPTIONS...]
Listen on sockets and launch child on connection.
The daemon command belongs after the options for systemd-socket-activate. Keep that boundary visible when the child itself has options. A common mix-up is assuming this creates or enables a persistent systemd unit: it does not. It starts a temporary listener and launches the requested process purely for testing.
Checkpoint
You have an installed version and a child command you can run harmlessly. The rest of this guide uses cat, which echoes input and exits when its connection closes.
2. Start a loopback echo service
Choose a high, unused port. The address 127.0.0.1 keeps other machines from connecting. -l chooses the listening address, --inetd maps the accepted connection to the child's standard input and output, and -a starts a separate child for each connection.
$ systemd-socket-activate -l 127.0.0.1:24761 --inetd -a cat
Listening on 127.0.0.1:24761 as 3.
Leave this running in one terminal. In a second terminal, connect with whatever client you have. For OpenBSD netcat, -N closes the connection once standard input reaches end of file.
$ printf 'socket-activate-ok\n' | nc -N 127.0.0.1 24761
socket-activate-ok
The listener's terminal should report a connection and a child exiting with code 0, though exact process IDs and client ports vary.
Connection from 127.0.0.1:48840 to 127.0.0.1:24761
Spawned cat (cat) as PID 1636503.
Execing cat (cat)
Child 1636503 died with code 0
If your netcat lacks that close option, connect interactively instead and press Ctrl-D after typing the test line.
Warning
Do not expose a test daemon on 0.0.0.0 unless you have deliberately considered who can reach it.
Stop the temporary listener with Ctrl-C when you are done. There is no unit file or enablement to undo. If the port is already occupied, pick another port rather than stopping an unrelated service.
3. Tell the two descriptor conventions apart
Without --inetd, the child inherits its normal standard input and output, and listening sockets are passed using systemd's socket-activation convention: the first socket created here is file descriptor 3. LISTEN_FDS tells a socket-aware program how many descriptors to inspect, and LISTEN_PID tells it which process should receive them.
With --inetd, the accepted connection itself becomes standard input and standard output, which is why plain cat worked above. This mode suits daemons designed for the older inetd style, but it is not interchangeable with a program that calls sd_listen_fds().
Checkpoint
Choose the interface your daemon expects before debugging the daemon itself. Use --inetd when it reads and writes the connected stream through standard input and output; omit it when the program expects systemd socket activation and reads descriptor 3 or later.
4. Pass a socket-aware daemon
Replace /path/to/daemon with the absolute path to a program that understands systemd socket activation, putting its own arguments after the daemon name.
$ systemd-socket-activate -l 127.0.0.1:24761 /path/to/daemon --daemon-option VALUE
Here the daemon's standard input and output remain inherited from the launcher, so its logs normally appear in the launching terminal. The listener hands the socket through the standard activation environment and descriptor layout. A daemon that expects inetd input will look idle or fail outright because it is checking the wrong place; retry with --inetd only if its documentation says that is the protocol it wants.
You can use more than one --listen option. Existing descriptors supplied through LISTEN_FDS keep their original positions, while sockets specified with --listen get consecutive descriptors after them. If the daemon distinguishes descriptors by name, supply matching names.
$ systemd-socket-activate \
-l 127.0.0.1:24761 \
--fdname=public \
/path/to/daemon
The name is made available through the activation metadata used by sd_listen_fds_with_names(). It does not rename a network port, and a name given for a non-existent extra descriptor is simply ignored.
5. Add only the environment the child needs
Use -E or --setenv to add an environment variable to the launched process. An explicit value is easiest to audit.
$ systemd-socket-activate \
-l 127.0.0.1:24761 \
--inetd -a \
--setenv=APP_MODE=test \
cat
Write only a variable name, and the launcher copies that variable's current value into the child environment instead.
$ export APP_MODE=test
$ systemd-socket-activate -l 127.0.0.1:24761 --setenv=APP_MODE /path/to/daemon
Quote values containing spaces or shell metacharacters.
Warning
Do not put passwords, API tokens or production connection strings on a command line, since command lines and shell history can be observable. Use a temporary test value and clear it afterwards with unset APP_MODE once you are done with it.
6. Diagnose the common failures
A bind failure usually means the address is already in use, not configured, or restricted to privileged processes. Check the port without touching any real service.
$ ss -ltn '( sport = :24761 )'
$ systemd-socket-activate -l 127.0.0.1:24762 --inetd -a cat
Using a different high port normally avoids needing sudo; do not reach for elevated privileges just to sidestep a port conflict. If the child exits immediately, run it directly first, confirm its arguments, then check whether it expects inetd descriptors or the systemd activation protocol.
When a client connects but gets no response, confirm the listener is in --inetd mode for a standard-input program like cat. For a socket-aware daemon, omit --inetd and inspect the daemon's own error output instead: a successful listener start only proves the socket was created, not that the daemon understood the descriptor.
Use --datagram or --seqpacket only when the daemon and client actually require those socket types. They are alternatives, not settings to combine: the manual forbids using both together. Start with the default stream socket unless the protocol specifically requires another type.
Done means
- Version and boundary checked: you confirmed the installed systemd-socket-activate version and the command boundary.
- Loopback listener worked: a loopback listener accepted a connection and a harmless child returned successfully.
- Right protocol picked: you selected
--inetdor systemd descriptor passing to match the daemon. - No credentials leaked: any environment variables were test-specific and free of credentials.
- Nothing left running: you stopped the temporary listener and changed no persistent unit or service configuration.