If you run small internal services on Linux, there is a good chance some of them spend most of their life doing absolutely nothing.
A webhook receiver. A tiny API. A maintenance endpoint. A sidecar utility you only hit a few times a day.
Keeping those processes alive all the time is often unnecessary. systemd has had a better answer for years: socket activation.
With socket activation, systemd opens the listening socket first. Your service starts only when traffic actually arrives. That gives you a few useful wins:
- less always-on process clutter
- cleaner boot behavior
- easier privilege separation
- a path to isolate services with
PrivateNetwork=yesorNetwork=none - a practical bridge for legacy apps via
systemd-socket-proxyd
In this guide, I will show two patterns:
- Native socket activation for a tiny Python service using a Unix socket
-
Proxy-based socket activation for apps that do not support inheriting sockets from
systemd
This is not theory. You will end with working unit files, test commands, and verification steps.
What socket activation actually does
A normal daemon starts first and binds its own port or Unix socket.
With socket activation, systemd flips that around:
-
systemdcreates and owns the listening socket - a client connects
-
systemdstarts the service - the service receives the already-open socket and handles traffic
For .socket units, Accept=no means the service itself accepts connections from the inherited listening socket. That is the default and the most common mode for modern daemons.
If you use a Unix socket, you also gain a very nice security property: clients can reach the service only through the path you expose, while the service itself can run with a much tighter sandbox.
Anti-duplication note
This topic intentionally avoids overlap with my recent posts on:
-
systemdtimers -
systemd-runsandboxing -
systemdcredentials systemd-analyze security
The angle here is different: on-demand service startup, inherited sockets, and proxying non-native applications into a socket-activated design.
Pattern 1: native socket activation with a tiny Python service
For a minimal, reproducible example, I prefer a Unix domain socket instead of a TCP port. It is simple, local-only, and easy to inspect.
Step 1: create the Python responder
Save this as /usr/local/bin/hello-activation.py:
#!/usr/bin/env python3
import os
import socket
import socketserver
SOCKET_FD = 3 # systemd passes the first inherited FD here
class Handler(socketserver.StreamRequestHandler):
def handle(self):
body = (
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain; charset=utf-8\r\n"
"Connection: close\r\n\r\n"
f"hello from pid {os.getpid()}\n"
)
self.wfile.write(body.encode("utf-8"))
class ActivatedUnixHTTPServer(socketserver.UnixStreamServer):
def __init__(self, sock):
self.address_family = socket.AF_UNIX
socketserver.BaseServer.__init__(self, None, Handler)
self.socket = sock
self.server_address = "systemd-activated"
if __name__ == "__main__":
sock = socket.fromfd(SOCKET_FD, socket.AF_UNIX, socket.SOCK_STREAM)
server = ActivatedUnixHTTPServer(sock)
server.serve_forever()
Make it executable:
sudo install -m 0755 hello-activation.py /usr/local/bin/hello-activation.py
Why this works
systemd passes inherited file descriptors starting at FD 3. The service opens that descriptor with socket.fromfd(...) and serves requests on it.
This is the core idea behind native socket activation.
Step 2: create the socket unit
Save this as /etc/systemd/system/hello-activation.socket:
[Unit]
Description=Socket for hello activation demo
[Socket]
ListenStream=/run/hello-activation.sock
SocketMode=0660
[Install]
WantedBy=sockets.target
Step 3: create the service unit
Save this as /etc/systemd/system/hello-activation.service:
[Unit]
Description=Hello activation demo service
Requires=hello-activation.socket
After=hello-activation.socket
[Service]
Type=simple
ExecStart=/usr/local/bin/hello-activation.py
User=nobody
Group=nogroup
NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectSystem=strict
ProtectHome=yes
RestrictAddressFamilies=AF_UNIX
SystemCallArchitectures=native
MemoryDenyWriteExecute=yes
[Install]
WantedBy=multi-user.target
A few notes:
-
User=nobodykeeps the process unprivileged -
ProtectSystem=strictmakes most of the filesystem read-only -
RestrictAddressFamilies=AF_UNIXis a nice fit because this example only needs a Unix socket - the service does not bind its own socket;
systemdalready did that
Step 4: enable the socket, not the service
sudo systemctl daemon-reload
sudo systemctl enable --now hello-activation.socket
Do not start the service manually first. The point is to let the socket trigger it.
Step 5: verify the idle state
Before the first request:
systemctl status hello-activation.socket --no-pager
systemctl status hello-activation.service --no-pager
ls -l /run/hello-activation.sock
You should see:
- the
.socketunit is active - the
.serviceunit is inactive or not yet started - the Unix socket path already exists
Step 6: send the first request
curl --unix-socket /run/hello-activation.sock http://localhost/
Expected output:
hello from pid 12345
Now check again:
systemctl status hello-activation.service --no-pager
journalctl -u hello-activation.service -n 20 --no-pager
That first client connection should have caused systemd to launch the service.
Common mistakes with native socket activation
1) starting the service instead of the socket
For on-demand startup, enable the .socket unit. Starting the service directly defeats the point.
2) writing an app that still tries to bind the socket itself
If your service inherits a socket from systemd, it should use the inherited descriptor, not try to bind() the same address again.
3) mixing up Accept=yes and Accept=no
For most service-style daemons, use Accept=no so the service receives the listening socket. With Accept=yes, systemd accepts each connection and can spawn a per-connection instance from a template service.
Pattern 2: when the app does not support socket activation
This is where systemd-socket-proxyd becomes genuinely useful.
A lot of software still expects to bind its own TCP or Unix socket and knows nothing about inherited file descriptors. Rewriting or replacing it may not be worth it.
systemd-socket-proxyd gives you a bridge:
-
systemdowns the public listening socket -
systemd-socket-proxydis socket-activated - it forwards traffic to the real backend
- the backend can stay on a private localhost port or Unix socket
This is a strong pattern for internal tools and side services.
Practical example: expose a localhost-only backend on demand
Let us say your backend listens on 127.0.0.1:9000 and cannot do native socket activation.
Step 1: create the public socket unit
Save this as /etc/systemd/system/demo-proxy.socket:
[Unit]
Description=Public socket for demo backend
[Socket]
ListenStream=0.0.0.0:8080
NoDelay=true
[Install]
WantedBy=sockets.target
If you only want local access, use 127.0.0.1:8080 instead of 0.0.0.0:8080.
Step 2: create the proxy service unit
Save this as /etc/systemd/system/demo-proxy.service:
[Unit]
Description=Proxy traffic from socket activation to backend
Requires=demo-proxy.socket
After=demo-proxy.socket
Requires=demo-backend.service
After=demo-backend.service
[Service]
Type=notify
ExecStart=/usr/lib/systemd/systemd-socket-proxyd --connections-max=256 --exit-idle-time=2min 127.0.0.1:9000
PrivateTmp=yes
PrivateNetwork=yes
NoNewPrivileges=yes
[Install]
WantedBy=multi-user.target
Step 3: define or reference your backend service
If your actual backend is already managed elsewhere, keep using that.
If you want a minimal example service for testing, this simple Python HTTP server is enough:
/etc/systemd/system/demo-backend.service
[Unit]
Description=Example backend on localhost:9000
[Service]
Type=simple
ExecStart=/usr/bin/python3 -m http.server 9000 --bind 127.0.0.1
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
[Install]
WantedBy=multi-user.target
Reload and enable the socket plus backend:
sudo systemctl daemon-reload
sudo systemctl enable --now demo-backend.service demo-proxy.socket
Step 4: test the public entry point
curl http://127.0.0.1:8080/
Then inspect the units:
systemctl status demo-proxy.socket --no-pager
systemctl status demo-proxy.service --no-pager
systemctl status demo-backend.service --no-pager
Why this pattern is useful
It gives you a clean separation:
- the backend only needs to serve localhost or a Unix socket
- the public listening endpoint is owned by
systemd - the proxy can exit when idle if you use
--exit-idle-time= - the proxy can sit in a tighter namespace than the backend or join the backend namespace when needed
A note on containers and rootless setups
Socket activation is not only for classic host daemons.
Podman documents socket-activated services and containers, including cases where the container runs with Network=none while still serving traffic through an inherited activated socket. That is a neat pattern when you want to remove unnecessary network privileges from the workload itself.
In plain English: the socket can exist on the host side, while the service handles requests without opening its own network path.
When socket activation is a bad fit
It is useful, but not magical.
Avoid it when:
- startup latency on the first request is unacceptable
- the service maintains a large in-memory cache that is expensive to rebuild
- the app behaves badly when handed inherited sockets
- you need long warm state all the time anyway
A tiny internal API? Great candidate.
A heavyweight app that takes 20 seconds to warm up? Probably not.
Verification checklist
If you want to be sure your setup is really working, verify all of this:
# socket exists before service traffic
systemctl status hello-activation.socket --no-pager
# service should be idle until first request
systemctl status hello-activation.service --no-pager
# trigger activation
curl --unix-socket /run/hello-activation.sock http://localhost/
# inspect logs after activation
journalctl -u hello-activation.service -n 20 --no-pager
# proxy example
ss -lx | grep hello-activation || true
ss -ltnp | grep ':8080' || true
And if something fails:
systemd-analyze verify /etc/systemd/system/hello-activation.socket /etc/systemd/system/hello-activation.service
systemd-analyze verify /etc/systemd/system/demo-proxy.socket /etc/systemd/system/demo-proxy.service
That catches a surprising number of unit-file mistakes before you waste time guessing.
My practical rule of thumb
I use socket activation for services that are:
- low traffic
- simple to start
- easy to statelessly restart
- safer when they do not own their own public listening socket
And I reach for systemd-socket-proxyd when I want the operational benefits of socket activation but the application itself was never designed for it.
That combination is quietly one of the most underrated tricks in the Linux toolbox.
Sources and references
- Debian manpage:
systemd.socket(5)https://manpages.debian.org/testing/systemd/systemd.socket.5.en.html - Linux man-pages:
systemd-socket-proxyd(8)https://man7.org/linux/man-pages/man8/systemd-socket-proxyd.8.html - Podman documentation: socket activation tutorial https://github.com/containers/podman/blob/main/docs/tutorials/socket_activation.md
- Python documentation:
socketserverhttps://docs.python.org/3/library/socketserver.html
If you want, the next useful follow-up is taking the same pattern and applying it to a rootless Podman service with a Unix socket and no general network access at all. That is where this starts getting really elegant.
Top comments (0)