Every web server quietly writes down everything that happens to it. Nginx logs every request to access.log, but a raw log file is almost useless when you actually want to answer a question: which pages are popular, where is the traffic coming from, which bot is hammering you, and why did bandwidth spike last night? GoAccess turns that log file into a live, interactive dashboard you can open in a browser, and it does it without shipping your data off to a third-party analytics service.
The catch is the “live” part. GoAccess generates its real-time dashboard by opening a WebSocket connection from the browser back to a small server that GoAccess runs. That WebSocket has to reach GoAccess somehow, and exposing an extra raw port to the internet is both ugly and insecure. The clean answer is to put Nginx in front of it as a reverse proxy: Nginx serves the HTML report over a normal port and forwards the WebSocket traffic to GoAccess internally. This guide walks through that setup end to end on a modern Ubuntu server.
This tutorial is for developers and sysadmins who already run Nginx and want self-hosted analytics they fully control. If you plan to expose the dashboard publicly, you should also lock it down, and I cover one solid way to do that in my guide on protecting an Nginx location with authentication.
Conceptual Overview
It helps to understand the moving parts before touching any config, because almost every problem people hit with GoAccess comes from misunderstanding how the real-time report connects.
There are three pieces:
- The log file. Nginx continuously appends request lines to
/var/log/nginx/access.log. GoAccess reads this file and parses each line. - The GoAccess process. When you run GoAccess in real-time mode, it does two things at once: it writes an HTML report to disk, and it starts a small WebSocket server (on port
7890by default) that pushes live updates to any browser viewing that report. - The browser. When you open the HTML report, the JavaScript inside it opens a WebSocket back to GoAccess. As new requests hit Nginx, GoAccess pushes updates over that socket and the charts move in real time.
The reason we need Nginx in the middle is that the browser’s WebSocket must reach the GoAccess WebSocket server. Rather than exposing port 7890 directly, we let Nginx serve the HTML file on a normal port and proxy the WebSocket (/ws) through to GoAccess on 127.0.0.1:7890. The browser only ever talks to Nginx; GoAccess stays bound to localhost.
The single most important detail in the entire setup is this: the WebSocket URL baked into the HTML report (the ws-url setting) must point at the address the browser can reach, which is Nginx, not the internal GoAccess port. Get that one value wrong and the report loads but never updates.
Prerequisites
Before you start, make sure you have:
- An Ubuntu server (this guide was tested on Ubuntu 24.04 LTS, and the steps are identical on 22.04).
sudoor root access.- A domain name pointing at the server if you want to reach the dashboard by name. You can substitute the server IP for quick local testing.
- Nginx already producing logs in the default COMBINED format, which is what a fresh Nginx install uses out of the box.
Everything below assumes you are running as root. If you are not, prefix the commands with sudo, or open a root shell once:
sudo -i
Step 1: Install Nginx and GoAccess
Install both packages from the distribution repositories. The version of GoAccess in current Ubuntu repositories is recent enough for this guide, but if you want the very latest release (1.9.x at the time of writing) GoAccess publishes an official APT repository, shown in the box below.
apt-get update
apt-get install -y nginx goaccess
Confirm GoAccess installed and check its version:
goaccess --version
GoAccess - 1.9.4
If your distribution ships an older build and you want the newest features, use the official repository instead:
wget -O - https://deb.goaccess.io/gnugpg.key | gpg --dearmor | tee /usr/share/keyrings/goaccess.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/goaccess.gpg arch=$(dpkg --print-architecture)] https://deb.goaccess.io/ $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/goaccess.list
apt-get update
apt-get install -y goaccess
Step 2: Configure the Nginx Reverse Proxy
Now create a virtual host that does two jobs: serve the HTML report from a web root, and proxy the WebSocket to GoAccess.
Create the vhost file:
nano /etc/nginx/sites-available/goaccess
Paste this configuration:
# Map the Upgrade header so WebSocket connections are handled correctly
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# The internal GoAccess WebSocket server
upstream gwsocket {
server 127.0.0.1:7890;
}
server {
listen 8080;
# Replace with your actual domain (or server IP for local testing)
server_name your.domain.com;
# Where GoAccess will write the HTML report
root /var/www/goaccess;
location / {
try_files $uri =404;
}
# Proxy the WebSocket through to GoAccess
location /ws {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://gwsocket;
proxy_buffering off;
proxy_read_timeout 7d;
}
}
A few things worth understanding rather than just copying:
- The
mapblock translates the HTTPUpgradeheader into theConnectionheader value Nginx needs to switch a normal request into a WebSocket. Without it, the proxy never upgrades the connection and the live updates silently fail. - The
/wslocation is the only thing that reaches GoAccess. Everything else just serves static files from the web root. proxy_read_timeout 7dkeeps the long-lived WebSocket from being closed by Nginx after the default 60 seconds of “inactivity.”
Create the web root that the root directive points at:
mkdir -p /var/www/goaccess
Enable the site by symlinking it into sites-enabled, then test and reload Nginx:
ln -s /etc/nginx/sites-available/goaccess /etc/nginx/sites-enabled/
nginx -t
systemctl reload nginx
If nginx -t reports syntax is ok and test is successful, the proxy side is ready.
Note on the original recipe. Older versions of this guide referenced
/etc/nginx/site-availableandsites-enable. The correct directory names aresites-availableandsites-enabled(both plural). A wrong path here is the most common reason the vhost never loads.
Step 3: Configure GoAccess
GoAccess reads its settings from /etc/goaccess/goaccess.conf (on some older builds it lives at /etc/goaccess.conf). Open it:
nano /etc/goaccess/goaccess.conf
Find and uncomment (or add) the following options. These tell GoAccess how to parse the Nginx log and how to run the real-time report:
time-format %H:%M:%S
date-format %d/%b/%Y
log-format COMBINED
real-time-html true
ws-url ws://your.domain.com:8080/ws
port 7890
keep-last 30
db-path /var/lib/goaccess
keep-db-files true
load-from-disk true
Here is why each setting matters:
- log-format COMBINED matches the default Nginx access log format. If you customized your log format, set this accordingly instead.
- real-time-html true is what turns on the live dashboard and the WebSocket server.
- ws-url ws://your.domain.com:8080/ws is the address the browser connects to. Notice it points at your domain on port 8080 (the Nginx listener) and path
/ws, not at port 7890. This is the value people get wrong most often. Changeyour.domain.comto your real domain or server IP. - port 7890 is the internal WebSocket port that Nginx proxies to. It stays on localhost.
- keep-db-files / load-from-disk / db-path persist parsed data to disk so your stats survive restarts instead of resetting to zero every time.
- keep-last 30 caps the in-memory data to the last 30 days, which keeps memory usage sane on a busy server.
Save and exit.
Important: if your dashboard is served over HTTPS (which it should be in production), the browser will refuse an insecure
ws://connection. In that case usewss://and drop the port so it goes over 443, for examplews-url wss://your.domain.com/ws. There is a full TLS example in the Best Practices section below.
Step 4: Generate the Live Report
With both sides configured, start GoAccess pointed at the Nginx log and writing into the web root:
goaccess /var/log/nginx/access.log -o /var/www/goaccess/index.html
Because real-time-html is enabled in the config, this command does not exit. It parses the existing log, writes index.html, and keeps running to push live updates over the WebSocket. Leave it running for now and open a second terminal for the next step.
Generate a little traffic so there is something to see (replace the domain accordingly):
curl -s http://your.domain.com:8080/ >/dev/null
Step 5: Open the Dashboard
In your browser, visit:
http://your.domain.com:8080/
You should see the GoAccess dashboard. Look in the top corner for the live indicator: when the WebSocket is connected it shows a small green dot or a “real-time” label. Refresh a few pages on your site and watch the request counts climb without reloading the dashboard. That movement is the proof that the proxied WebSocket is working end to end.
Step 6: Keep It Running With systemd
Running GoAccess by hand in a terminal works for a demo, but it dies the moment you close the session. For anything real, run it as a systemd service so it starts on boot and restarts if it crashes.
Create the unit file:
nano /etc/systemd/system/goaccess.service
[Unit]
Description=GoAccess real-time web log analyzer
After=network.target
[Service]
ExecStart=/usr/bin/goaccess /var/log/nginx/access.log -o /var/www/goaccess/index.html
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Make sure daemonize is not enabled in goaccess.conf, because systemd needs the process to stay in the foreground to supervise it. Then enable and start the service:
systemctl daemon-reload
systemctl enable --now goaccess
systemctl status goaccess
A active (running) status means GoAccess will now keep your dashboard alive across reboots and crashes.
Common Mistakes and Troubleshooting
The report loads but never updates (no live data). This is almost always a wrong ws-url. Open the browser developer console (F12) and look at the Network tab for a failed WebSocket. If it is trying to reach port 7890 or the wrong host, fix ws-url in goaccess.conf to point at your Nginx address and /ws path, then restart GoAccess. Remember the report must be regenerated for the new URL to take effect.
WebSocket connection fails over HTTPS. A page served over https:// cannot open an insecure ws:// socket; browsers block mixed content. Switch ws-url to wss:// and proxy it through your TLS server block, as shown in Best Practices below.
nginx -t fails or the site never appears. Double-check the symlink path. It must be /etc/nginx/sites-enabled/, plural, pointing at the file in /etc/nginx/sites-available/. A typo here means Nginx never loads the vhost at all.
Token or unable-to-parse errors on startup. Your log-format does not match the actual Nginx log. If you customized log_format in Nginx, set the matching format string in goaccess.conf instead of COMBINED. You can confirm the first log line with head -n1 /var/log/nginx/access.log and compare.
Stats reset to zero after a restart. You did not persist the database. Make sure db-path, keep-db-files true, and load-from-disk true are all set, and that the db-path directory exists and is writable.
Permission denied reading the log. GoAccess must be able to read /var/log/nginx/access.log. Running the service as root (as in the unit above) avoids this, but if you run it as a non-root user, add that user to the adm group which owns the Nginx logs.
Best Practices
Never expose the dashboard unprotected. Your access log reveals visitor IPs, requested URLs, and traffic patterns. At minimum, put it behind authentication. The simplest approach is HTTP Basic Auth, and a more robust database-backed variant is in my guide on Nginx authentication. Apply the auth_basic directives to the location / block of the GoAccess vhost.
Serve it over HTTPS with a proxied secure WebSocket. In production, run the dashboard on 443 with a real certificate and proxy the WebSocket as wss. The server block looks like this, and the matching config line is ws-url wss://your.domain.com/ws:
server {
listen 443 ssl;
server_name your.domain.com;
ssl_certificate /etc/letsencrypt/live/your.domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your.domain.com/privkey.pem;
root /var/www/goaccess;
location / {
try_files $uri =404;
}
location /ws {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://gwsocket;
proxy_buffering off;
proxy_read_timeout 7d;
}
}
You can obtain the certificate for free with Certbot/Let’s Encrypt.
Persist the database and cap retention. Keep keep-db-files, load-from-disk, and a sensible keep-last so the dashboard survives restarts without growing unbounded on a high-traffic server.
Match your real log format. If you run a custom Nginx log_format, mirror it exactly in goaccess.conf. Mismatched formats are the number one cause of empty or garbled reports.
Bind GoAccess to localhost only. Leave the WebSocket server on 127.0.0.1:7890 and let Nginx be the only public entry point. There is no reason to expose port 7890 to the internet.
Conclusion
You now have a self-hosted, real-time analytics dashboard: Nginx serves the GoAccess HTML report and proxies its WebSocket, GoAccess parses the access log live, and a systemd service keeps the whole thing running across reboots. No third-party tracker, no JavaScript injected into your pages, just your own server logs turned into something you can actually read.
The one idea worth remembering is the data flow: the browser only ever talks to Nginx, and Nginx forwards the WebSocket to GoAccess on localhost. Once that mental model is clear, the ws-url setting stops being mysterious and the whole setup becomes obvious.
From here, lock the dashboard down with authentication and TLS before exposing it, then explore GoAccess’s other tricks: parsing multiple log files at once, generating static HTML reports on a schedule, or feeding it logs from other services entirely.
Sources: GoAccess FAQ, GoAccess Man Page, GoAccess Release Notes