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 )