DEV Community

Cover image for Make Idle Linux Services Disappear: A Practical Guide to systemd Socket Activation + systemd-socket-proxyd
Lyra
Lyra

Posted on

Make Idle Linux Services Disappear: A Practical Guide to systemd Socket Activation + systemd-socket-proxyd

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=yes or Network=none
  • a practical bridge for legacy apps via systemd-socket-proxyd

In this guide, I will show two patterns:

  1. Native socket activation for a tiny Python service using a Unix socket
  2. 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:

  1. systemd creates and owns the listening socket
  2. a client connects
  3. systemd starts the service
  4. 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:

  • systemd timers
  • systemd-run sandboxing
  • systemd credentials
  • 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()
Enter fullscreen mode Exit fullscreen mode

Make it executable:

sudo install -m 0755 hello-activation.py /usr/local/bin/hello-activation.py
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

A few notes:

  • User=nobody keeps the process unprivileged
  • ProtectSystem=strict makes most of the filesystem read-only
  • RestrictAddressFamilies=AF_UNIX is a nice fit because this example only needs a Unix socket
  • the service does not bind its own socket; systemd already did that

Step 4: enable the socket, not the service

sudo systemctl daemon-reload
sudo systemctl enable --now hello-activation.socket
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You should see:

  • the .socket unit is active
  • the .service unit 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/
Enter fullscreen mode Exit fullscreen mode

Expected output:

hello from pid 12345
Enter fullscreen mode Exit fullscreen mode

Now check again:

systemctl status hello-activation.service --no-pager
journalctl -u hello-activation.service -n 20 --no-pager
Enter fullscreen mode Exit fullscreen mode

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:

  • systemd owns the public listening socket
  • systemd-socket-proxyd is 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Reload and enable the socket plus backend:

sudo systemctl daemon-reload
sudo systemctl enable --now demo-backend.service demo-proxy.socket
Enter fullscreen mode Exit fullscreen mode

Step 4: test the public entry point

curl http://127.0.0.1:8080/
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

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)