gghstats-selfhosted: production-shaped manifests for gghstats
You already read gghstats: Keep GitHub traffic past 14 days — gghstats is the small Go service that keeps GitHub traffic history in SQLite instead of losing it after GitHub’s ~14-day window. The app ships binaries, a multi-arch GHCR image, and a focused README.
What lives outside the application repo is everything that answers: how do I run this for real on my box, VPS, or cluster?
That’s gghstats-selfhosted — a separate repository with deployment manifests only: docker run, Docker Compose (minimal, Traefik + HTTPS, optional observability), and a Helm chart for Kubernetes. No Go code here; it stays easy to fork, pin, and diff.
Live app demo (read-only): gghstats.hermesrodriguez.com
What the app looks like
Flat screenshots with a white backdrop, perspective, and soft shadow (local asset pipeline — not in the GitHub repos):
Main dashboard (repository list):

Repository detail (charts and tables):

Why split “app” and “how to run it”?
- gghstats = releases, security advisories, feature issues, container tags.
-
gghstats-selfhosted = Compose files, Helm templates, env patterns, and docs that change when deployment stories evolve (Traefik labels, persistence, layout under
run/).
You can ignore the self-hosted repo forever and still run from GHCR with a one-liner — but if you want opinionated layouts (shared GGHSTATS_HOST_DATA, secrets outside the git clone, optional Prometheus/Grafana/Loki behind the same Traefik network), the split keeps each repo readable.
Who this is for
- Self-hosters who already run a VPS, Compose, or a small Kubernetes cluster and want a repeatable layout instead of a one-off paste from Stack Overflow.
- Operators who care about pinning an image tag, a single persistent path for SQLite, and secrets that never live in git — the same reasons larger teams split “app” and “platform,” just with a tiny footprint.
What this layout tries to spare you
- Wiring Traefik + Let’s Encrypt from scratch on every new service.
- Stuffing Compose samples, Helm, and env docs into the application repo (noisier release notes, harder security reviews).
- Forgetting which volume holds
gghstats.dbwhen you bump the image six months later.
Context (other approaches)
GitHub’s Traffic view only goes back about 14 days. Other open-source projects chase “GitHub stats” with different goals (dashboards, exporters, hosted analytics). gghstats stays narrow: persist traffic via the API into SQLite, ship one Go binary or GHCR image, and let a separate repo own how you run it. If that trade-off fits you, these manifests are the glue.
What you get in run/
| Path | Roughly |
|---|---|
run/standalone/{linux,macos,windows}/ |
Notes for the binary-only path |
run/docker/ |
Single-container docker run
|
run/docker-compose/minimal/ |
One service, quick VPS |
run/docker-compose/traefik/ |
HTTPS + Let’s Encrypt + edge network for the app |
run/docker-compose/observability/ |
Optional Prometheus / Grafana / Loki (after Traefik) |
run/kubernetes/helm/gghstats/ |
Helm chart gghstats (same name as the app; not the GitHub repo name) |
run/kubernetes/manifests/ |
Plain YAML if you prefer not to use Helm |
The table in the README is the fastest way to jump to the flow you want.
Quick starts (copy, adjust, run)
Pick one path. Replace ghp_xxx, host paths, your-github-user/*, image tags, and domains with yours. Pin ghcr.io/hrodrig/gghstats: to a tag that exists on GHCR / releases (example below uses v0.1.2).
GitHub token (scopes and safety)
GGHSTATS_GITHUB_TOKEN must be a Personal Access Token that can reach the repos matched by GGHSTATS_FILTER. Follow gghstats — Token setup:
| Token type | Scopes / access (summary) |
|---|---|
| Classic PAT |
public_repo — enough for public repos only. Use repo if you track private repositories (or use GGHSTATS_INCLUDE_PRIVATE=true). |
| Fine-grained PAT | Grant access to the repositories you need; include whatever repository permissions GitHub requires for the Traffic and repo metadata APIs for those repos (the token wizard lists them per permission). |
Safety: do not commit the token, put it in a public gist, or paste it into issues. Prefer env vars, Compose env_file, or Kubernetes Secrets. The app’s startup banner only shows a masked token. If the dashboard is empty after sync, verify filter rules and token scope (see Troubleshooting in the app README).
Binary (no Docker)
Grab a release binary from gghstats Releases, extract, then:
export GGHSTATS_GITHUB_TOKEN=ghp_xxx
./gghstats serve
Open http://localhost:8080.
Docker (one container)
export GGHSTATS_HOST_DATA=/home/gghstats/gghstats-data
mkdir -p "$GGHSTATS_HOST_DATA"
docker run -d \
-e GGHSTATS_GITHUB_TOKEN=ghp_xxx \
-e GGHSTATS_FILTER="your-github-user/*" \
-p 8080:8080 \
-v "${GGHSTATS_HOST_DATA}:/data" \
--name gghstats \
ghcr.io/hrodrig/gghstats:v0.1.2
Docker Compose (minimal, one service)
git clone https://github.com/hrodrig/gghstats-selfhosted.git
cd gghstats-selfhosted
export GGHSTATS_HOST_DATA=/home/gghstats/gghstats-data
mkdir -p "$GGHSTATS_HOST_DATA"
cp run/common/.env.example "${GGHSTATS_HOST_DATA}/.env"
# Edit "${GGHSTATS_HOST_DATA}/.env" — at least GGHSTATS_GITHUB_TOKEN, GGHSTATS_VERSION, GGHSTATS_HOST_DATA
docker compose --env-file "${GGHSTATS_HOST_DATA}/.env" \
-f run/docker-compose/minimal/docker-compose.yml up -d
Docker Compose + Traefik (HTTPS, production-shaped)
Needs DNS A/AAAA to this host and 80 / 443 reachable.
git clone https://github.com/hrodrig/gghstats-selfhosted.git
cd gghstats-selfhosted
export GGHSTATS_HOST_DATA=/home/gghstats/gghstats-data
mkdir -p "$GGHSTATS_HOST_DATA"
cp run/common/.env.example "${GGHSTATS_HOST_DATA}/.env"
# Edit "${GGHSTATS_HOST_DATA}/.env" — token, GGHSTATS_HOSTNAME, ACME_EMAIL, GGHSTATS_VERSION, GGHSTATS_HOST_DATA, …
docker compose --env-file "${GGHSTATS_HOST_DATA}/.env" \
-f run/docker-compose/traefik/docker-compose.yml up -d
Kubernetes (Helm)
helm repo add gghstats https://hrodrig.github.io/gghstats-selfhosted
helm repo update
helm show values gghstats/gghstats > my-values.yaml
# Edit my-values.yaml — e.g. image.tag, persistence, resources; keep githubToken.value empty (PAT goes in the Secret below)
kubectl create namespace gghstats
kubectl create secret generic gghstats-secret -n gghstats \
--from-literal=github-token=ghp_xxx
helm install gghstats gghstats/gghstats -n gghstats -f my-values.yaml
my-values.yaml: start from helm show values (above) so you inherit defaults and values.schema.json constraints (e.g. resources). Do not put the PAT in that file — use the Secret and leave githubToken.value empty. Details: README — Kubernetes Helm.
Example my-values.yaml fragment — token only in the Secret created above; the chart reads it via githubToken.existingSecret (or default secretName):
# Excerpt — always start from: helm show values gghstats/gghstats > my-values.yaml
image:
tag: "v0.1.2"
githubToken:
value: ""
existingSecret: "gghstats-secret"
resources:
requests: { cpu: "50m", memory: "128Mi" }
limits: { cpu: "1", memory: "512Mi" }
Helm chart security (defaults): the workload runs non-root (UID/GID 1000), with readOnlyRootFilesystem: true, capabilities.drop: [ALL], and a RuntimeDefault seccomp profile; SQLite lives under the /data mount and /tmp is a small emptyDir. Adjust only if your image requires it — see the chart README.
Reality check: 0.1.x is still early; pin tags, read CHANGELOG on upgrades, and expect manifests to evolve with releases.
Two repos, one story
How the two repos relate:
gghstats (app repo) gghstats-selfhosted (deploy repo)
| |
| builds | Compose, Helm, run/
v v
GHCR image ─────────────┬──────────── Manifests
│
v
your VPS / Kubernetes
Links
| What | Where |
|---|---|
| Deployment manifests | github.com/hrodrig/gghstats-selfhosted |
| Application | github.com/hrodrig/gghstats |
| Helm index | hrodrig.github.io/gghstats-selfhosted/index.yaml |
| Versioning, contributing, changelog | README — Versioning · CONTRIBUTING · CHANGELOG |
Closing
If you’re already self-hosting databases, proxies, and dashboards, gghstats is one more small service — and gghstats-selfhosted is the folder structure I wished existed when I wired mine up: copy run/common/.env.example, set GGHSTATS_HOST_DATA, choose Compose or Helm, and keep your PAT out of git.
Questions and PRs welcome on develop; merge and releases follow the repo docs.
Cross-posted from the author’s notes; exact commands, versioning, and release policy are always the repositories linked above.
Disclosure (Dev.to / transparency): The author used AI-assisted editing (e.g. drafting structure, wording, and Markdown) and reviewed and approved the final text. Technical claims are meant to match the linked repositories at publish time; if something drifts, trust the repos and CHANGELOG over this post.

Top comments (0)