How to Run GoAccess Behind an Nginx Proxy for Real-Time Web Analytics

Written by: Bagus Facsi Aginsa
Published at: 17 Apr 2020


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 7890 by 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).
  • sudo or 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 map block translates the HTTP Upgrade header into the Connection header 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 /ws location is the only thing that reaches GoAccess. Everything else just serves static files from the web root.
  • proxy_read_timeout 7d keeps 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-available and sites-enable. The correct directory names are sites-available and sites-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. Change your.domain.com to 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 use wss:// and drop the port so it goes over 443, for example ws-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