Repo: github.com/hrodrig/gghstats · Releases: Releases
Self-hosted dashboard and CLI for GitHub repository traffic stats. GitHub only keeps traffic for 14 days; gghstats keeps historical data indefinitely in SQLite.
If you want your own self-hosted deployment (Docker Compose, Traefik with TLS, Helm, optional Prometheus/Grafana/Loki), use the companion repo gghstats-selfhosted — it lists the supported options and example manifests.
Releases: GitHub Releases ship binaries (tarballs/zip + checksums). Multi-arch container images (linux/amd64, linux/arm64) are on GHCR as ghcr.io/hrodrig/gghstats:v<version> (same v prefix as the Git tag, e.g. v0.1.3) and :latest. Pushing a v* tag on main triggers the Release workflow (GoReleaser). Day-to-day work happens on develop (see Release workflow).
Live: gghstats.hermesrodriguez.com
- Demo
- Features
- Repository page charts
- Quick start
- Install
- Web UI assets (developers)
- Usage
- Examples
- Configuration
- Environment file
- Typical scenarios
- Deployments
- Troubleshooting
- Release workflow
- Security and quality
- Database
- Community standards
- Acknowledgments
- License
- Collects views, clones, referrers, popular paths, and star history
- Auto-discovers repositories (or filters by org/repo rules)
- Web dashboard with Chart.js graphs
- JSON API for external integrations
- CLI mode for fetch/report/export
- Single binary, SQLite storage, no external DB dependency
- Docker image on GHCR; Compose / Helm examples live in gghstats-selfhosted
On each repository’s detail page, the Clones and Views bar charts are stacked from GitHub’s daily traffic API:
| Segment | Meaning | GitHub field |
|---|---|---|
| Lower (theme primary color) | Unique visitors or cloners that day | uniques |
| Upper (theme info color) | Total views or total clones that day | count |
Exact colors depend on light/dark theme (Bootstrap --bs-primary / --bs-info, overridden in the app’s neo-brutalist CSS). The legend is hidden on the chart; use the tooltip on each bar for values.
cp .env.example .env
# Edit .env: set GGHSTATS_GITHUB_TOKEN (and optionally GGHSTATS_FILTER, GGHSTATS_PORT, etc.)
docker compose up -d --buildOpen http://localhost:8080.
The template .env.example lists variables for the Go binary and this dev Compose file. Production (Traefik, published image, Helm, observability) is in gghstats-selfhosted.
docker run -d \
-e GGHSTATS_GITHUB_TOKEN=ghp_xxx \
-e GGHSTATS_FILTER="your-github-user/*" \
-p 8080:8080 \
-v ./data:/data \
--name gghstats \
ghcr.io/hrodrig/gghstats:v0.1.3go install github.com/hrodrig/gghstats/cmd/gghstats@latest- Binary archives: Releases (pick OS/arch; verify
checksums.txt). - OCI image:
ghcr.io/hrodrig/gghstats:v0.1.3orghcr.io/hrodrig/gghstats:latest(image tag matches the Git release tag; multi-arch manifest).
git clone https://github.com/hrodrig/gghstats.git
cd gghstats
make installFavicons and the web app manifest live under assets/favicons/ and are embedded at build time via assets/embed.go (go:embed favicons/*). The HTTP server exposes each file under /static/<filename> (see table). Other UI assets (CSS, Bootstrap) remain under web/static/ via web/embed.go.
| File | Role |
|---|---|
assets/favicons/favicon.svg |
Source artwork (vector). Edit this when changing the mark; regenerate the raster files below. |
assets/favicons/favicon-16x16.png |
PNG 16×16 (tabs, legacy). |
assets/favicons/favicon-32x32.png |
PNG 32×32. |
assets/favicons/favicon.ico |
Multi-size ICO (16 + 32). |
assets/favicons/apple-touch-icon.png |
180×180 (iOS / “Add to Home Screen”). |
assets/favicons/android-chrome-192x192.png |
192×192 (PWA / Android). |
assets/favicons/android-chrome-512x512.png |
512×512 (PWA splash / install). |
assets/favicons/manifest.json |
Web app manifest (/static/manifest.json; linked from layout.html). |
Regenerating rasters after you change favicon.svg: from the repository root, with librsvg (rsvg-convert) and ImageMagick (magick) on your PATH:
SVG=assets/favicons/favicon.svg
rsvg-convert -w 16 -h 16 "$SVG" -o assets/favicons/favicon-16x16.png
rsvg-convert -w 32 -h 32 "$SVG" -o assets/favicons/favicon-32x32.png
rsvg-convert -w 180 -h 180 "$SVG" -o assets/favicons/apple-touch-icon.png
rsvg-convert -w 192 -h 192 "$SVG" -o assets/favicons/android-chrome-192x192.png
rsvg-convert -w 512 -h 512 "$SVG" -o assets/favicons/android-chrome-512x512.png
magick assets/favicons/favicon-16x16.png assets/favicons/favicon-32x32.png assets/favicons/favicon.icoCommit everything under assets/favicons/ together so all icons stay in sync.
export GGHSTATS_GITHUB_TOKEN="ghp_your_token"
gghstats serveServer behavior:
- Runs initial sync when database is empty
- Re-syncs on schedule (default
1h) - Serves dashboard on http://localhost:8080
- Stores data in
./data/gghstats.db - Liveness/readiness:
GET /api/v1/healthz→{"status":"ok"}(no auth; Kubernetes-style) - Prometheus:
GET /metrics(disable withGGHSTATS_METRICS=false) - Listen port:
GGHSTATS_PORT(default8080) orgghstats serve --port <port> - First stderr line on start: version, build date,
GOOS/GOARCH, listen address, masked GitHub token (XXXX....YYYY); then slog atGGHSTATS_LOG_LEVEL(defaultinfo). Every structured slog line is prefixed withgghstatsso it is easy to grep in shared log streams.
gghstats fetch --repo your-github-user/my-app --token "$GGHSTATS_GITHUB_TOKEN"
gghstats report --repo your-github-user/my-app --token "$GGHSTATS_GITHUB_TOKEN"
gghstats export --repo your-github-user/my-app --token "$GGHSTATS_GITHUB_TOKEN" --output traffic.csvGGHSTATS_GITHUB_TOKEN=ghp_xxx \
GGHSTATS_DB=./data/gghstats.db \
GGHSTATS_SYNC_INTERVAL=30m \
gghstats serveUse your repository as owner/repo (example below uses a placeholder).
gghstats fetch --repo your-github-user/my-app --token "$GGHSTATS_GITHUB_TOKEN"
gghstats report --repo your-github-user/my-app --token "$GGHSTATS_GITHUB_TOKEN" --days 14
gghstats export --repo your-github-user/my-app --token "$GGHSTATS_GITHUB_TOKEN" --days 30 --output traffic-30d.csvmake release-check STRICT_RELEASE=1make snapshot
make test-releaseAll runtime configuration uses env vars (serve) or flags (fetch/report/export).
- Template:
.env.example— copy to.envand fill in secrets..envis gitignored (dotfiles are excluded by default in this repo). - Compose:
docker composeloads.envfrom the project directory automatically.
| Variable | Default | Description |
|---|---|---|
GGHSTATS_GITHUB_TOKEN |
(required) | GitHub personal access token |
GGHSTATS_DB |
./data/gghstats.db |
SQLite database path |
GGHSTATS_HOST |
0.0.0.0 |
Bind address |
GGHSTATS_PORT |
8080 |
Listen port |
GGHSTATS_FILTER |
* |
Repo filter expression |
GGHSTATS_INCLUDE_PRIVATE |
false |
Include private repos |
GGHSTATS_SYNC_INTERVAL |
1h |
Sync frequency |
GGHSTATS_API_TOKEN |
(none) | Protect /api/* endpoints |
GGHSTATS_LOG_LEVEL |
info |
debug, info, warn, or error (slog only; startup banner always prints) |
GGHSTATS_METRICS |
(enabled) | Set to false to disable GET /metrics |
- Go to https://github.com/settings/tokens
- Generate a classic token
- Use
public_reposcope (orrepofor private repos)
Replace your-github-user with your GitHub username or organization, and my-app / other-repo / legacy-repo with your real repository names.
GGHSTATS_FILTER="your-github-user/*"
GGHSTATS_FILTER="your-github-user/my-app,your-github-user/other-repo"
GGHSTATS_FILTER="*,!fork"
GGHSTATS_FILTER="*,!archived"
GGHSTATS_FILTER="your-github-user/*,!fork,!archived"
GGHSTATS_FILTER="*,!your-github-user/legacy-repo"When GGHSTATS_API_TOKEN is configured:
curl -H "x-api-token: your-token" http://localhost:8080/api/reposexport GGHSTATS_FILTER="your-github-user/*"
gghstats serveexport GGHSTATS_FILTER="your-github-user/*,!fork,!archived"
gghstats serveexport GGHSTATS_API_TOKEN="my-api-token"
gghstats serve
curl -H "x-api-token: my-api-token" http://localhost:8080/api/reposgghstats export --repo your-github-user/my-app --days 30 --output traffic-30d.csvProduction and optional observability (Traefik + TLS, Prometheus / Grafana stack, Helm) live in a separate repository so release versioning applies to the application only. For self-hosted setups, start here:
github.com/hrodrig/gghstats-selfhosted
Clone that repo on your server, copy .env.example → .env, and follow its README for the deployment path you choose. For the optional metrics/logs stack, see run/docker-compose/observability/README.md (on the default branch).
Set GGHSTATS_GITHUB_TOKEN in your shell or .env file before running serve.
- Wait for the initial sync to finish.
- Verify filter rules (
GGHSTATS_FILTER) are not excluding all repos. - Confirm token scope includes repository metadata access.
Set another listen port via env or flag:
export GGHSTATS_PORT=9090
gghstats serve
# or: gghstats serve --port 9090Confirm request header exactly matches configured token:
curl -H "x-api-token: $GGHSTATS_API_TOKEN" http://localhost:8080/api/repos- Branch policy: day-to-day development on
develop; tagged releases are cut frommain. VERSIONfile: semantic version withoutv(for example0.1.3). Must match the static Version badge at the top of this README.- Git tags: annotated tag with
vprefix (for examplev0.1.3), on the commit you want released.
Pushing a tag matching v* runs .github/workflows/release.yml: make release-check, then goreleaser release --clean with GITHUB_TOKEN (releases + GHCR).
# 1) On develop: land changes, bump version if needed
git checkout develop
make release-check # optional: STRICT_RELEASE=1 (adds docker image scan)
make test-release # optional: dry-run GoReleaser + local Docker build
# 2) Update VERSION, README version badge, CHANGELOG; commit on develop
# 3) Merge into main (PR or fast-forward), then tag and push
git checkout main && git pull origin main
git merge --ff-only develop # or: merge via GitHub PR
git push origin main
git tag -a v0.1.3 -m "Release 0.1.3"
git push origin v0.1.3 # triggers Release workflow — builds and publishes artifactsFor the next release after 0.1.3, set VERSION to 0.1.4 (etc.), update the badge and CHANGELOG, then repeat with v0.1.4.
If you run GoReleaser locally instead of relying on CI, checkout main at the tagged commit, export GITHUB_TOKEN (or GH_TOKEN) with repo and packages access to push GHCR, then:
make release # runs release-check then goreleaser release --clean- Update
CHANGELOG.md(move[Unreleased]into the new version section). - Keep
VERSION(nov), README Version badge, and CHANGELOG in sync; the OCI tag uses the samevprefix as the Git tag. Deployment image pins live in gghstats-selfhosted. - Ensure CI and Security workflows are green before pushing the release tag.
- Docker:
Dockerfileis for localmake docker-build/docker-scan. GoReleaser usesDockerfile.release(pre-built Linux binaries; same pattern as multi-arch release images).
make tools
make lint
make test
make security
make release-checkSecurity tooling:
govulncheckgocyclo(complexity gate)grype(filesystem image/source scanning)
SQLite path comes from GGHSTATS_DB. Main tables: repos, views, clones, referrers, paths, stars.
- Upserts are idempotent
- Startup migration uses
PRAGMA user_version
- License:
LICENSE - Contributing:
CONTRIBUTING.md - Code of conduct:
CODE_OF_CONDUCT.md - Security policy:
SECURITY.md - Changelog:
CHANGELOG.md - CODEOWNERS:
.github/CODEOWNERS
Thanks for using and contributing to gghstats.
Hats off to ghstats by vladkens: a self-hosted GitHub traffic dashboard in Rust that also keeps historical traffic beyond GitHub’s short default window, with SQLite and a small deployment story. gghstats is a separate Go implementation and design, but that project deserves credit as important prior work in the same problem space.
MIT

