A self-hosted, real-time network edge visibility dashboard. HawkEYE sits at the boundary of your homelab or VPS and visualises incoming traffic as animated arcs on a live world map, alongside a Fail2Ban threat shield showing active bans and jail status.
Built for homelabbers who run a reverse proxy at the edge of their network and want a public-facing, GDPR-friendly showcase of what's hitting it.
- Live world map with animated arcs showing where requests originate
- Colour-coded by HTTP status (2xx / 3xx / 4xx / 5xx)
- Real-time request feed showing country, city, method and URI
- Stats panel: total requests, unique IPs, top origin countries, RPS, uptime
- WebSocket-powered — updates instantly as traffic hits your server
- No IP addresses displayed publicly (GDPR-friendly)
- Live Fail2Ban jail status
- Shows currently banned count, total bans, failed attempts per jail
- Auto-refreshes every 60 seconds
- No banned IPs displayed publicly (GDPR-friendly)
- Dark/light theme toggle with localStorage persistence
- Smooth fade transitions between pages
- Retro terminal aesthetic — Space Mono + Syne fonts, scanline overlay, green glow
- Fully responsive with mobile drawer layout
- A VPS or server running Caddy as a reverse proxy (native install)
- Docker and Docker Compose installed
- Fail2Ban installed natively (optional — Threat Shield won't work without it)
- A domain pointed at your VPS
Before deploying, if you're using Fail2Ban, you need two one-time systemd configurations to ensure the socket is always accessible to the Docker container across reboots and Fail2Ban restarts.
Step 1 — Fix socket permissions on every Fail2Ban start:
sudo mkdir -p /etc/systemd/system/fail2ban.service.d
sudo nano /etc/systemd/system/fail2ban.service.d/socket-perms.confPaste:
[Service]
ExecStartPost=/bin/bash -c 'sleep 2 && chmod 666 /var/run/fail2ban/fail2ban.sock'Step 2 — Auto-recreate the HawkEYE backend when Fail2Ban restarts:
sudo nano /etc/systemd/system/hawkeye-backend-restart.servicePaste (update the path if your install isn't in /root/traffic-viz):
[Unit]
Description=Restart HawkEYE backend after Fail2Ban restart
After=fail2ban.service
[Service]
Type=oneshot
ExecStart=/bin/bash -c 'sleep 3 && docker compose -f /root/hawkeye/docker-compose.yml up -d --force-recreate backend'
[Install]
WantedBy=fail2ban.serviceStep 3 — Apply:
sudo systemctl daemon-reload
sudo systemctl enable hawkeye-backend-restart.service
sudo systemctl restart fail2banEvery time Fail2Ban restarts, the socket permissions are automatically fixed and the backend container is recreated with the fresh socket — no manual intervention needed.
Add to the top of your Caddyfile (global options block):
{
log {
output file /var/log/caddy/access.log {
roll_size 100mb
roll_keep 5
roll_keep_for 720h
}
format json
}
}Reload Caddy:
sudo systemctl reload caddygit clone https://github.com/YOUR_USERNAME/hawkeye.git
cd hawkeyeEdit docker-compose.yml and set your server's coordinates and display name:
environment:
SERVER_LAT: 51.5074 # Your server latitude
SERVER_LON: -0.1278 # Your server longitude
SERVER_CITY: "My Server" # Display name on the mapFind your coordinates at latlong.net.
docker compose up -d --buildAdd to your Caddyfile:
traffic.yourdomain.com {
reverse_proxy localhost:8080
}Reload Caddy:
sudo systemctl reload caddyVisit https://traffic.yourdomain.com — you should see the live map.
The Threat Shield page requires Fail2Ban to be installed natively on the host. The socket is mounted into the Docker container.
The socket path varies by distro. Check yours:
ls /var/run/fail2ban/fail2ban.sock
# or
ls /run/fail2ban/fail2ban.sockUpdate docker-compose.yml to match:
volumes:
- /var/run/fail2ban/fail2ban.sock:/run/fail2ban/fail2ban.sockIf Fail2Ban is not installed, the Threat Shield page will show an error — the Traffic Monitor will still work fine.
If you want to exclude certain subdomains from appearing in HawkEYE (e.g. internal tools, Jellyfin), add log { output discard } to those Caddy blocks:
jellyfin.yourdomain.com {
log {
output discard
}
reverse_proxy 10.0.0.2:8096
}HawkEYE has a built-in GDPR_MODE flag controlled via docker-compose.yml. This lets you toggle IP visibility without touching any code.
environment:
GDPR_MODE: "true" # IPs hidden — safe for public deployments (default)
GDPR_MODE: "false" # IPs visible — full detail for private/internal useWhen GDPR_MODE=true (default):
- No IP addresses shown in the live request feed
- No IP shown in map marker tooltips
- Fail2Ban Threat Shield shows ban counts only — no banned IPs listed
When GDPR_MODE=false:
- Full IP addresses shown in the live request feed
- IP shown in map marker tooltips
- Full banned IP list shown per jail on the Threat Shield page
The mode is read from the backend at runtime and applied to both pages automatically — no frontend changes needed when switching.
After changing GDPR_MODE, restart the backend to apply:
docker compose up -d --force-recreate backendFor a fully privacy-respecting public deployment, also consider:
- IP masking in Caddy logs — truncates IPs at source so they're never stored in full:
log {
format filter {
wrap json
fields {
request>remote_ip ip_mask {
ipv4 /24
ipv6 /48
}
}
}
}- Log retention — already partially configured with
roll_keep 5, addroll_keep_for 720hto cap storage at 30 days - Access control — add
basicauthin Caddy if you want to restrict who can view the dashboard
hawkeye/
├── backend/
│ ├── Dockerfile # Node.js 20 Alpine + fail2ban-client
│ ├── package.json
│ └── server.js # Express + WebSocket server, log tailer, GeoIP
├── frontend/
│ ├── index.html # Traffic Monitor (MapLibre GL map + live feed)
│ └── fail2ban.html # Threat Shield (Fail2Ban jail status)
├── nginx.conf # Nginx reverse proxy config for frontend → backend
├── docker-compose.yml # Two services: backend + frontend (nginx)
└── README.md
| Component | Technology |
|---|---|
| Backend | Node.js, Express, ws, chokidar, geoip-lite |
| Frontend | Vanilla JS, MapLibre GL, Space Mono + Syne (Google Fonts) |
| Map tiles | CartoCDN (dark/light) |
| Container | Docker Compose (Node 20 Alpine + nginx:alpine) |
| Reverse proxy | Caddy (native, not containerised) |
| Intrusion detection | Fail2Ban (native, socket-mounted) |
MIT — do whatever you want with it.