A self-hosted Docker setup for github-readme-stats, exposed publicly via a Cloudflare Tunnel.
On startup, the container clones the upstream repo and starts the stats server using its built-in express.js standalone server. A cron job restarts the container daily at 4 AM to pull the latest changes.
Important
This setup was designed and tested on Ubuntu 24.04. Scripts and instructions may not work as-is on other Linux distributions or operating systems.
- Docker with the Compose plugin (
docker compose versionshould work) - A GitHub Personal Access Token (see step 1a below)
- A Cloudflare Tunnel token (see step 2)
If Docker is not installed, run the official install script then add your user to the docker group:
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
newgrp dockerNote
Using a fine-grained token limits the scope to issues in your repositories and includes only public commits.
Original instructions from: github-readme-stats fine-grained token docs
- Go to Account → Settings → Developer Settings → Personal access tokens → Fine-grained tokens
- Click Generate new token → Generate new token
- Select an expiration date
- Select All repositories
- Under Repository permissions, enable read-only for:
- Commit statuses
- Contents
- Issues
- Metadata
- Pull requests
- Click Generate token and copy it
Alternatively, you can use a classic token with read:user and repo scopes.
Caution
The classic repo scope grants full read/write access to all your repositories. Use a fine-grained token instead if possible.
cp .env.example .envEdit .env and fill in your values:
| Variable | Required | Description |
|---|---|---|
PAT_1 |
Yes | GitHub Personal Access Token |
WHITELIST |
Yes | Your GitHub username (restricts API access) |
CLOUDFLARE_TUNNEL_TOKEN |
Yes | Token from the Cloudflare Zero Trust dashboard |
GITHUB_README_STATS_REF |
No | Branch, tag, or commit hash to pin. Defaults to master |
CACHE_SECONDS |
No | Response cache duration in seconds. Defaults to 14400 (4 hours) |
- Go to Cloudflare Zero Trust → Networks → Connectors
- Click Create a tunnel → choose Cloudflared → give it a name (e.g.
readme-stats) - Cloudflare will show a command like
cloudflared service install <token>— copy only the token at the end of the command and paste it into your.envasCLOUDFLARE_TUNNEL_TOKEN - Under Public Hostname, add a route:
- Subdomain / Domain: your public hostname (e.g.
readmestats.yourdomain.com) - Service:
http://readme-stats:9000
- Subdomain / Domain: your public hostname (e.g.
The readme-stats service name must match exactly — it's how the tunnel reaches the container over the internal Docker network.
Note
If you'd rather not use Cloudflare Tunnel, you can expose the service through any reverse proxy (nginx, Caddy, Traefik, etc.) or direct port forwarding by adding a ports entry to the readme-stats service in docker-compose.yml.
docker compose up -dVerify everything is running:
docker compose psIf a service isn't running, check the logs:
docker compose logsThe container re-clones the upstream repo on every start, so a daily restart picks up any changes automatically.
bash setup-cron.shThis adds a cron job that restarts the container at 4 AM UTC daily and logs output to ~/readme-stats-restart.log.
-
Runtime cloning: The upstream repo is cloned each time the container starts rather than at image build time. Every restart picks up the latest code from the configured ref. To pin to a specific version, set
GITHUB_README_STATS_REFto a commit hash in.env. -
Daily restart: The 4 AM UTC cron job exists solely to trigger a fresh clone and pick up upstream changes. If you don't want automatic updates, skip step 4.
-
Cloudflare Tunnel: The
readme-statsservice has no published ports — all traffic is routed through the tunnel. Port configuration belongs in the Cloudflare dashboard, not indocker-compose.yml. -
Alpine base image: The Dockerfile is pinned to
alpine:3.21. If you upgrade, verify that thenodejs,npm, andgitpackages are still available under the same names.
Note
I have ADHD and will likely never update this repo again. Feel free to make issues or submit PRs if it would be useful, but know that main may never be updated again.