Skip to content

Self-hosting

Terminal window
docker run -p 9000:9000 --env-file cards.env ghcr.io/stats-forge/github-stats-forge-server

A card is then at http://localhost:9000/api/stats?username=you. cards.env holds one line, PAT_1= and a GitHub token — see Where the token lives.

Prefer it to the GitHub Action when the numbers should be live, or the token should stay on a machine you own.

GET and HEAD, one path per card; a trailing slash resolves to the same route.

PathDraws
/api/statscommits, PRs, issues, a rank
/api/top-langsmost-used languages
/api/pinone repository
/api/organ organization
/api/org-activityits PRs, issues and commits
/api/contributed-torepositories contributed to
/api/gistone gist
/api/wakatimeWakaTime coding time
/healthz{"status":"ok"}

Every option on Common options and each card’s page is a query param, spelled the same.

The image also serves this site at /, built from the same commit, and /anvil/ draws from the instance itself, with your tokens.

GitHub’s image proxy shows only a 200, so a failure answers 200 with the error drawn as a card, and says what happened in headers:

HeaderSays
Card-Statussuccess or error
Card-Error-Codewhy it failed
Card-Error-Paramthe parameter at fault, when the failure names one
Card-Cachehit, miss or stale

STRICT_HTTP_STATUS=true answers with the real status code instead, for anything that is not a README.

Without a list, an instance draws a card for anyone, on your rate limit:

Terminal window
ALLOWLIST=you,your-org
GIST_ALLOWLIST=0123456789abcdef0123456789abcdef

ALLOWLIST names the GitHub logins it serves, case-insensitively, on every card that takes one. A gist is named by its id, so only GIST_ALLOWLIST guards the gist card. Neither covers the WakaTime card, and neither is authentication: they limit whose cards are drawn, not who asks.

Everything but PAT_1 is optional.

VariableDefaultWhat it does
PAT_1, PAT_2, …—GitHub tokens, used in turn; a second doubles the rate limit
PORT / HOST9000 / 0.0.0.0where it listens
ALLOWLIST—GitHub logins this instance will draw
GIST_ALLOWLIST—gist ids it will draw
EXCLUDE_REPO—repositories to leave out of the language and star counts
FETCH_MULTI_PAGE_STARS1pages of starred repositories to count, or true for all
STRICT_HTTP_STATUSfalseanswer with the status code a failure deserves
CACHE_SECONDS—default lifetime of a drawn card; 0 turns caching off
STALE_SECONDS86400how long a held card covers a failing redraw; 0 for none
CORS_ORIGIN—the one origin allowed to read a card from script

See GitHub tokens for which token each card needs.

Never -e PAT_1=…: the token would land in your shell history, the process list and docker inspect. The server reads it from the environment, once at startup.

Terminal window
umask 077
printf 'PAT_1=%s\n' "$token" > cards.env
docker run -p 9000:9000 --env-file cards.env ghcr.io/stats-forge/github-stats-forge-server

--env-file takes each value literally, quotes included. Keep the file out of version control.

Terminal window
export PAT_1="$(pass github/cards)"
docker run -p 9000:9000 -e PAT_1 ghcr.io/stats-forge/github-stats-forge-server
services:
cards:
image: ghcr.io/stats-forge/github-stats-forge-server:latest
ports:
- '9000:9000'
environment:
PAT_1: ${PAT_1:?set PAT_1 in a .env file beside this one}

The token goes in a .env beside it, and :? refuses to start without one. The repository’s docker-compose.yml is this file.

Terminal window
# cards.env holds: PAT_1=op://Private/github-cards/credential
op run --env-file cards.env -- \
docker run -p 9000:9000 -e PAT_1 ghcr.io/stats-forge/github-stats-forge-server

A manager that only prints the value goes through a shell assignment, which keeps it out of the arguments:

Terminal window
PAT_1="$(aws secretsmanager get-secret-value \
--secret-id github-cards --query SecretString --output text)" \
docker run -p 9000:9000 -e PAT_1 ghcr.io/stats-forge/github-stats-forge-server
envFrom:
- secretRef:
name: github-cards

Sync that Secret from your cloud’s secret manager with the External Secrets Operator or the Secrets Store CSI Driver.

No tag is latest. Each release also publishes its exact version and a major.minor that follows its patches:

Terminal window
docker run -p 9000:9000 --env-file cards.env ghcr.io/stats-forge/github-stats-forge-server:0.4

What changed is on the releases page.

A card is held for ten hours, a failure that might fix itself for ten minutes, and one that will not for an hour. If a redraw fails, the last good card is served for up to a day longer, or for STALE_SECONDS. ?cache_seconds= asks for a life between four and twenty-four hours; CACHE_SECONDS=0 turns caching off.

![your stats](https://cards.example.com/api/stats?username=you&theme=tokyonight)

Everything on In a README applies unchanged.