Build a Socket-Activated Service with systemd.socket
You will finish with a small TCP echo service managed by systemd socket activation, a verifier-tested pair of unit files, and a clear way to remove it. The example uses a user service on 127.0.0.1:8765, so it does not need root and is not exposed on your network.
The route
Jump straight to the step you need, or tick off Done means at the end.
Allow about 20 minutes. You need systemd 255 or a compatible release, a shell, and systemd-analyze. The installed machine used for this guide reports systemd 255.4-1ubuntu8.17. A real daemon must understand systemd socket activation, either through the native file-descriptor protocol or the traditional socket-on-standard-input method used here.
Safety: binding a service to a public address, changing a system service, or accepting untrusted input is a separate deployment decision. Start with loopback and a high port. The example is deliberately disposable.
1. Understand the two unit files
A .socket unit opens and supervises the listening socket. A matching .service unit handles traffic when systemd receives it. By default, echo.socket activates echo.service when Accept=no, or instances of [email protected] when Accept=yes. The example chooses the latter so each incoming TCP connection gets a separate service process.
Save these files as echo.socket and [email protected] in a temporary directory first:
[Unit]
Description=Example socket activated echo service
[Socket]
ListenStream=127.0.0.1:8765
Accept=yes
[Install]
WantedBy=sockets.target
[Unit]
Description=Example echo connection
[Service]
ExecStart=/usr/bin/cat
StandardInput=socket
StandardOutput=socket
cat is only a test handler. It reads the accepted connection from standard input and writes the same bytes to standard output. In a production service, replace it with a program designed for activation and check its documentation for how it receives passed file descriptors.
2. Check the installed syntax before installing anything
Run this ordinary, read-only command from the directory containing the two files:
$ systemd-analyze verify echo.socket [email protected]
No output and exit status 0 mean that systemd could parse the units and did not find a verification error. This checks syntax and many unit relationships, but it does not bind port 8765 or prove that your user manager is running.
Checkpoint: if verification fails, fix the first reported error before continuing. Common distractions are a typo in ListenStream=, a missing [Socket] section, or writing echo.service when Accept=yes requires the [email protected] template.
3. Install the pair as user units
Create the user unit directory and copy the files into it. This changes your user systemd configuration, but does not require elevated privileges:
$ mkdir -p "$HOME/.config/systemd/user"
$ cp echo.socket [email protected] "$HOME/.config/systemd/user/"
Reload the user manager so it notices the files:
$ systemctl --user daemon-reload
$ systemctl --user cat echo.socket
[Socket]
ListenStream=127.0.0.1:8765
Accept=yes
If systemctl --user reports that it cannot connect to the bus, your session does not have a running user manager. Do not switch to sudo systemctl as a reflex: that controls the system manager and is a different scope. You can still complete the syntax check, or use a normal desktop or login session with user systemd support.
4. Start the socket, not the service
Start the socket unit. This is the point where systemd attempts to bind the address:
$ systemctl --user start echo.socket
$ systemctl --user is-active echo.socket
active
$ systemctl --user status echo.socket --no-pager
There should be no long-running echo@ service yet. Socket activation starts the handler when a connection arrives. If the start fails, inspect the journal:
$ journalctl --user -u echo.socket -n 30 --no-pager
The usual causes are another process already using port 8765, a stale socket unit from an earlier test, or a user manager that cannot access the unit file. Change the port to another high, unused value and update the client command if the port is occupied. Do not kill an unknown process merely to free a port.
5. Make one connection and verify activation
Use netcat from the same machine. This is an ordinary client command:
$ printf '%s\n' 'socket activation works' | nc 127.0.0.1 8765
socket activation works
The response shows that the accepted connection reached cat. Check the instantiated service and its recent log:
$ systemctl --user list-units 'echo@*' --all --no-legend
$ journalctl --user -u 'echo@*.service' -n 30 --no-pager
The instance name contains an escaped connection identifier, so do not script against one exact instance name. The service may already be inactive when you inspect it because cat exits after the client closes the connection. That is expected for this test.
6. Understand the settings that matter
ListenStream= selects a stream socket. A bare port is interpreted as IPv6, while 127.0.0.1:8765 is explicitly IPv4. An address such as /run/example.sock would instead create an AF_UNIX socket. For a specific IP address that may not exist when the unit starts, the manpage documents FreeBind=yes; use it only when that early binding behaviour is intentional.
Accept=no is the recommended shape for new daemons: one service receives the listening sockets and manages connections itself. Accept=yes is mainly useful for inetd-style programs and requires a template service. It also creates one service instance per connection, so consider MaxConnections=, MaxConnectionsPerSource=, and cleanup behaviour before using it on an untrusted interface.
The defaults can be surprising. A file-system socket node defaults to mode 0666, while its automatically created parent directories default to 0755. Set SocketMode=, SocketUser=, and SocketGroup= explicitly for a Unix socket carrying sensitive data. For network sockets, the default BindIPv6Only=default follows the kernel setting, which can make an IPv6 bind reachable over IPv4 too.
7. Stop and remove the test safely
Stop the socket first. This prevents new activations but does not necessarily terminate an already running service instance:
$ systemctl --user stop echo.socket
$ systemctl --user is-active echo.socket
inactive
Then stop any remaining test instances and remove only the files created for this guide:
$ systemctl --user stop 'echo@*.service' 2>/dev/null || true
$ rm "$HOME/.config/systemd/user/echo.socket" "$HOME/.config/systemd/user/[email protected]"
$ systemctl --user daemon-reload
The final command is destructive to these two unit files, but it does not remove application data. If you need the test again, restore the two files from your temporary copies, run daemon-reload, and start echo.socket.
Done means
systemd-analyze verifyaccepted the socket and matching service template.- The socket bound only to loopback and used a high test port.
- A client connection caused an
echo@service instance to run. - You understand why
Accept=yesneeds a template and whenAccept=nois preferable. - You can inspect failures with
systemctl --user statusandjournalctl --user. - The test socket and user unit files are stopped and removed when the experiment is over.