Skip to main content
By default everything runs on one box: your app transmits to the agent on loopback, and the agent drains to PostgreSQL. But the agent doesn’t have to live with your app. A common production layout is a dedicated monitoring server — one machine running the agent daemon and (often) PostgreSQL, with one or more app servers shipping telemetry to it over the network via NIGHTOWL_INGEST_URI. Use this layout when you run multiple app servers and want one telemetry pipeline, or when you want monitoring load (buffering, drain, Postgres writes) off your app boxes entirely. If your app runs on Laravel Vapor, see the Vapor guide — same split, serverless specifics.

How it fits together

The app servers collect telemetry and transmit it over TCP. The monitoring server buffers and drains it to PostgreSQL. Only the monitoring server holds database credentials — the app servers never open a database connection.
Install the nightowl/agent package in your application as usual and deploy the same release to every machine, monitoring server included. On the app servers the package only redirects Nightwatch’s collector to NIGHTOWL_INGEST_URI — you never run the daemon there. On the monitoring server you run php artisan nightowl:agent.

Which variable goes on which machine

Each machine reads its own .env. Setting a variable on the wrong side does nothing — this is the most common misconfiguration in this layout.

Setup

1

Run the agent on the monitoring server

Deploy your application there and start the daemon under a supervisor, exactly as in Production deployment. Bind it beyond loopback so the app servers can reach it:
Run php artisan nightowl:migrate on this machine as part of its deploy — it owns the schema.
2

Open the ingest port to your app servers only

Add a firewall rule (ufw, cloud firewall, or security group) allowing inbound TCP 2407 from each app server’s address — ideally their private addresses on the same VPC:Telemetry travels over plain TCP (token-gated, not encrypted in transit), so never open 2407 to 0.0.0.0/0.
3

Point each app server at the agent

On every app server:
Prefer the private/VPC address. If a server must cross the public internet to reach the agent, set NIGHTOWL_INGEST_TIMEOUT=3 — public routes have latency spikes a 1-second budget will occasionally lose to.If you cache configuration, run php artisan config:cache and restart your workers after changing these.
4

Verify

Open the health dashboard: ingest rate should track your app traffic and buffer depth should stay low. Deploy new releases to all machines together — the app servers and the daemon must agree on the wire format — and restart the daemon after upgrading.

Troubleshooting

Intermittent stream_socket_client(): Unable to connect to tcp://... (Connection timed out) in your exceptions feed, while telemetry mostly flows. This is the transmit timeout losing to the network, not a broken setup. The default NIGHTOWL_INGEST_TIMEOUT is 0.5 seconds — tuned for loopback, tight for a network hop and much too tight for the public internet. Raise it on the app servers (it does nothing on the monitoring server), and switch NIGHTOWL_INGEST_URI to the private address if you’re currently using the public one. Each occurrence also means that batch of telemetry was dropped. If you run several app servers, open the timeout exception’s detail page: the SERVERS stat tells you where it comes from. All of them → look at the agent machine (health dashboard, load, network). Just one → that machine’s route or firewall. Every connection times out, nothing arrives. The packets are being dropped before they reach the daemon — almost always the firewall (a closed port times out; a reachable machine with no listener refuses). Check the firewall rule first, then that the daemon is running and bound beyond loopback: ss -tlnp | grep 2407 on the monitoring server should show 0.0.0.0:2407 (or the private IP), not 127.0.0.1:2407. Connections refused. The machine is reachable but nothing listens on that address: the daemon isn’t running, or NIGHTOWL_AGENT_HOST is still the loopback default. Telemetry arrives but the payloads are rejected. NIGHTOWL_TOKEN differs between the app server and the monitoring server — it must be identical on both sides.