A lightweight, browser-based viewer for Pino JSON logs with optional real-time log watching.
- Upload
.log,.txt,.json, or.ndjsonfiles - Paste newline-delimited Pino JSON logs directly into the app
- Real-time log watching from mounted directories (Docker)
- Live-streaming via WebSocket with auto-reconnect
- Follow mode to auto-scroll as new logs arrive
- Filter logs by level and module
- Search across full log payloads
- Sort by timestamp, level, module, or message
- Copy individual log entries as JSON
- Light/dark theme toggle
- Node.js 22+
- pnpm 10+
If you do not have pnpm installed, enable it with Corepack:
corepack enablepnpm install --frozen-lockfilepnpm devThen open http://localhost:3000.
To develop with the watch server running alongside Next.js:
# Terminal 1 — Next.js frontend
pnpm dev
# Terminal 2 — Watch API server
WATCH_PATHS='[{"name":"My App","path":"./test-logs"}]' npx tsx server/index.tsThe frontend auto-detects the API server at http://localhost:3001 during development.
pnpm dev— run the app in development modepnpm build— create a production buildpnpm start— run the production serverpnpm lint— run linting
Paste newline-delimited JSON like:
{"level":30,"time":1739530000000,"msg":"Server started","module":"api"}
{"level":40,"time":1739530001000,"msg":"Slow query detected","module":"db"}
{"level":50,"time":1739530002000,"msg":"Unhandled exception","module":"worker"}Pinoctular is available as a multi-arch Docker image (linux/amd64 and linux/arm64) from the GitHub Container Registry.
Create a compose.yml:
services:
pinoctular:
container_name: pinoctular
image: ghcr.io/chrisstayte/pinoctular:latest
ports:
- 1738:80
restart: unless-stoppedThen start it:
docker compose up -dPinoctular will be available at http://localhost:1738.
Change
1738to any host port you prefer.
docker run -d \
--name pinoctular \
-p 1738:80 \
--restart unless-stopped \
ghcr.io/chrisstayte/pinoctular:latestEach release publishes a tagged image. To pin to a specific version instead of latest:
image: ghcr.io/chrisstayte/pinoctular:1.2.0Available tags can be found on the packages page.
If you're running Pinoctular behind a reverse proxy (e.g. Nginx, Caddy, Traefik), point it at the container's port. For example with Caddy:
pinoctular.example.com {
reverse_proxy localhost:1738
}
If you prefer to build the image yourself:
git clone https://github.com/chrisstayte/pinoctular.git
cd pinoctular
docker build -t pinoctular .
docker run -d -p 1738:80 pinoctularPinoctular can watch directories on disk and stream new log lines to the browser in real time. This feature is designed for Docker deployments where you mount your application's log directories into the container.
When watching is enabled, the home page displays cards for each watched folder. Selecting a folder loads historical logs and opens a live WebSocket connection that streams new lines as they are written.
Set the WATCH_PATHS environment variable to a JSON array of folder configurations. Each entry needs a name (display label) and a path (directory inside the container):
[
{ "name": "App", "path": "/logs/app" },
{ "name": "Worker", "path": "/logs/worker" }
]Mount the corresponding host directories as volumes so the container can access the log files.
services:
pinoctular:
container_name: pinoctular
image: ghcr.io/chrisstayte/pinoctular:latest
ports:
- 1738:80
restart: unless-stopped
environment:
- WATCH_PATHS=[{"name":"App","path":"/logs/app"},{"name":"Worker","path":"/logs/worker"}]
volumes:
- /var/log/myapp:/logs/app
- /var/log/myworker:/logs/workerdocker run -d \
--name pinoctular \
-p 1738:80 \
-e 'WATCH_PATHS=[{"name":"App","path":"/logs/app"}]' \
-v /var/log/myapp:/logs/app \
--restart unless-stopped \
ghcr.io/chrisstayte/pinoctular:latestThe Docker image runs two processes:
- nginx serves the static frontend on port 80.
- Node.js API server (started automatically when
WATCH_PATHSis set) watches the configured directories for.logfile changes and exposes a REST + WebSocket API on an internal port. nginx proxies/api/*requests to this server.
If WATCH_PATHS is not set, only nginx runs and the watch feature is unavailable. The frontend gracefully detects this and hides all watch-related UI.
- Only files with the
.logextension are monitored. - Watching is non-recursive (top-level files in each configured directory).
- The server uses polling (1-second interval) for reliable cross-platform file change detection.
- File rotation is handled automatically. If a log file is truncated (rotated), the server resets its read offset and starts from the beginning of the new content.
Once you select a watched folder in the UI:
- Historical log lines are fetched via HTTP (up to 10,000 lines by default).
- A WebSocket connection opens and subscribes to that folder.
- New lines written to any
.logfile in the folder are pushed to the browser in real time. - The connection includes a heartbeat and auto-reconnects with exponential backoff (up to 30 seconds) if interrupted.
While viewing a watched source, a Follow button appears in the header. When enabled, the log table auto-scrolls to the bottom as new entries arrive so you always see the latest output.
| Variable | Required | Description |
|---|---|---|
WATCH_PATHS |
No | JSON array of {"name":"...","path":"..."} objects defining folders to watch. When set, the API sidecar starts automatically and the frontend detects it at runtime. Omit to disable watching entirely. |
NEXT_PUBLIC_APP_VERSION |
No | Version string displayed in the app header. |
This project deploys to GitHub Pages via .github/workflows/nextjs.yml and is available at:

