# NginxPulse: a Go log analysis panel for Nginx access logs

> NginxPulse parses Nginx access logs into a PostgreSQL-backed dashboard with PV filtering, IP geolocation and client parsing. It is a small self-hosted tool, not a replacement for a full analytics platform, and since 1.5.3 it requires PostgreSQL.

**likaia/nginxpulse** — 轻量级 Nginx 访问日志分析与可视化面板，提供实时统计、PV 过滤、IP 归属地与客户端解析。

- Repository: https://github.com/likaia/nginxpulse
- Website: https://nginx-pulse.kaisir.cn/
- Stars: 2,695 · Forks: 205
- Language: Go
- License: MIT
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/likaia-nginxpulse

## The problem NginxPulse targets

Nginx writes access logs as plain text lines. Answering even basic questions from those lines means writing awk pipelines, and the answers get stale as soon as the log rotates. NginxPulse is built for the operator who already has Nginx access logs on disk and wants a browser view of them without shipping the data to a hosted analytics service. The README describes it as a lightweight Nginx access log analysis and visualization panel offering real-time statistics, PV filtering, IP geolocation and client parsing. The audience is narrow on purpose: someone who can mount a log directory into a container and is willing to run a database next to it. It is not aimed at teams that need session stitching, funnels or cross-domain attribution, because nothing in the repository suggests those features exist.

## How parsing, geolocation and storage fit together

The backend is Go 1.24.x with Gin and Logrus, and it stores everything in PostgreSQL through pgx. The geolocation pipeline is the part worth understanding before you deploy, because it explains why a fresh install shows incomplete data. Log parsing and IP resolution are deliberately decoupled: the parser only writes rows and marks addresses as pending, and a background task fills in the location later. Resolution itself is layered. Empty, local and loopback addresses become "local"; private ranges become "internal/local network". Otherwise the code checks a persistent plus in-memory cache (the README gives a default ceiling of 1,000,000 entries), then the embedded ip2region database, and only if that returns unknown does it call a remote API, by default ip-api.com/batch, with a 1.2 second timeout and batches of at most 100 addresses. Remote failure resolves to "unknown". The ip2region_v4.xdb and ip2region_v6.xdb files are compiled into the binary, extracted to ./var/nginxpulse_data/ on first start, and the project tries to load a vector index to speed up lookups. The practical consequence is that this project makes outbound requests to a third-party domain by default, and the README says the deployment environment must allow that egress. A self-hosted geolocation service is mentioned as an option, with details pointed at the documentation site. The frontend is Vue 3, Vite, TypeScript, PrimeVue and ECharts/Chart.js, and there is a separate mobile build served at /m with only overview, daily report, real-time and logs pages.

## Installing NginxPulse with Docker on port 8088

The single image bundles the frontend Nginx, the backend and a PostgreSQL instance, and publishes port 8088. The README is explicit that the data directories must be mounted: /app/var/nginxpulse_data and /app/var/pgdata. Without them the container exits with an error. Mount your Nginx log directory read-only and the configs directory so the generated configuration survives a restart.

```bash
docker run -d --name nginxpulse \
  -p 8088:8088 \
  -v ./docker_local/logs:/share/logs:ro \
  -v ./docker_local/nginxpulse_data:/app/var/nginxpulse_data \
  -v ./docker_local/pgdata:/app/var/pgdata \
  -v ./docker_local/configs:/app/configs \
  -v /etc/localtime:/etc/localtime:ro \
  magiccoders/nginxpulse:latest
```

Replace docker_local with directories that exist on the host. With no configuration file or environment variables, the first start opens an initialization wizard. Saving it writes configs/nginxpulse_config.json, and the README states a restart is required for the change to take effect. If you prefer a file from the start, mount configs/nginxpulse_config.json into the container at /app/configs/nginxpulse_config.json. The Compose variant in the repository adds a second published port, 8089, network_mode: host and stop_grace_period: 90s; the README recommends keeping that grace period so the bundled PostgreSQL shuts down consistently and does not enter recovery on the next start. Timezone matters: log timestamps are parsed using the system timezone, so mount /etc/localtime or set TZ=Asia/Shanghai with tzdata present. The mobile entry point is http://<host>:8088/m, and first-time initialization has to be done on a desktop because the mobile pages tell you to open a computer.

## Running NginxPulse as a single binary without Docker

Since version 1.5.3 SQLite is gone, so single-process deployment requires your own PostgreSQL and a DB_DSN value, or a database.dsn entry in configs/nginxpulse_config.json. Binaries for each platform come from the repository releases. The single build embeds the frontend, so one process serves both the UI at http://localhost:8088 and the API under http://localhost:8088/api/. Configuration can be read from ./configs/nginxpulse_config.json, a relative path, or injected as an environment variable when you do not want a file on disk:

```bash
CONFIG_JSON="$(cat /path/to/nginxpulse_config.json)" ./nginxpulse
```

Both the config path and the data directory ./var/nginxpulse_data are relative to the working directory, which is the failure mode to watch for under systemd: set WorkingDirectory, or use CONFIG_JSON. If you build from source instead, the repository ships a Makefile with make frontend, make frontend-mobile, make backend, make single, make dev and make clean, and make single produces linux/amd64 and linux/arm64 artifacts under bin/ plus a config at bin/configs/nginxpulse_config.json with port :8088 by default.

