Hello,
Looking around the forum I found several caddy + goaccess integration example, but none that works with caddy v2. Did anyone already managed to have this integration active, both for the log analysis and the websocket redirection ?
1. The problem Iβm having:
Examples of integration of goaccess and caddy are relying on caddy v1
2. Error messages and/or full log output:
3. Caddy version:
v2.10.0
4. How I installed and ran Caddy:
a. System environment:
Linux, Ubuntu, systemd
b. Command:
PASTE OVER THIS, BETWEEN THE ``` LINES.
Please use the preview pane to ensure it looks nice.
c. Service/unit/compose file:
PASTE OVER THIS, BETWEEN THE ``` LINES.
Please use the preview pane to ensure it looks nice.
d. My complete Caddy config:
PASTE OVER THIS, BETWEEN THE ``` LINES.
Please use the preview pane to ensure it looks nice.
5. Links to relevant resources:
timelordx
(timelordx)
June 10, 2025, 1:01am
2
Just a quick example setup on my Mac:
Install Caddy via Homebrew:
brew install caddy
caddy version
v2.10.0 h1:fonubSaQKF1YANl8TXqGcn4IbIRUDdfAkpcsfI/vX5U=
Install GoAccess via Homebrew:
brew install goaccess
goaccess -V
GoAccess - 1.9.4.
For more details visit: https://goaccess.io/
Copyright (C) 2009-2024 by Gerardo Orellana
Build configure arguments:
--enable-geoip=mmdb
File structure :
tree -L2 /REDACTED/Caddy
/REDACTED/Caddy
βββ data
β βββ caddy
βββ etc
β βββ Caddyfile
βββ logs
β βββ access_log.json
βββ maxmind
β βββ GeoLite2-Country.mmdb
βββ www
βββ index.html
βββ stats
Caddyfile
{
http_port 8080
}
:8080 {
root * /REDACTED/Caddy/www
file_server
log {
output file /REDACTED/Caddy/logs/access_log.json {
roll_size 10MB
roll_keep 2
}
format json
}
}
Run Caddy:
caddy run --config /REDACTED/Caddy/etc/Caddyfile
Run goAccess:
goaccess /REDACTED/Caddy/logs/access_log.json -o /REDACTED/Caddy/www/stats/index.html --real-time-html --log-format=CADDY --geoip-database=/REDACTED/Caddy/maxmind/GeoLite2-Country.mmdb
Visit http://localhost:8080/stats/
:
Obviously, tweak it as needed to fit your setup. It took me longer to write this up than to get it running
timelordx
(timelordx)
June 10, 2025, 1:47am
3
And of course, I missed the important question you asked - the GoAccess WebSocket. Sorry about that!
Letβs assume your GoAccess stats site is stats.example.com
, and your WebSocket will run on ws.example.com
.
Caddyfile
:
stats.example.com {
root * /REDACTED/Caddy/www
file_server
log {
output file /REDACTED/logs/access_log.json {
roll_size 10MB
roll_keep 2
}
format json
}
}
ws.example.com {
reverse_proxy 127.0.0.1:7890
}
Run goAccess:
goaccess /REDACTED/Caddy/logs/access_log.json -o /REDACTED/Caddy/www/index.html --real-time-html --log-format=CADDY --geoip-database=/REDACTED/Caddy/maxmind/GeoLite2-Country.mmdb --ws-url=wss://ws.example.com:443
Note the --ws-url=wss://ws.example.com:443
option. That tells GoAccess to use the WebSocket endpoint, which is handled by the ws.example.com
Caddy virtual host proxying to GoAccess.
1 Like
timelordx
(timelordx)
June 10, 2025, 7:23am
4
Alright, Iβve been playing around with GoAccess stats today and put together a full demo setup.
Iβm using user ID 1050
and group ID 1050
in this example, so be sure to adjust the IDs, TZ
, and paths as needed for your own environment.
Caddy Docker Compose:
services:
caddy:
image: timelordx/caddy-dns-acmedns:latest
container_name: caddy
environment:
- TZ=PST8PDT
volumes:
- /volume/docker/caddy/etc/caddy:/etc/caddy:ro
- /volume/docker/caddy/config:/config
- /volume/docker/caddy/data:/data
- /volume/docker/caddy/logs:/logs
- /volume/docker/caddy/www:/www:ro
network_mode: host
restart: unless-stopped
MaxMind Docker Compose:
services:
maxmind:
image: ghcr.io/maxmind/geoipupdate
container_name: maxmind
user: "1050:1050"
environment:
- GEOIPUPDATE_ACCOUNT_ID=REDACTED
- GEOIPUPDATE_LICENSE_KEY=REDACTED
- 'GEOIPUPDATE_EDITION_IDS=GeoLite2-ASN GeoLite2-City GeoLite2-Country'
- GEOIPUPDATE_FREQUENCY=72
volumes:
- /volume/docker/maxmind:/usr/share/GeoIP
restart: unless-stopped
GoAccess Docker Compose:
services:
goaccess:
image: allinurl/goaccess
container_name: goaccess
user: "1050:1050"
environment:
- TZ=PST8PDT
volumes:
- /volume/docker/goaccess/db:/db
- /volume/docker/caddy/logs:/logs:ro
- /volume/docker/caddy/www/stats:/www
- /volume/docker/maxmind:/maxmind:ro
command:
- '--geoip-database=/maxmind/GeoLite2-City.mmdb'
- '--log-file=/logs/access.log'
- '--log-format=CADDY'
- '--db-path=/db'
- '--persist'
- '--restore'
- '--real-time-html'
- '--output=/www/index.html'
- '--ws-url=wss://ws.example.com:443'
ports:
- 127.0.0.1:7890:7890
restart: unless-stopped
Caddyfile
:
########################################
## GLOBAL OPTIONS
########################################
{
email acme@example.com
}
########################################
## SNIPPETS
########################################
(default_imports) {
encode gzip zstd
## a bunch of headers and other optional stuff
}
########################################
## Default wildcard VIRTUAL used solely
## to obtain the wildcard certificate
########################################
*.example.com {
## TLS settings (ACMEDNS)
tls {
dns acmedns /etc/caddy/creds/acmedns-example.com.json
}
## Full Lockdown β Do Not Serve Any Content
abort
}
########################################
webstats.example.com {
########################################
import default_imports
@allowed_ips {
remote_ip private_ranges
}
handle @allowed_ips {
root * /www/stats/
file_server
}
handle {
abort
}
}
########################################
ws.example.com {
########################################
import default_imports
@allowed_ips {
remote_ip private_ranges
}
handle @allowed_ips {
reverse_proxy 127.0.0.1:7890
}
handle {
abort
}
}
########################################
## A couple of websites that generate
## log output to /logs/access.log
########################################
...
...
...
########################################
## Drop all HTTP requests for otherwise
## unhandled sites
########################################
http:// {
abort
}
In general:
MaxMind keeps the GeoIP database up to date
GoAccess listens only on the loopback interface
Iβm using my own timelordx/caddy-dns-acmedns
Docker image just for ACME-DNS support - this is only relevant to how I obtain TLS certificates
Caddy restricts access to the stats interface to local networks
Stats are served at webstats.example.com
, with the WebSocket endpoint at ws.example.com
You can easily adjust it all to your needs.
1 Like
Mohammed90
(Mohammed Al Sahaf)
June 10, 2025, 9:37am
5
@timelordx , would you please copy those posts into a post under wiki category?
2 Likes
timelordx
(timelordx)
June 10, 2025, 7:18pm
6
Sounds good! Iβll clean it up this week and post it as a wiki article.
2 Likes
Would it be possible to use this without your custom caddy build / with the regular image or is it too specific ? (not sure what in your configuration required the custom image)
timelordx
(timelordx)
June 11, 2025, 8:39pm
8
You definitely can use the regular Caddy image.
As I mentioned in my previous post:
Iβm using my own timelordx/caddy-dns-acmedns
Docker image just for ACME-DNS support - this is only relevant to how I obtain TLS certificates
I simply copy-pasted my running setup as an example. In that setup, Iβm using a wildcard certificate obtained via the ACME-DNS module, which is the only reason for the custom image.
That said, Iβve since simplified the configuration and removed the separate WebSockets site. Everything is now served under a single domain:
This setup lets you analyze Caddy access logs with GoAccess and view real-time web stats via webstats.example.com, served directly by your Caddy server. It also supports WebSockets through the same domain.
Components (Docker Images) Used:
Caddy web server
GoAccess
MaxMind GeoIP (optional)
Caddy Docker Compose:
services:
caddy:
image: caddy:latest
container_name: caddy
volumes:
- /volume/docker/caddy/etc/caddy:/etc/caddy:ro
- /volume/docker/caddy/config:/config
β¦
1 Like