Skip to content

I Deleted My Dashboard, Then Called It Back

Adityo Guni Waluyo

I tore out my self-built GoAccess stack for 3proxy's built-in admin counters, then realized counters can't answer which URLs eat the bandwidth.

TL;DR

The author deleted a self-built GoAccess dashboard for 3proxy, replacing it with the built-in admin directive and secure-mode counters. That answered how much traffic, but not which URLs or hosts consumed it. GoAccess returned properly: stripped fractional timestamps, filtered ADMIN lines, and nginx proxying WebSocket with explicit upgrade headers and longer read timeouts.

# I Deleted My Dashboard, Then Called It Back

My 3proxy dashboard was a stack I had assembled myself: a tail -F pipe into an awk feeder, a hand-tuned GoAccess log-format, nginx in front. It worked. But the same repo also had a day/night config pair swapped by cron with kill -HUP to reset the bandlimit, so every change meant keeping two config paths in sync. That Saturday afternoon I looked at the whole thing and decided the dashboard was the heavy part. One commit later, the GoAccess stack was gone, replaced by three lines of built-in 3proxy config [1]:

counter /var/log/3proxy/3proxy.count
countall 1 H 1000000 * * * * * *
admin -p3224 -i0.0.0.0 -s

3proxy ships a built-in web admin through its admin config directive [1]. The -s flag puts it in secure mode: traffic counters view only, no ability to reset anything [1]. The counter file is where traffic data is stored permanently [5], so it needs a writable volume; in my compose the log volume does double duty. Port 3224 published, and I was done. No nginx, no extra container, no cron.

Two commits and roughly an hour later, the question changed. Not "how much traffic today" but "which URLs are eating the bandwidth?" The counters answer how much. Host breakdown, path breakdown, which request was the hungriest: none of that is there [1]. Secure mode is counters view, period.

My wrong guess: assuming the counters covered every monitoring need. It turns out "how much" and "where" need different tools.

The Return, Done Properly

GoAccess had to come back. This time not a copy of the old config. Three things I had underestimated the first time:

The image ENTRYPOINT. The allinurl/goaccess image runs goaccess directly as its entrypoint. My startup script never runs unless I override the entrypoint to /bin/sh /dashboard.sh.

The timestamp format. 3proxy log lines start with an epoch timestamp plus a fractional part, and GoAccess expects %f as the time format for microsecond timestamps [2]. I strip the fraction before the parser sees it, with awk, and configure the rest through GoAccess:

time-format %s
date-format %s
log-format %x %^ %^ %^ %h:%^ %^ %b %^ %^ %U

Result: 0 invalid lines. The ADMIN lines from port 3224 (nine fields, no request) get filtered too so the parser stays clean.

-o file mode is WebSocket-only. In realtime mode GoAccess serves WebSocket on port 7890; a plain GET gets a 400. So nginx:alpine sits in front: serve the static report.html, proxy /ws to GoAccess. The Upgrade and Connection headers must be set explicitly, because they are hop-by-hop headers nginx does not forward by default [3]. Then proxy_read_timeout goes up to 86400 seconds; the 60-second default kills idle connections [3]. GoAccess 1.11, released July 19, 2026, reconnects to the WebSocket with exponential backoff starting at 1 second for up to 20 attempts [4], but I prefer not to lean on that every minute.

One detail that makes the stack tidy: the threeproxy-log volume is shared. 3proxy writes the access log and the counter file there, GoAccess reads the same log from its /log mount, and nginx serves report.html straight from the same volume. No log copying between containers. In nginx, I copied the map $http_upgrade $connection_upgrade block from the official documentation verbatim, because the value has to be dynamic: WebSocket connections get upgrade, plain requests get close [3].

Final check: GET on port 3225 returns 200, total_requests=68, failed=0. Two tools, one question each.

My take now: run both. The built-in admin is the quick check: open 3224, read the numbers, close the tab. GoAccess is for the per-URL and per-host breakdown. I used to run GoAccess for a question two lines of counters should answer; now I never ask the counters the "where" question.

Both earn their place. One answers how much, the other answers where.

Related articles