## Permissions, SELinux and the geolocation dependency

The image runs as a non-root user named nginxpulse, and the README makes a point that surprises people: seeing your logs with docker exec proves nothing, because docker exec runs as root by default while the application user may have no access. The documented fix is to match the container user's UID/GID to the owner of the host log and data directories, passing PUID and PGID at startup and then chowning the data directories and granting read access to the logs. On RHEL, CentOS and Fedora, SELinux can block a mounted volume even when the files look present; the README suggests appending :z or :Z to the volume specification, with :Z restricting the directory to the current container and :z allowing sharing. It explicitly calls chmod -R 777 a bad practice and recommends it only for temporary troubleshooting. The second constraint is external: unless you stand up your own geolocation service, deployment hosts need outbound access to ip-api.com, and when that call fails or times out the address is recorded as unknown and regional statistics stay incomplete. The README also notes that while resolution is pending the UI shows a pending state, so a freshly imported log file will not have full geographic breakdowns immediately. That is a design trade-off, not a bug, but it means the panel is a poor fit for environments with strict egress control unless the self-hosted resolver path is configured.

## Alternatives and where NginxPulse loses

The obvious comparison is GoAccess, which reads Nginx logs and renders a terminal or HTML report from a single binary with no database and no background resolution queue. GoAccess is the better choice when you want a report generated on demand from a file and nothing running in between. NginxPulse takes the opposite approach: it persists parsed rows in PostgreSQL, keeps a cache of resolved addresses, and serves a live dashboard with historical queries. That buys you a queryable history across log rotations, at the cost of a database you now have to back up and upgrade. If your actual need is product analytics rather than log inspection, neither tool is right, because both model requests, not users. Within NginxPulse's own scope, the honest limitations are that geolocation depends on a third-party API by default, that the mobile view is limited to four pages, and that the SQLite removal in 1.5.3 means there is no longer a zero-dependency single-file deployment. Anyone upgrading across that boundary has to provision PostgreSQL first.

## Licence and upgrade cost

The project is MIT licensed, which permits commercial and private use, modification and redistribution provided the copyright notice and permission notice are preserved. The repository ships a LICENSE file at the top level; read it rather than relying on this summary, and note that MIT says nothing about the terms of the external ip-api.com service or of PostgreSQL, which carry their own conditions. On upgrades, releases have moved quickly: v1.6.22, v1.6.23 and v1.6.24 all landed in May 2026, the last on 2026-05-19, which is also the date of the most recent push to the main branch. That is more than four months before today, so the project is not being actively developed right now; treat the current release as the state of the code rather than expecting fixes to arrive quickly. The upgrade mechanics are the part that costs time: the bundled PostgreSQL lives in /app/var/pgdata, which is why the README insists on the 90 second stop grace period, and an external database is recommended at version 16. Because the config file lives in /app/configs and the schema lives in PostgreSQL, a container image bump is mostly a restart, but the 1.5.3 SQLite removal is a hard cut with no documented rollback path in the README, so anyone still on an older release should plan a fresh PostgreSQL instance and re-import rather than assume an in-place migration exists.

## Conclusion

Adopt NginxPulse if you already run Nginx, want a self-hosted panel on port 8088 without sending log data to a third party, and can supply PostgreSQL 16 or accept the bundled instance. Skip it if you need SQLite-only single-binary simplicity, or if the deployment host cannot reach ip-api.com for geolocation. Before rolling it out, verify the container user's UID/GID against your log and data directories, confirm the timezone mount, and check that /app/var/nginxpulse_data and /app/var/pgdata are mounted or the container will exit.

## FAQ

### What is NginxPulse?

It is a lightweight Nginx access log analysis and visualization panel written in Go, with a Vue 3 frontend, that provides real-time statistics, PV filtering, IP geolocation and client parsing. It stores parsed data in PostgreSQL and serves a dashboard on port 8088.

### How do I install NginxPulse with Docker?

Run the magiccoders/nginxpulse:latest image, publish port 8088, and mount /app/var/nginxpulse_data and /app/var/pgdata plus your log directory. Without those data mounts the container exits with an error.

### Does NginxPulse still support SQLite?

No. The README states that for versions above 1.5.3 SQLite is fully deprecated, and single-process deployment requires your own PostgreSQL configured through DB_DSN or database.dsn.

### Why does NginxPulse show addresses as pending or unknown?

Log parsing only stores rows and marks addresses as pending; a background task resolves them afterwards. If the local ip2region lookup returns unknown, the project calls a remote API (ip-api.com/batch by default) and records unknown when that call fails or times out.

## Sources

- [License: MIT](https://github.com/likaia/nginxpulse/blob/main/LICENSE)
- [likaia/nginxpulse on GitHub](https://github.com/likaia/nginxpulse)
- [Project website](https://nginx-pulse.kaisir.cn/)
- [README](https://github.com/likaia/nginxpulse/blob/main/README.md)
- [Releases](https://github.com/likaia/nginxpulse/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/likaia-nginxpulse
