Install fails
Problems running the one-line installer on a fresh machine.
The installer stops at Install + enable jt-gelflow.service via systemd? [Y/n] and pressing Enter does nothingfixed in v1.5.4+
Cause. curl | sudo bash collides with sudo's use_pty default on Ubuntu 26 / Debian 13. Sudo gives the child its own pty, so the script can write to /dev/tty but your keystrokes go to a different pty and read never sees them.
Fix. Run it non-interactively:
curl -fsSL https://raw.githubusercontent.com/jasoncheng7115/jt-gelflow/main/install.sh | sudo JT_GELFLOW_YES=1 bashSince v1.5.4 the prompt also times out after 60 seconds and continues with the default, so waiting works too.
error: cannot reach https://github.com — check network / DNS / proxyfixed in v1.5.6+
Two different causes, and they look identical.
Before v1.5.6 the reachability check used curl before the installer had installed it, so a minimal image with no curl failed here even with perfect networking. Upgrading to v1.5.6+ fixes that — the check now falls back to wget, then to a bash TCP connect.
Otherwise the network really is blocked. Verify from the box itself:
getent hosts github.com
curl -sI https://github.com | head -1Behind a corporate proxy, export it before running the installer:
export https_proxy=http://proxy.example:3128
export http_proxy=$https_proxyerror: no supported package manager found (apt/dnf/yum/pacman/zypper)
The installer bootstraps dependencies through your distro's package manager and only knows those five. On anything else, install the prerequisites yourself and then run the installer — it skips whatever is already present:
git,curl,python3(3.10+) andpython3-pipnodejs18+ is optional — the repo ships a pre-builtdist/
warn: Node 18+ recommended, found 12.x — falling back to committed dist/
This is not an error and needs no action. Vite 5 needs Node 18+, and Ubuntu 22.04's apt ships Node 12. The repository commits a pre-built dist/ for exactly this reason, so the installer uses it and you get the same frontend.
You only need a newer Node if you intend to modify the frontend source and rebuild.
error: needs root, or the install aborts on a permission error
The installer writes to /opt, /usr/local/bin and /etc/systemd/system, so it needs root. Note where sudo goes in the pipeline — it must apply to bash, not to curl:
# correct
curl -fsSL https://raw.githubusercontent.com/jasoncheng7115/jt-gelflow/main/install.sh | sudo bashThe jt-gelflow CLI is friendlier — it re-executes itself through sudo automatically, so jt-gelflow restart works from a normal account.
Upgrade fails
Problems running jt-gelflow update or re-running the installer.
fatal: Not possible to fast-forward, aborting.fixed in v1.5.6+
Cause. Your local checkout and the upstream repository no longer share a straight line of history — usually because upstream history was rewritten, leaving your HEAD on a commit that no longer exists there.
Fix, for any version. Re-run the installer. It detects this exact case, rescues your config.json, realigns with upstream and restores your settings:
curl -fsSL https://raw.githubusercontent.com/jasoncheng7115/jt-gelflow/main/install.sh | sudo JT_GELFLOW_YES=1 bashYour settings are safe. config.json is rescued before the realignment and copied back afterwards — it is never overwritten by an upgrade.
Before v1.5.6 the CLI ran a bare git pull --ff-only with no fallback, so jt-gelflow update simply stopped here. From v1.5.6 the CLI recovers on its own and you will see warn: fast-forward refused — realigning with origin/main (settings preserved) instead.
error: /opt/jt-gelflow is not a git checkout. Reinstall via install.sh.
jt-gelflow update upgrades by pulling git history, so it needs the .git directory. This appears when the directory was copied rather than cloned, or .git was deleted.
Re-running the installer repairs it. Your config.json is detected and preserved:
curl -fsSL https://raw.githubusercontent.com/jasoncheng7115/jt-gelflow/main/install.sh | sudo JT_GELFLOW_YES=1 bashThe upgrade succeeded but the browser still shows the old interface
The browser is serving a cached JavaScript bundle. Force-reload the page:
- Windows / Linux —
Ctrl+Shift+R - macOS —
Cmd+Shift+R
Confirm which version is actually running by opening Settings — the version is shown at the bottom of the panel. Compare it with:
grep -m1 version /opt/jt-gelflow/package.jsonWill upgrading overwrite my settings?
No. config.json is deliberately untracked by git; the repository ships config.example.json as the template, and the installer only copies it when no config.json exists. Every destructive step rescues the file first and restores it afterwards.
If you want a snapshot before upgrading anyway:
sudo cp -a /opt/jt-gelflow/config.json /opt/jt-gelflow/config.json.before-$(date +%F)Since v1.5.5 the file is stored with 0600 permissions, and re-running the installer tightens an older 0644 file without touching its contents.
How do I pin to a specific version, or go back to an older one?
Install a tagged release by setting JT_GELFLOW_BRANCH:
JT_GELFLOW_BRANCH=v1.5.8 \
curl -fsSL https://raw.githubusercontent.com/jasoncheng7115/jt-gelflow/v1.5.8/install.sh | sudo bashOr move an existing checkout:
cd /opt/jt-gelflow
sudo git fetch --tags origin
sudo git checkout v1.5.8
sudo systemctl restart jt-gelflow.serviceA pinned checkout has a detached HEAD. From v1.5.6, jt-gelflow update realigns it onto main rather than failing — so run it only when you want to stop tracking that tag.
Service will not start
jt-gelflow status shows failed, inactive, or a restart loop.
Where do I look first?
These two commands answer almost every start-up failure:
sudo jt-gelflow status
sudo journalctl -u jt-gelflow.service -n 200 --no-pagerThe journal carries the Python traceback, which names the actual cause on its last line.
ModuleNotFoundError: No module named 'aiohttp' after upgrading Ubuntu, restarting every 5 secondsafter an OS upgrade
Cause. The OS upgrade moved the system to a new Python — Ubuntu 22.04 → 24.04 goes from 3.10 to 3.12, and 24.04 → 26.04 to 3.14. JT-GELFLOW's dependencies were installed with pip for the old version, under /usr/local/lib/python3.10/, and the new interpreter does not look there. The service fails at import, and Restart=on-failure retries it every five seconds.
It is easy to miss at first. Until the machine reboots, the old process keeps running from memory, so the dashboard still works right after the upgrade and only breaks at the next reboot.
Fix. Reinstall the dependencies for the new Python and restart — one command:
sudo jt-gelflow updateYour config.json is not touched. Re-running the installer works too. A plain pip install -r requirements.txt is refused on Ubuntu 24.04+ with externally-managed-environment; jt-gelflow update already passes the flag that lets it through.
The old /usr/local/lib/python3.10/ directory is no longer used — leave it or delete it.
From v1.5.8 the journal names this directly instead of showing a bare traceback: JT-GELFLOW cannot start: Python module 'aiohttp' is not installed for Python 3.12, followed by the fix.
OverflowError: bind(): port must be 0-65535 or OSError: Int or String expected, restarting foreverfixed in v1.5.5+
Cause. config.json holds a port value that is not a usable port. Before v1.5.5 the API accepted any value and wrote it to disk, so this could be triggered by a single bad request; the service then failed at the next start and, under Restart=on-failure, looped.
Fix. Edit the file and set sane ports:
sudo nano /opt/jt-gelflow/config.json
# "http_port": 8099, "gelf_udp_port": 12201, "gelf_tcp_port": 12202
sudo systemctl restart jt-gelflow.serviceIf the file is beyond repair, start over from the shipped template — this discards your settings:
sudo cp /opt/jt-gelflow/config.example.json /opt/jt-gelflow/config.json
sudo chmod 600 /opt/jt-gelflow/config.json
sudo systemctl restart jt-gelflow.servicev1.5.5 closes the hole at both ends: the API now refuses an invalid value with 400, and on start-up an out-of-range field falls back to its default instead of crashing, so an affected install recovers by itself on upgrade.
OSError: [Errno 98] Address already in use
Another process holds 8099, 12201 or 12202. Find it:
sudo ss -tulpn | grep -E ':8099|:12201|:12202'Then either stop that process, or move JT-GELFLOW by editing http_port / gelf_udp_port / gelf_tcp_port in /opt/jt-gelflow/config.json and restarting. Remember to repoint your Graylog GELF Output if you change a GELF port.
A stale copy of JT-GELFLOW itself is a common culprit — a manually started python3 run.py left running alongside the service.
jt-gelflow logs shows nothing even though the service is runningfixed in v1.5.5+
Before v1.5.5 the unit ran Python with its output going to the journal — a pipe — so Python block-buffered stdout and held messages in a 4 KB buffer. On a quiet install nothing appeared until the buffer filled or the process exited.
v1.5.5 adds Environment=PYTHONUNBUFFERED=1 to the unit. Upgrade, then confirm:
systemctl show jt-gelflow.service -p EnvironmentI installed with JT_GELFLOW_DIR and the service points at /opt/jt-gelflowfixed in v1.5.7+
Before v1.5.7 the installer copied the systemd unit verbatim, and that file hardcodes /opt/jt-gelflow in WorkingDirectory, ExecStart and ReadWritePaths — so a custom directory got a unit pointing somewhere else. Since v1.5.5 tightened the unit to ProtectSystem=strict, the wrong ReadWritePaths also left the server unable to write its own config.json.
Upgrading to v1.5.7+ and re-running the installer rewrites all three directives. To check:
grep -E 'WorkingDirectory|ExecStart|ReadWritePaths' \
/etc/systemd/system/jt-gelflow.serviceInstalls at the default location were never affected.
Cannot reach the web interface
The service is running but the dashboard will not load.
connection refused on port 8099
Work outward from the box. First, is it listening at all?
sudo ss -tlnp | grep 8099
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8099/api/configA 200 locally means the server is fine and the problem is the network path — see the next entry. No listener means the service is not actually up; check sudo jt-gelflow status.
Works locally but not from another machine
A firewall is the usual answer. JT-GELFLOW binds 0.0.0.0, so it is not the server.
# ufw
sudo ufw allow 8099/tcp
sudo ufw allow 12201/udp
sudo ufw allow 12202/tcp
# firewalld
sudo firewall-cmd --permanent --add-port=8099/tcp
sudo firewall-cmd --reloadOpen these deliberately. The API has no authentication — anyone who can reach 8099 can read your observed network topology and change the configuration. Expose it only on a trusted network, or put it behind an authenticating reverse proxy.
The page loads but the status light stays red (WebSocket disconnected)
Behind a reverse proxy, the /ws upgrade needs to be passed through. For nginx:
location / {
proxy_pass http://127.0.0.1:8099;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
}A short proxy_read_timeout drops an idle socket and shows the same symptom.
Since v1.5.5 the handshake also rejects cross-origin connections with 403. If your proxy rewrites Host so that it no longer matches the Origin the browser sends, the socket is refused — keep proxy_set_header Host $host; as above.
CORS errors in the browser console after upgrading to v1.5.5behaviour change
v1.5.5 removed CORS entirely. It had been configured to reflect any origin while also allowing credentials, which let any website the operator visited read every API response and change the configuration.
The bundled frontend is served from the same origin and never needed it, so a normal install is unaffected. If you serve the frontend separately — a Vite dev server, for instance — proxy the API instead of calling it cross-origin:
// vite.config.ts
server: {
proxy: {
'/api': 'http://127.0.0.1:8099',
'/ws': { target: 'ws://127.0.0.1:8099', ws: true },
},
}My monitoring check started returning 404 after v1.5.5behaviour change
Unknown /api/* paths now return 404 with a JSON body instead of falling through to the single-page app and returning 200 with HTML. A side effect is that API routes only answer their own verb, so a HEAD probe returns 404.
Use GET for health checks:
curl -fsS -o /dev/null http://127.0.0.1:8099/api/configNo data, or the wrong data
The dashboard loads but the views stay empty or look wrong.
The interface works but no traffic ever appears
Check in this order.
1. Are GELF messages arriving at all? Send a test message from the box:
printf '{"version":"1.1","host":"test","short_message":"t","source_ip":"8.8.8.8","destination_ip":"10.0.0.10","network_bytes":100}' \
| nc -u -w1 127.0.0.1 12201If that node appears, the collector is fine and Graylog is not reaching you — check its GELF Output host, port and protocol, and your firewall.
2. Do the field names match? This is the most common cause. Open Settings → Field Mapping and make sure src_field and dst_field are the field names your logs actually carry. Settings → Discovered Fields lists what has genuinely been received, with sample values.
3. Is a filter hiding everything? Internal Filter IPs and Top-N limits apply per view; an aggressive filter can empty a view completely.
All internal addresses sit on one dot on the 2D Map and 3D Globe
This is expected. Private addresses have no public geolocation, so they all fall back to the single coordinate pair in Settings → GeoIP (internal_fallback_lat / internal_fallback_lng).
For a multi-site network, enrich the logs upstream so each internal address arrives with its own location. A Graylog pipeline rule can set source_ip_geolocation per subnet, and JT-GELFLOW will use it in preference to the fallback.
The Sankey view is empty, or every band is the same width
Empty usually means the columns it needs are not populated. Sankey requires an external and an internal address; if your zone CIDRs do not classify traffic correctly, nothing pairs up. Check Settings → Zones → internal CIDRs.
Uniform width means there is no byte field to weigh the bands with. When value_field is missing from every message, each flow contributes value_default (1), so width equals event count. Either point value_field at the field that carries byte counts, or set Settings → Sankey width to events to make that explicit.
Settings and configuration
Where things live and what the server will accept.
Where is the configuration file?
/opt/jt-gelflow/config.json # your settings (never tracked by git)
/opt/jt-gelflow/config.example.json # shipped templateSince v1.5.5 the live file is mode 0600. Edit it directly only while the service is stopped, or use the Settings panel — the server rewrites the whole file when it saves.
Saving settings returns Invalid value for 'X'
Working as intended, since v1.5.5. The named field received a value the server cannot run with — a port outside 1–65535, text where a number belongs, a latitude past the poles. Nothing was written; correct that one field and save again.
Ranges are deliberately permissive: only genuinely impossible values are rejected, so an upgrade never silently rewrites a setting you rely on.
How do I reset everything to defaults?
sudo systemctl stop jt-gelflow.service
sudo cp -a /opt/jt-gelflow/config.json /opt/jt-gelflow/config.json.bak
sudo cp /opt/jt-gelflow/config.example.json /opt/jt-gelflow/config.json
sudo chmod 600 /opt/jt-gelflow/config.json
sudo systemctl start jt-gelflow.serviceThe backup line is worth keeping — field mappings are tedious to reconstruct.
How do I remove JT-GELFLOW completely?
sudo jt-gelflow uninstall # keeps config.json
sudo jt-gelflow uninstall --purge # removes everything, config includedAfter a plain uninstall, re-running the installer later finds the surviving config.json and seeds the fresh checkout from it — removing and reinstalling is safe.
No match
Try a shorter phrase, or paste the exact error text.