feat(cli): support systemd socket activation (#11169)

* feat(cli): support systemd socket activation

Serve the API from a single stream listener inherited through the systemd activation protocol while retaining the existing address bind path when no listener is provided. Validate activation metadata, preserve the public-bind safety check, and document an on-demand systemd setup.

Assisted-by: Codex:gpt-5

* fix(cli): satisfy listener cleanup lint

Make the best-effort close explicit so errcheck accepts the deferred systemd listener cleanup.

Assisted-by: Codex:gpt-5 [golangci-lint]

---------

Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
This commit is contained in:
localai-org-maint-bot
2026-07-29 03:44:24 +02:00
committed by GitHub
parent 84972cb745
commit 8f9184fbb2
6 changed files with 280 additions and 4 deletions

View File

@@ -243,6 +243,23 @@ func (r *RunCMD) Run(ctx *cliContext.Context) error {
return nil
}
activatedListeners, err := systemdActivatedListeners()
if err != nil {
return fmt.Errorf("loading systemd socket activation listeners: %w", err)
}
activatedListener, err := selectSystemdListener(activatedListeners)
if err != nil {
for _, listener := range activatedListeners {
_ = listener.Close()
}
return err
}
if activatedListener != nil {
defer func() {
_ = activatedListener.Close()
}()
}
os.MkdirAll(r.BackendsPath, 0750)
os.MkdirAll(r.ModelsPath, 0750)
@@ -732,8 +749,13 @@ func (r *RunCMD) Run(ctx *cliContext.Context) error {
// LAN, or VPN that's the historical "trusted network" deployment, but on
// a public IP it makes every model, gallery install, settings change, and
// admin endpoint reachable by anyone who can connect to the port.
listenAddress := r.Address
if activatedListener != nil {
listenAddress = activatedListener.Addr().String()
}
authConfigured := app.AuthDB() != nil || len(r.APIKeys) > 0
if err := requireAuthOrTrustedBind(r.Address, authConfigured, r.AllowInsecurePublicBind); err != nil {
if err := requireAuthOrTrustedBind(listenAddress, authConfigured, r.AllowInsecurePublicBind); err != nil {
return err
}
@@ -743,7 +765,11 @@ func (r *RunCMD) Run(ctx *cliContext.Context) error {
return err
}
xlog.Info("LocalAI is started and running", "address", r.Address)
if activatedListener != nil {
appHTTP.Listener = activatedListener
xlog.Info("Using systemd socket activation listener", "address", listenAddress)
}
xlog.Info("LocalAI is started and running", "address", listenAddress)
// Start P2P if token was provided via CLI/env or loaded from runtime_settings.json
if token != "" || app.ApplicationConfig().P2PToken != "" {
@@ -762,11 +788,11 @@ func (r *RunCMD) Run(ctx *cliContext.Context) error {
// backends like PostgreSQL need to call the embeddings API during
// collection initialization.
go func() {
waitForServerReady(r.Address, app.ApplicationConfig().Context)
waitForServerReady(listenAddress, app.ApplicationConfig().Context)
app.StartAgentPool()
}()
return appHTTP.Start(r.Address)
return appHTTP.Start(listenAddress)
}
// waitForServerReady polls the given address until the HTTP server is

View File

@@ -0,0 +1,17 @@
package cli
import (
"fmt"
"net"
)
func selectSystemdListener(listeners []net.Listener) (net.Listener, error) {
switch len(listeners) {
case 0:
return nil, nil
case 1:
return listeners[0], nil
default:
return nil, fmt.Errorf("systemd socket activation requires exactly one stream listener, got %d", len(listeners))
}
}

View File

@@ -0,0 +1,71 @@
//go:build linux
package cli
import (
"fmt"
"net"
"os"
"strconv"
)
const systemdListenFDStart = 3
func systemdActivatedListeners() ([]net.Listener, error) {
listenPID := os.Getenv("LISTEN_PID")
listenFDs := os.Getenv("LISTEN_FDS")
if listenPID == "" && listenFDs == "" {
return nil, nil
}
defer func() {
for _, key := range []string{"LISTEN_PID", "LISTEN_FDS", "LISTEN_FDNAMES"} {
_ = os.Unsetenv(key)
}
}()
pid, err := strconv.Atoi(listenPID)
if err != nil {
return nil, fmt.Errorf("invalid LISTEN_PID %q: %w", listenPID, err)
}
count, err := strconv.Atoi(listenFDs)
if err != nil || count < 0 {
return nil, fmt.Errorf("invalid LISTEN_FDS %q", listenFDs)
}
if pid != os.Getpid() || count == 0 {
return nil, nil
}
return listenersFromSystemdFDs(systemdListenFDStart, count)
}
func listenersFromSystemdFDs(start, count int) (_ []net.Listener, err error) {
listeners := make([]net.Listener, 0, count)
defer func() {
if err != nil {
for _, listener := range listeners {
_ = listener.Close()
}
}
}()
for offset := range count {
fd := uintptr(start + offset)
file := os.NewFile(fd, fmt.Sprintf("LISTEN_FD_%d", fd))
if file == nil {
return nil, fmt.Errorf("opening systemd listener file descriptor %d", fd)
}
listener, listenerErr := net.FileListener(file)
closeErr := file.Close()
if listenerErr != nil {
return nil, fmt.Errorf("using systemd file descriptor %d as a stream listener: %w", fd, listenerErr)
}
if closeErr != nil {
_ = listener.Close()
return nil, fmt.Errorf("closing inherited systemd file descriptor %d: %w", fd, closeErr)
}
listeners = append(listeners, listener)
}
return listeners, nil
}

View File

@@ -0,0 +1,9 @@
//go:build !linux
package cli
import "net"
func systemdActivatedListeners() ([]net.Listener, error) {
return nil, nil
}

View File

@@ -0,0 +1,101 @@
//go:build linux
package cli
import (
"net"
"os"
"strconv"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("selectSystemdListener", func() {
It("keeps normal address binding when systemd passes no listener", func() {
listener, err := selectSystemdListener(nil)
Expect(err).NotTo(HaveOccurred())
Expect(listener).To(BeNil())
})
It("uses the single stream listener passed by systemd", func() {
inherited, err := net.Listen("tcp", "127.0.0.1:0")
Expect(err).NotTo(HaveOccurred())
DeferCleanup(inherited.Close)
listener, err := selectSystemdListener([]net.Listener{inherited})
Expect(err).NotTo(HaveOccurred())
Expect(listener).To(BeIdenticalTo(inherited))
})
It("rejects ambiguous activation with multiple stream listeners", func() {
first, err := net.Listen("tcp", "127.0.0.1:0")
Expect(err).NotTo(HaveOccurred())
DeferCleanup(first.Close)
second, err := net.Listen("tcp", "127.0.0.1:0")
Expect(err).NotTo(HaveOccurred())
DeferCleanup(second.Close)
listener, err := selectSystemdListener([]net.Listener{first, second})
Expect(err).To(MatchError(ContainSubstring("exactly one")))
Expect(listener).To(BeNil())
})
})
var _ = Describe("systemdActivatedListeners", func() {
It("turns an inherited TCP file descriptor into a working listener", func() {
original, err := net.Listen("tcp", "127.0.0.1:0")
Expect(err).NotTo(HaveOccurred())
file, err := original.(*net.TCPListener).File()
Expect(err).NotTo(HaveOccurred())
Expect(original.Close()).To(Succeed())
listeners, err := listenersFromSystemdFDs(int(file.Fd()), 1)
Expect(err).NotTo(HaveOccurred())
Expect(listeners).To(HaveLen(1))
DeferCleanup(listeners[0].Close)
client, err := net.Dial("tcp", listeners[0].Addr().String())
Expect(err).NotTo(HaveOccurred())
DeferCleanup(client.Close)
server, err := listeners[0].Accept()
Expect(err).NotTo(HaveOccurred())
Expect(server.Close()).To(Succeed())
})
It("ignores descriptors intended for another process and clears the activation environment", func() {
Expect(os.Setenv("LISTEN_PID", strconv.Itoa(os.Getpid()+1))).To(Succeed())
Expect(os.Setenv("LISTEN_FDS", "1")).To(Succeed())
Expect(os.Setenv("LISTEN_FDNAMES", "localai-http")).To(Succeed())
DeferCleanup(func() {
_ = os.Unsetenv("LISTEN_PID")
_ = os.Unsetenv("LISTEN_FDS")
_ = os.Unsetenv("LISTEN_FDNAMES")
})
listeners, err := systemdActivatedListeners()
Expect(err).NotTo(HaveOccurred())
Expect(listeners).To(BeEmpty())
Expect(os.Getenv("LISTEN_PID")).To(BeEmpty())
Expect(os.Getenv("LISTEN_FDS")).To(BeEmpty())
Expect(os.Getenv("LISTEN_FDNAMES")).To(BeEmpty())
})
It("reports malformed activation metadata instead of silently binding another socket", func() {
Expect(os.Setenv("LISTEN_PID", strconv.Itoa(os.Getpid()))).To(Succeed())
Expect(os.Setenv("LISTEN_FDS", "not-a-number")).To(Succeed())
DeferCleanup(func() {
_ = os.Unsetenv("LISTEN_PID")
_ = os.Unsetenv("LISTEN_FDS")
})
listeners, err := systemdActivatedListeners()
Expect(err).To(MatchError(ContainSubstring("LISTEN_FDS")))
Expect(listeners).To(BeNil())
})
})

View File

@@ -59,6 +59,58 @@ After installation, you can:
- Configure models in the models directory
- Customize settings via environment variables or config files
## Start LocalAI on demand with systemd
LocalAI accepts a single TCP listener passed through the systemd socket
activation protocol. This lets systemd listen on the public port and start
LocalAI only when the first client connects.
Create `/etc/systemd/system/local-ai.socket`:
```ini
[Unit]
Description=LocalAI API socket
[Socket]
ListenStream=8080
NoDelay=true
[Install]
WantedBy=sockets.target
```
Create the matching `/etc/systemd/system/local-ai.service`:
```ini
[Unit]
Description=LocalAI
[Service]
Type=simple
User=localai
Group=localai
ExecStart=/usr/local/bin/local-ai run
WorkingDirectory=/var/lib/local-ai
```
Adjust the user, binary path, working directory, and model configuration for
your installation. Then enable the socket, not the service:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now local-ai.socket
```
The first connection to port 8080 starts `local-ai.service`; systemd holds that
connection until LocalAI is ready to accept it. `LOCALAI_ADDRESS` and
`--address` are ignored while an inherited listener is present. LocalAI
rejects activation with multiple stream listeners so it cannot silently choose
the wrong endpoint.
For a Podman-managed container, configure Podman to preserve and pass the
systemd socket file descriptor into the container. The LocalAI process inside
the container consumes the same activation protocol.
## Next Steps
- [Try it out with examples](/basics/try/)