Deployment manifests for gghstats — Compose, Helm, docker run, optional observability. App source and releases: github.com/hrodrig/gghstats.
Demo: gghstats.hermesrodriguez.com · Observability example: gghstats-obs.hermesrodriguez.com
Policies: Community and policies, community standards — changelog, contributing, security, code of conduct, agent guidelines.
- Pick a path
- Standalone binary
- Docker single container
- Docker Compose minimal
- Docker Compose Traefik HTTPS
- Observability optional
- Authelia SSO (advanced)
- Kubernetes Helm
- Recommended VPS baseline (optional)
- Opt-in alerts
- Persistent data and secrets
- Custom UI theme (optional)
- Repository layout
- Versioning
- Validate gghstats image upgrade (Compose / Traefik)
- Community and policies
- Community standards
- License
| You want… | Section |
|---|---|
| Binary only (no Docker) | Standalone binary — Linux: systemd, .deb/.rpm, config |
Linux .deb / .rpm + systemd |
run/standalone/linux/README.md |
Single container (docker run) |
Docker single container |
| Compose, one service (quick VPS) | Docker Compose minimal |
| HTTPS + domain (Traefik + Let’s Encrypt) | Docker Compose Traefik HTTPS |
| Prometheus / Grafana / Loki (after Traefik) | Observability optional |
| Authelia SSO + 2FA (advanced, after Traefik) | Authelia SSO |
| Kubernetes | Kubernetes Helm |
| Harden a fresh VPS (optional, agnostic Ansible; recommendations only) | Recommended VPS baseline |
| Simpler or custom dashboard look (CSS overlay) | Custom UI theme (optional) |
Shared env template for Compose: copy run/common/.env.example to ${GGHSTATS_HOST_DATA}/.env, set GGHSTATS_HOST_DATA inside that file, and pass --env-file "${GGHSTATS_HOST_DATA}/.env" to Compose. Deeper walkthroughs: run/README.md.
Goal: run the app without Docker — tarball, .deb, .rpm, or systemd on Linux.
All implementation detail (env file paths, systemctl, package install, filters on a server) is in run/standalone/ — start with Linux for packages and systemd.
Smoke test (UI only) — minimal path to try the dashboard; not production. Same commands as gghstats Quick start:
curl -fsSL https://raw.githubusercontent.com/hrodrig/gghstats/main/scripts/install.sh | sh
export GGHSTATS_GITHUB_TOKEN=ghp_xxx
gghstats run --openrun is an alias for serve; --open opens the browser when the dashboard is ready. Background sync starts automatically. Open http://localhost:8080 if you did not use --open. Other install options (Homebrew, .deb, tarball): gghstats Install.
macOS / Windows: run/standalone/macos · run/standalone/windows
Goal: one container, no Compose file.
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:v1.0.1Use an image tag that exists on GHCR (releases); match GGHSTATS_VERSION in run/common/.env.example.
Check: curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/ (expect 200 or 3xx).
Remove:
docker stop gghstats && docker rm gghstatsMore: run/docker/README.md
Goal: quick stack from this repo (single service, GHCR image).
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": GGHSTATS_GITHUB_TOKEN, GGHSTATS_VERSION, and GGHSTATS_HOST_DATA (same path as above)
docker compose --env-file "${GGHSTATS_HOST_DATA}/.env" -f run/docker-compose/minimal/docker-compose.yml up -dCheck: curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/ (or your GGHSTATS_PORT).
Remove:
docker compose --env-file "${GGHSTATS_HOST_DATA}/.env" -f run/docker-compose/minimal/docker-compose.yml downMore: run/docker-compose/minimal/README.md
Goal: production-style TLS on your domain (ports 80 / 443).
Prerequisites: DNS A/AAAA for GGHSTATS_HOSTNAME → this host; 80 and 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": GGHSTATS_GITHUB_TOKEN, GGHSTATS_HOSTNAME, ACME_EMAIL, GGS_UID, GGS_GID,
# GGHSTATS_VERSION, and GGHSTATS_HOST_DATA (same absolute path as above — SQLite lives next to this file)
docker compose --env-file "${GGHSTATS_HOST_DATA}/.env" -f run/docker-compose/traefik/docker-compose.yml up -dCheck: curl -sS -o /dev/null -w '%{http_code}\n' https://your-hostname/ after DNS and TLS succeed.
Remove:
docker compose --env-file "${GGHSTATS_HOST_DATA}/.env" -f run/docker-compose/traefik/docker-compose.yml downMore: run/docker-compose/traefik/README.md
Rate limiting (defence in depth): gghstats ≥ 0.7.5 applies built-in per-IP rate limiting (120 req/min, burst 20). The Traefik compose keeps its own rate-limit middleware at the edge — together they provide layered protection. If you tune the in-app limits, you can relax the Traefik layer. See gghstats README — Rate limiting.
Trusted proxies (≥ 0.10.2): behind Traefik, set
GGHSTATS_TRUSTED_PROXIESto the Docker network CIDR Traefik uses when dialing gghstats (often a private range such as172.16.0.0/12) so in-app rate limit / IP whitelist see the real client viaX-Forwarded-For. Empty = ignore forwarded headers (safe default). Passed through by this Compose stack from${GGHSTATS_HOST_DATA}/.env.IP whitelist (≥ 0.7.6): restrict access by IP/CIDR with
GGHSTATS_WHITELISTandGGHSTATS_WHITELIST_PATHS. Non-matching IPs receive 403. Scope to specific paths (e.g./api/) while keeping the dashboard public. See gghstats README — IP whitelist.Remote sync (≥ 0.7.10): when
GGHSTATS_API_TOKENis set, dashboard Sync all sendsx-api-tokenand bypasses the IP whitelist on protected paths — you can keep/api/whitelisted without opening it to the whole internet.
/metricsis public by default. The Traefik compose excludes it from the public router (!PathPrefix). Prometheus scrapes internally viahttp://gghstats:8080/metricson the Docker network. If you use minimal Compose ordocker run, protect/metricswith a firewall or setGGHSTATS_METRICS=false.
Goal: Prometheus, Grafana, Loki, etc. Requires the Traefik stack above so network gghstats_edge exists. Use the same GGHSTATS_HOST_DATA as your main ${GGHSTATS_HOST_DATA}/.env (SQLite and secrets in one host directory).
export GGHSTATS_HOST_DATA=/home/gghstats/gghstats-data
mkdir -p "$GGHSTATS_HOST_DATA"
cp run/docker-compose/observability/observability.env.example "${GGHSTATS_HOST_DATA}/.env.observability"
# Edit "${GGHSTATS_HOST_DATA}/.env.observability" — set GRAFANA_ADMIN_PASSWORD at minimum (must match GGHSTATS_HOST_DATA used for Traefik / main .env)Expose Grafana on HTTPS via Traefik (public hostname) — use this if you want Grafana on the internet with the same Traefik / Let’s Encrypt as gghstats (recommended once DNS is ready):
-
In
"${GGHSTATS_HOST_DATA}/.env.observability", set a dedicated FQDN and matching root URL (must match what users open in the browser):GRAFANA_HOSTNAME=gghstats-obs.example.com GRAFANA_ROOT_URL=https://gghstats-obs.example.com
-
DNS: point
GRAFANA_HOSTNAMEto this host (A/AAAA or CNAME), same idea asGGHSTATS_HOSTNAMEfor the main app. -
Start the stack with both Compose files (the second file adds Traefik labels only; it does not add another Traefik container). Use both
-flines on everyup/pull/downthat recreates Grafana, or HTTPS routing breaks until you fix it.
docker compose --env-file "${GGHSTATS_HOST_DATA}/.env.observability" -p gghstats-obs \
-f run/docker-compose/observability/docker-compose.observability.yml \
-f run/docker-compose/observability/docker-compose.observability.traefik.yml \
up -dLocal / LAN only (no Traefik route for Grafana) — Grafana on http://localhost:${GRAFANA_PORT:-3000}; omit docker-compose.observability.traefik.yml:
docker compose --env-file "${GGHSTATS_HOST_DATA}/.env.observability" -p gghstats-obs \
-f run/docker-compose/observability/docker-compose.observability.yml up -dCheck / troubleshoot / SSH tunnel: run/docker-compose/observability/README.md (curl checks, down -v).
Remove (containers + stack volumes): use the same -f list you used for up. Examples:
# If you started with Traefik overlay (two files), remove with two files:
docker compose --env-file "${GGHSTATS_HOST_DATA}/.env.observability" -p gghstats-obs \
-f run/docker-compose/observability/docker-compose.observability.yml \
-f run/docker-compose/observability/docker-compose.observability.traefik.yml \
down -v
# If you started with only the base file:
docker compose --env-file "${GGHSTATS_HOST_DATA}/.env.observability" -p gghstats-obs \
-f run/docker-compose/observability/docker-compose.observability.yml down -vRecommended: install from the Helm repository on GitHub Pages (index.yaml; chart packages are attached to GitHub Releases as gghstats-<version>.tgz).
GitHub Pages: The Pages URL serves index.yaml for Helm and includes a short HTML landing for humans. helm repo add only needs the HTTPS base URL — you do not have to open the site in a browser.
Naming (this repo vs the chart): This GitHub repository is gghstats-selfhosted (deployment manifests only). The Helm chart lives under run/kubernetes/helm/gghstats/ — the final directory name gghstats is the chart name (see name: in Chart.yaml), the same name as the application the chart deploys. It is not the repository name. Published chart packages and chart-releaser GitHub Releases use the pattern gghstats-<chart-version> (e.g. gghstats-0.1.7.tgz); Git tags for this repo use v<semver> (e.g. v0.1.8) per VERSION.
helm repo add gghstats https://hrodrig.github.io/gghstats-selfhosted
helm repo update
helm install gghstats gghstats/gghstats -n gghstats --create-namespace -f my-values.yamlGitHub token (recommended): do not put the PAT in my-values.yaml. Create a secret that matches the chart defaults (secretName: gghstats-secret, secretKey: github-token — see githubToken in values.yaml). Replace YOUR_GITHUB_TOKEN with your classic or fine-grained PAT (repo scope as needed):
kubectl create namespace gghstats
kubectl create secret generic gghstats-secret \
-n gghstats \
--from-literal=github-token=YOUR_GITHUB_TOKENThen run helm install (omit --create-namespace if the namespace already exists). Keep githubToken.value: empty in my-values.yaml so Helm does not embed the token in a release manifest.
Use helm show values gghstats/gghstats (after helm repo update) or the copy in the repo browser to build my-values.yaml (image tag, persistence, resources, etc.). Pick any namespace with -n (here gghstats); if you use another name, use the same namespace in kubectl and helm and adjust my-values.yaml if you reference the secret explicitly.
If helm repo add fails (network, Pages outage, or first minutes after a new release), try again later or install from this repository below.
From this repository (sources, templates, contributing): the chart under run/kubernetes/helm/gghstats/ is the same chart; clone it to inspect YAML, open issues, or install without the published repo:
git clone https://github.com/hrodrig/gghstats-selfhosted.git
cd gghstats-selfhosted
helm install gghstats ./run/kubernetes/helm/gghstats -n gghstats --create-namespace -f my-values.yamlSee values.yaml in-tree for defaults.
Check: kubectl get pods -n gghstats -l app.kubernetes.io/name=gghstats and your Ingress/Service URL.
Remove:
helm uninstall gghstats -n gghstatsMore: run/kubernetes/helm/gghstats/README.md · run/kubernetes/manifests
Goal: generic host hygiene on a Debian/Ubuntu VPS — not gghstats containers or Traefik labels. The playbook can optionally install Docker Engine (CE) for Compose; skip that tag for binary-only or Helm nodes.
If the attack vector is a misconfigured or compromised VPS, HTTP headers on the app will not help; operators secure SSH, firewall, and automatic security updates themselves. This repo ships an optional, agnostic Ansible playbook you run from your laptop against your server inventory.
Operator responsibility: content under run/vps-recommended/ is recommendations only — not a managed service. You must analyze, validate, and choose what to apply on your VPS; outcomes are your responsibility. See the full disclaimer in that README.
Note: step-by-step guides below often use Docker Compose because it is the usual path on a VPS; the baseline here is not Compose-specific — the same practices apply to docker run, a standalone binary, or Helm on Kubernetes (plus cluster-level controls where relevant).
| Topic | Where |
|---|---|
| Philosophy, tags, dry-run | run/vps-recommended/README.md |
| Playbook | run/vps-recommended/ansible/playbook.yml |
Typical order: (1) VPS baseline → (2) pick any install path (Compose is the common example; others are equivalent from a host-security view) → (3) store secrets and SQLite outside the clone (persistent data — Compose uses ${GGHSTATS_HOST_DATA}/.env; Helm uses Secrets/PVC; binary uses host paths you choose).
Goal: Single Sign-On with 2FA (TOTP/WebAuthn) in front of gghstats. Protects /api/ and /h2h — dashboard stays public.
Prerequisites: Traefik stack running on gghstats_edge.
gghstats v0.10.0+ sends traffic/ops alerts to Slack, generic webhooks (Discord, Teams, n8n), or Loki; v0.10.1+ adds SMTP email. Alerts remain off unless GGHSTATS_ALERTS_ENABLED=true.
Compose (minimal / Traefik): keep provider secrets in ${GGHSTATS_HOST_DATA}/.env; both stacks explicitly pass the standard GGHSTATS_* sink variables. Configure GGHSTATS_ALERT_SINKS and GGHSTATS_ALERT_RULES using the examples in run/common/.env.example, then recreate with up -d (not restart alone).
Minimal Slack-only config (replace the webhook URL; never commit real secrets):
GGHSTATS_ALERTS_ENABLED=true
GGHSTATS_SLACK_WEBHOOK_URL='https://hooks.slack.com/services/T…/B…/…'
GGHSTATS_ALERT_SINKS='[{"type":"slack","webhook_url_env":"GGHSTATS_SLACK_WEBHOOK_URL"}]'
# Optional rules — evaluated after a successful sync, not by alert test:
# GGHSTATS_ALERT_RULES='[{"scope":"all_repos","metric":"clones","window":"lifetime","op":"gte","value":21000,"fire":"once"}]'Smoke-test from the clone root (Traefik stack; use minimal if that is your compose file):
./run/scripts/compose-stack.sh traefik up -d --pull always
docker compose --env-file "${GGHSTATS_HOST_DATA}/.env" \
-f run/docker-compose/traefik/docker-compose.yml \
exec gghstats gghstats alert test --sink slackExpected console output:
alert test: sent kind "traffic" to 1 sink(s)
Exit 0 = delivered; 1 = config; 4 = delivery failure. The Slack channel should show a synthetic payload (example/repo, rule: delivery check) — not your real repos:
gghstats alert test only checks sink delivery. Real rules fire after the next successful sync (or use the dashboard Sync now button if GGHSTATS_SYNC_ON_STARTUP=false).
Other families: --sink smtp, --sink webhook, or --sink loki.
Helm: set alerting.enabled, alerting.sinks, and alerting.rules. Put provider credentials in an existing Secret whose keys are the exact environment-variable names referenced by the sink JSON, then set alerting.existingSecret to that Secret name. Example:
kubectl create secret generic gghstats-alerts -n gghstats \
--from-literal=GGHSTATS_SLACK_WEBHOOK_URL='https://hooks.slack.com/services/...'alerting:
enabled: true
existingSecret: gghstats-alerts
sinks: '[{"type":"slack","webhook_url_env":"GGHSTATS_SLACK_WEBHOOK_URL"}]'
rules: '[{"scope":"all_repos","metric":"clones","window":"lifetime","op":"gte","value":21000,"fire":"once"}]'Full rule/sink contract: gghstats SPEC §8.
Recommended on servers: colocate SQLite and env files outside the clone (see below).
Keep SQLite, ${GGHSTATS_HOST_DATA}/.env, and ${GGHSTATS_HOST_DATA}/.env.observability in one host directory (e.g. /home/gghstats/gghstats-data/). Set GGHSTATS_HOST_DATA inside the main .env to that absolute path. Run Compose from the clone root with --env-file "${GGHSTATS_HOST_DATA}/.env" for the app stacks and --env-file "${GGHSTATS_HOST_DATA}/.env.observability" for observability (-p gghstats-obs). See run/common/.env.example and run/docker-compose/observability/observability.env.example. Optional helper: run/scripts/compose-stack.sh — e.g. ./run/scripts/compose-stack.sh full restart for Traefik + gghstats + observability without remembering compose order (--help for prod vs full).
Requires a gghstats image 0.2.0 or newer (this repo’s Compose defaults use v1.0.1). The app serves an extra stylesheet at GET /theme/custom.css when GGHSTATS_CUSTOM_CSS points at a regular .css file readable inside the container.
The path in GGHSTATS_CUSTOM_CSS / env.customCss is always inside the container. Only the tree mounted at /data is the stable, writable place operators should use for SQLite and a custom CSS file in our manifests.
Docker Compose (minimal or Traefik)
Each stack bind-mounts the host directory ${GGHSTATS_HOST_DATA} (or the repo’s data/ fallback when unset) to /data in the container. On the host, put custom-theme.css next to your database directory contents — e.g. ${GGHSTATS_HOST_DATA}/custom-theme.css — so it appears as /data/custom-theme.css in the container. Set GGHSTATS_CUSTOM_CSS=/data/custom-theme.css in ${GGHSTATS_HOST_DATA}/.env. You do not need an extra volume line for the theme if it lives under that same host path.
docker run (single container)
Mount a host directory the same way: -v /path/on/host:/data, store the .css file under /path/on/host/, then set GGHSTATS_CUSTOM_CSS=/data/your-theme.css (see run/docker/README.md).
Helm / Kubernetes
When persistence.enabled is true, the chart mounts a PVC at /data (see templates/deployment.yaml and values.yaml → persistence). env.customCss must be a path on that PVC, e.g. /data/custom-theme.css, not somewhere under the read-only container root (the chart uses readOnlyRootFilesystem: true). The SQLite file from env.dbPath is also under /data.
If persistence.enabled is false, /data is still a writable emptyDir, but anything you put there (including a theme file) is lost when the Pod is deleted unless you repopulate it.
After install, common options are kubectl cp a file into the running pod’s /data/ (fix ownership/read bits for UID 1000 if your storage driver requires it), or your own Job / initContainer / GitOps pattern to populate /data. The chart does not bundle theme files; copy from upstream contrib/themes or supply your own.
Compose / Traefik or minimal (steps)
- Pin the image: set
GGHSTATS_VERSION=v1.0.1in${GGHSTATS_HOST_DATA}/.env(seerun/common/.env.example). - Copy a starter from
gghstatscontrib/themes/(or write your own) into the host directory that is bind-mounted to/data(same as Persistent data and secrets), e.g.${GGHSTATS_HOST_DATA}/custom-theme.css. - Set
GGHSTATS_CUSTOM_CSS=/data/custom-theme.cssin that.env. - Recreate the app container so env and mounts apply:
docker compose … up -d(notrestartalone if you also changedGGHSTATS_VERSION— see Versioning).
Helm (values)
Set env.customCss to a path under /data (e.g. /data/custom-theme.css) and ensure that file exists on the PVC as described above. Leave env.customCss empty to keep the stock neo-brutalist UI.
Upstream reference: gghstats README — Custom UI theme (optional).
run/
├── vps-recommended/ # Optional agnostic VPS Ansible (not gghstats install)
├── common/.env.example # Shared vars for Compose + image tag
├── scripts/ # compose-stack.sh (docker compose helper)
├── standalone/{linux,macos,windows}/
├── docker/ # docker run
├── docker-compose/
│ ├── minimal/
│ ├── traefik/
│ └── observability/
└── kubernetes/
├── helm/gghstats/ # Helm chart named "gghstats" (app); not the repo name
└── manifests/
testing/
├── platforms/ # Ansible: minimal Compose on real VPS (make test-compose-platforms)
├── kind/ # kind + Helm smoke test docs
└── scripts/ # test-helm-kind.sh
Makefile # release-check, test-compose-platforms, test-helm-kind
Maintainers: make release-check before tagging; optional testing/platforms and make test-helm-kind — see CONTRIBUTING.md.
VERSION— semver of this repository (Compose, docs,run/, etc.). When you change it, align the Version badge in this README and (if you keep a release entry) CHANGELOG.md; onmain, tag withv<semver>(e.g.v0.1.12). This number is not tied to the Helm chart on every bump.- Helm chart (
run/kubernetes/helm/gghstats/Chart.yaml→version:) — semver of the chart package published to GitHub Pages / Releases. Bumpversion:when the chart itself changes (templates,values, etc.). It may lag behindVERSION(e.g. repo0.2.0, chart0.1.5until you edit the chart). chart-releaser may skip publishing ifrun/kubernetes/helm/did not change — expected for docs-only repo releases. Chart.yaml→appVersion— gghstats application / image line; align with gghstats releases when you bump the deployed image story.GGHSTATS_VERSIONin${GGHSTATS_HOST_DATA}/.env(or the env file you pass to Compose) — container image tag on GHCR (gghstats releases), not the same asVERSION.- Optional (gghstats ≥ 0.6.0):
GGHSTATS_DEFAULT_LOCALEandGGHSTATS_ENABLED_LOCALESfor dashboard UI languages (API/CLI stay English). See gghstats — Web UI languages (i18n) andrun/common/.env.example.
Upgrading the app image: (1) Set GGHSTATS_VERSION in ${GGHSTATS_HOST_DATA}/.env. (2) Pull the image from GHCR (so the new tag exists locally). (3) Run up -d so Compose recreates the service with that tag — e.g. ./run/scripts/compose-stack.sh traefik pull then … traefik up -d, or … traefik up -d --pull always. Optionally traefik down before pull/up if you want everything stopped first. docker compose restart / compose-stack.sh … restart only restart the existing container — they do not apply a new image tag. See run/common/.env.example.
Use this checklist from the repository clone root after editing GGHSTATS_VERSION (Traefik + gghstats stack; same idea for minimal with minimal instead of traefik in the commands below).
GGHSTATS_HOST_DATApoints at the directory that contains your.env(same as for normal Compose).- Confirm the tag in the env file:
grep GGHSTATS_VERSION "${GGHSTATS_HOST_DATA}/.env" - Confirm Compose resolves the expected image line (no need to start containers):
You should see
docker compose --env-file "${GGHSTATS_HOST_DATA}/.env" \ -f run/docker-compose/traefik/docker-compose.yml config \ | grep -E 'image:|gghstats'
ghcr.io/hrodrig/gghstats:<your-tag>(e.g.v1.0.1). - Pull and recreate the service (do not rely on
restartalone):Or one step:./run/scripts/compose-stack.sh traefik pull ./run/scripts/compose-stack.sh traefik up -d
./run/scripts/compose-stack.sh traefik up -d --pull always
- Confirm the running container uses that image:
docker ps --format 'table {{.Names}}\t{{.Image}}' | grep gghstats
- Smoke check — UI: open
GGHSTATS_HOSTNAMEin the browser (HTTPS if Traefik + ACME). The gghstats web UI shows the app version in the sidebar (must match the release you deployed).
Optional — observability stack: if you run Prometheus / Grafana / Loki on the same host, after the Traefik stack is healthy run ./run/scripts/compose-stack.sh --traefik observability ps and open your Grafana URL (e.g. GF_SERVER_ROOT_URL / GRAFANA_HOSTNAME) to confirm dashboards load. Observability images are separate from gghstats; they only need a full cycle if you change those compose images.
| Document | Purpose |
|---|---|
| CHANGELOG.md | Release history and notable changes to this repository (manifests, docs, layout). |
| CONTRIBUTING.md | How to open issues/PRs, branch policy (develop → main), and checks before submitting. |
| CODE_OF_CONDUCT.md | Community standards (Contributor Covenant). |
| SECURITY.md | How to report security vulnerabilities responsibly. |
| AGENTS.md | Guidelines for AI coding agents (Cursor, etc.) working in this repo. |
Application issues (bugs, features in the Go app or UI) belong in gghstats — not here.
- License:
LICENSE - Contributing:
CONTRIBUTING.md - Code of conduct:
CODE_OF_CONDUCT.md - Security policy:
SECURITY.md - Changelog:
CHANGELOG.md - Agent guidelines:
AGENTS.md
Thanks for self-hosting gghstats with these manifests. We would love to hear how easy or difficult it was to run gghstats self-hosted (Compose, Helm, docker run, observability, or anything in run/). Share feedback in GitHub Issues or, if enabled for this repository, Discussions.
MIT — see LICENSE.

