PAGI
view release on metacpan or search on metacpan
lib/PAGI/Server.pm view on Meta::CPAN
B<CLI:>
pagi-server --socket /tmp/pagi.sock ./app.pl
B<With workers:>
pagi-server --socket /tmp/pagi.sock --workers 4 ./app.pl
In multi-worker mode, the parent process creates the Unix socket and all
worker processes inherit the file descriptor via C<fork()>. The kernel
distributes incoming connections across workers.
=head2 How It Works
=over 4
=item 1. On startup, any existing file at the socket path is removed (stale
socket cleanup).
=item 2. The server creates and binds a C<SOCK_STREAM> Unix domain socket
at the specified path using C<IO::Socket::UNIX> (multi-worker) or
C<IO::Async::Listener> with C<< family => 'unix' >> (single-worker).
=item 3. If C<socket_mode> is set, C<chmod()> is called immediately after
binding to set the file permissions.
=item 4. In multi-worker mode, the parent creates the socket before forking.
Workers inherit the listening fd and each runs its own C<IO::Async::Listener>
wrapping the inherited handle.
=item 5. On graceful shutdown (SIGTERM/SIGINT), the socket file is unlinked
by both the single-worker C<shutdown()> path and the multi-worker
C<_initiate_multiworker_shutdown()> path.
=back
=head2 nginx Configuration
B<Basic upstream:>
upstream pagi_backend {
server unix:/var/run/myapp/pagi.sock;
keepalive 32;
}
server {
listen 80;
server_name myapp.example.com;
location / {
proxy_pass http://pagi_backend;
# Required for upstream keepalive:
proxy_http_version 1.1;
proxy_set_header Connection "";
# Forward client info (since PAGI can't see it over Unix socket):
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
B<Important:> The C<keepalive> directive is critical for performance. Without
it, nginx opens a new Unix socket connection for every request.
C<proxy_http_version 1.1> and C<proxy_set_header Connection ""> are required
for keepalive to work.
B<With TLS termination at nginx:>
server {
listen 443 ssl http2;
ssl_certificate /etc/ssl/myapp.crt;
ssl_certificate_key /etc/ssl/myapp.key;
location / {
proxy_pass http://pagi_backend;
# ... same proxy headers as above
}
}
nginx handles TLS and HTTP/2 with clients, then speaks plain HTTP/1.1 to
PAGI over the Unix socket. This is the recommended production pattern.
=head2 Socket Permissions and Security
By default, the socket file inherits permissions from the process umask.
Use C<socket_mode> to set explicit permissions:
# CLI
pagi-server --socket /var/run/myapp/pagi.sock --socket-mode 0660 ./app.pl
# Programmatic
PAGI::Server->new(
app => $app,
socket => '/var/run/myapp/pagi.sock',
socket_mode => 0660,
);
B<Production recommendations:>
=over 4
=item * B<Use a dedicated directory>, not C</tmp/>. Directories like
C</var/run/myapp/> or C</run/myapp/> prevent symlink attacks and provide
an additional permission layer.
=item * B<Use C<0660> with a shared group.> Create a group (e.g., C<myapp>)
that both the application user and the nginx user belong to:
sudo groupadd myapp
sudo usermod -aG myapp www-data # nginx user
sudo usermod -aG myapp myappuser # app user
sudo mkdir -p /var/run/myapp
sudo chown myappuser:myapp /var/run/myapp
sudo chmod 0750 /var/run/myapp
=item * B<Use systemd C<RuntimeDirectory>> for automatic directory management:
# /etc/systemd/system/myapp.service
[Service]
User=myappuser
Group=myapp
RuntimeDirectory=myapp
RuntimeDirectoryMode=0750
ExecStart=/usr/local/bin/pagi-server \
--socket /run/myapp/pagi.sock \
--socket-mode 0660 \
--workers 4 \
/opt/myapp/app.pl
systemd creates C</run/myapp/> on service start and cleans it up on stop.
=back
=head2 TLS Over Unix Sockets
TLS can be used over Unix sockets, though this is unusual â normally the
reverse proxy handles TLS termination. When TLS is configured on a Unix
socket listener, the server logs an info-level note suggesting reverse proxy
TLS termination instead.
The combination is allowed because it has legitimate uses (encrypted
inter-container communication, compliance requirements). All major ASGI
servers (Uvicorn, Hypercorn, Granian) also allow it.
=head2 HTTP/2 Over Unix Sockets
h2c (HTTP/2 cleartext) works over Unix sockets. This is useful for gRPC
backends or reverse proxies that support HTTP/2 to upstreams (e.g., Envoy).
Note that nginx does B<not> currently support HTTP/2 to upstream backends
(except for gRPC via C<grpc_pass>).
=head2 Scope Differences
For Unix socket connections, the PAGI scope differs from TCP connections:
=over 4
=item * B<C<client> is absent> â Unix sockets have no peer IP address or
port. The C<client> key is omitted entirely from the scope hashref (not set
to C<undef>). This is spec-compliant: the PAGI specification marks C<client>
as optional.
=item * B<C<server> is C<[$socket_path, undef]>>> â instead of C<[$host, $port]>.
=back
B<Middleware implications:> Any middleware that accesses C<< $scope->{client} >>
must check C<< exists $scope->{client} >> first. For client IP identification
behind a reverse proxy, use C<X-Forwarded-For> or C<X-Real-IP> headers
instead of C<< $scope->{client} >>. The C<PAGI::Middleware::XForwardedFor>
middleware (if available) handles this automatically.
B<Access log:> Unix socket connections log C<unix> as the client IP in the
access log instead of an IP address.
=head2 Stale Socket Cleanup
If a socket file already exists at the configured path (e.g., from a previous
crash), it is automatically removed before binding. This matches the behavior
of Starman, Gunicorn, Uvicorn, and other production servers. The socket file
is also removed during graceful shutdown (SIGTERM/SIGINT).
If the server is killed with SIGKILL (C<kill -9>), the socket file will
B<not> be cleaned up. It will be removed on the next startup.
=head1 MULTI-LISTENER SUPPORT (EXPERIMENTAL)
B<This feature is experimental.> The API is subject to change in future
releases.
A single PAGI::Server instance can listen on multiple endpoints
simultaneously. This is useful for:
=over 4
=item * B<TCP for health checks + Unix socket for app traffic> â load
balancers probe a TCP port while nginx uses the Unix socket.
=item * B<Multiple TCP ports> â serve different interfaces on different ports.
=item * B<Gradual migration> â listen on both old and new ports during
a transition.
=back
=head2 Programmatic API
my $server = PAGI::Server->new(
app => $app,
listen => [
{ host => '0.0.0.0', port => 8080 },
{ socket => '/tmp/pagi.sock', socket_mode => 0660 },
],
);
Each spec in the C<listen> array is a hashref with either C<< { host, port } >>
for TCP or C<< { socket } >> (with optional C<socket_mode>) for Unix sockets.
B<Note:> Per-listener TLS configuration is not yet supported. TLS is configured
server-wide via the C<ssl> constructor option and applies to all TCP listeners.
Unix socket listeners behind a reverse proxy do not need TLS â the proxy handles
TLS termination.
=head2 CLI
The C<--listen> flag is repeatable. The server auto-detects TCP vs Unix
socket: values containing C<:> are parsed as C<host:port>, everything else
is treated as a Unix socket path.
( run in 0.576 second using v1.01-cache-2.11-cpan-39bf76dae61 )