diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000..f874b93cff83c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,35 @@ +# Dependencies and build outputs (rebuilt inside the image) +**/node_modules +**/build +**/dist +**/coverage +**/.turbo + +# Vercel-specific scaffolding, not used in the container image +**/.vercel +apps/backend/_dot_vercel_copy +apps/backend/.vercelignore +apps/backend/vercel.json +vercel-preparation.sh + +# VCS and tooling +.git +.github +.husky +.vscode +.devcontainer + +# Local env and logs +**/.env +**/.env.* +**/*.log +npm-debug.log* + +# Docs and misc (not needed at runtime) +**/*.md +docs + +# The image files themselves +Dockerfile +.dockerignore +docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000..c7bf0a5e2b030 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,79 @@ +# syntax=docker/dockerfile:1 + +# ------------------------------------------------------------------------------ +# github-stats-extended -- container image for self-hosting the backend. +# +# This image runs the backend as an HTTP server (apps/backend/express.js), +# so the cards can be served anywhere, not only on Vercel. The design keeps +# room for a future static-SVG generation mode (see the issue for context): +# additional entrypoints can be added as separate CMD targets without changing +# the build stages below. +# ------------------------------------------------------------------------------ + +ARG NODE_VERSION=24 +ARG PNPM_VERSION=10.34.1 + +# ------------------------------------------------------------------------------ +# Stage 1: base -- pin Node and enable pnpm via corepack. +# ------------------------------------------------------------------------------ +FROM node:${NODE_VERSION}-slim AS base +ARG PNPM_VERSION +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable && corepack prepare pnpm@${PNPM_VERSION} --activate +WORKDIR /app + +# ------------------------------------------------------------------------------ +# Stage 2: build -- install the whole workspace and build the core package. +# The backend itself runs from source (.js), so only packages/core needs a +# compile step (tsc -> build/). A pnpm store cache mount speeds up rebuilds. +# ------------------------------------------------------------------------------ +FROM base AS build + +# Copy the manifests first so the dependency layer can be cached independently +# of source changes. +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ +COPY turbo.json tsconfig.base.json tsconfig.json ./ +COPY packages/core/package.json packages/core/ +COPY apps/backend/package.json apps/backend/ +COPY apps/frontend/package.json apps/frontend/ + +# Full install (dev deps included) so turbo/tsc are available for the build. +# --frozen-lockfile keeps the image reproducible against the committed lockfile. +RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ + pnpm install --frozen-lockfile + +# Bring in the sources and build the core package. +COPY . . +RUN pnpm run build:packages + +# Produce a self-contained, production-only bundle for the backend. `pnpm deploy` +# resolves the `workspace:^` link to packages/core and copies everything the +# backend needs into /app/deploy, with no dev dependencies. +RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ + pnpm --filter=@stats-organization/github-readme-stats-backend deploy \ + --prod --legacy /app/deploy + +# ------------------------------------------------------------------------------ +# Stage 3: runtime -- minimal image running the backend as a non-root user. +# ------------------------------------------------------------------------------ +FROM node:${NODE_VERSION}-slim AS runtime +ENV NODE_ENV=production +# The server reads PORT (falling back to 9000). We default it to 9000 and expose +# that port; override at run time with -e PORT=... if needed. +ENV PORT=9000 +WORKDIR /app + +# node:*-slim already ships an unprivileged `node` user. Use it. +COPY --from=build --chown=node:node /app/deploy ./ +USER node + +EXPOSE 9000 + +# Lightweight liveness probe against the built-in status endpoint. +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||9000)+'/api/status/up').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" + +# HTTP-server mode. A future static-generation mode can be added as an +# alternative command (e.g. CMD ["node", "generate-svg.js"]). +CMD ["node", "express.js"] diff --git a/README.md b/README.md index 3549fe8e97daa..e6ceb6f3f9218 100644 --- a/README.md +++ b/README.md @@ -67,9 +67,9 @@ GitHub-Stats-Extended aims to be fully compatible with github-readme-stats. For The [GitHub-Stats-Extended Wizard](https://github-stats-extended.vercel.app/frontend) offers some essential customization options. For more advanced customization check out the [advanced documentation](docs/advanced_documentation.md). # Run It Yourself -If you want to run GitHub-Stats-Extended on your own, there are two main deployment options: you can use [github-readme-stats-action](https://github.com/stats-organization/github-readme-stats-action) to generate cards in your own GitHub Actions workflow. Or you can self-host GitHub-Stats-Extended on Vercel. +If you want to run GitHub-Stats-Extended on your own, there are a few deployment options: you can use [github-readme-stats-action](https://github.com/stats-organization/github-readme-stats-action) to generate cards in your own GitHub Actions workflow, self-host GitHub-Stats-Extended on Vercel, or run it as a container with [Docker](docs/docker.md). -See [Run It Yourself](docs/deploy.md) for detailed instructions. +See [Run It Yourself](docs/deploy.md) for detailed instructions, or [Running with Docker](docs/docker.md) for the container setup. # Acknowledgements This project is based on [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). On top of that project's functionality GitHub-Stats-Extended adds several new features and improvements. See [Fork Information](docs/fork.md) for a list of changes. The frontend added to GitHub-Stats-Extended is based on [GitHub Trends](https://github.com/avgupta456/github-trends). Big thanks to [@anuraghazra](https://github.com/anuraghazra), [@avgupta456](https://github.com/avgupta456), [@rickstaa](https://github.com/rickstaa), [@qwerty541](https://github.com/qwerty541) and everyone else who worked on these projects! ❤️ diff --git a/apps/backend/package.json b/apps/backend/package.json index a1530b4be811a..a53c895e4fa8b 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -8,6 +8,7 @@ "private": true, "main": "index.js", "scripts": { + "start": "node express.js", "test": "vitest", "test:update:snapshot": "vitest -u", "test:e2e": "vitest --config vitest.config.e2e.ts", @@ -17,13 +18,13 @@ }, "devDependencies": { "axios-mock-adapter": "2.1.0", - "express": "5.2.1", "jsdom": "catalog:default", "vitest": "catalog:default" }, "dependencies": { "@stats-organization/github-readme-stats-core": "workspace:^", "axios": "catalog:default", + "express": "5.2.1", "pg": "^8.22.0" } } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000..e0be15ba4283a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,66 @@ +# github-stats-extended -- self-hosting via Docker Compose. +# +# Minimal usage (no database, cards only): +# PAT_1=ghp_your_token docker compose up backend +# +# With the optional Postgres backend (enables request analytics, the +# authenticate/user-access endpoints, and repeat-recent): +# PAT_1=ghp_your_token docker compose --profile with-db up +# +# All configuration is passed through environment variables. At least one +# PAT_* variable (a GitHub personal access token) is required to render cards. + +services: + backend: + build: + context: . + dockerfile: Dockerfile + image: github-stats-extended:local + ports: + - "${PORT:-9000}:9000" + environment: + # --- Required: at least one GitHub token --- + PAT_1: ${PAT_1:?set PAT_1 to a GitHub personal access token} + # Add more tokens to spread the rate limit (PAT_2, PAT_3, ...): + # PAT_2: ${PAT_2:-} + + # --- Optional core configuration (see packages/core config) --- + WHITELIST: ${WHITELIST:-} + GIST_WHITELIST: ${GIST_WHITELIST:-} + EXCLUDE_REPO: ${EXCLUDE_REPO:-} + FETCH_MULTI_PAGE_STARS: ${FETCH_MULTI_PAGE_STARS:-} + + # --- Optional Postgres connection. Leave unset to run without a DB. --- + # When using the `with-db` profile below, this points at the db service. + POSTGRES_URL: ${POSTGRES_URL:-} + restart: unless-stopped + + # Variant that wires the backend to the bundled Postgres service. + backend-with-db: + extends: + service: backend + profiles: ["with-db"] + environment: + POSTGRES_URL: postgres://postgres:postgres@db:5432/stats + depends_on: + db: + condition: service_healthy + + db: + image: postgres:17-alpine + profiles: ["with-db"] + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: stats + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d stats"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + +volumes: + pgdata: diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 0000000000000..e77d46a8f33f0 --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,83 @@ +# Running with Docker + +The backend can run as a standalone HTTP server inside a container, so you can +host the cards anywhere instead of relying on Vercel. This is useful for +self-hosting, air-gapped environments, or local testing. + +## What the image does + +The image builds the `core` package and runs `apps/backend/express.js`, which +starts an HTTP server that answers the same routes as the Vercel deployment +(`/api`, `/api/pin`, `/api/top-langs`, `/api/gist`, `/api/wakatime`, and the +status/auth endpoints). The server listens on `0.0.0.0` and reads the `PORT` +variable, falling back to `9000`. + +A database is **optional**. Without `POSTGRES_URL`, all database-backed features +(request analytics, `repeat-recent`, the authenticate / user-access flow) are +skipped and card rendering works normally. + +## Requirements + +- Docker (with BuildKit, the default in current Docker versions). +- At least one GitHub personal access token, exposed as `PAT_1`. Additional + tokens (`PAT_2`, `PAT_3`, ...) spread the requests across the GitHub API rate + limit. + +## Quick start (Docker) + +Build the image: + +```bash +docker build -t github-stats-extended:local . +``` + +Run it: + +```bash +docker run --rm -p 9000:9000 -e PAT_1=ghp_your_token github-stats-extended:local +``` + +Then request a card: + +```bash +curl "http://localhost:9000/api?username=anuraghazra" +``` + +## Quick start (Docker Compose) + +Cards only, no database: + +```bash +PAT_1=ghp_your_token docker compose up backend +``` + +With the bundled Postgres (enables analytics and the auth endpoints): + +```bash +PAT_1=ghp_your_token docker compose --profile with-db up +``` + +## Configuration + +All configuration is passed through environment variables. + +| Variable | Required | Description | +| ------------------------ | -------- | ------------------------------------------------------------------ | +| `PAT_1`, `PAT_2`, ... | Yes | GitHub personal access tokens. At least one is required. | +| `PORT` | No | HTTP port. Defaults to `9000`. | +| `POSTGRES_URL` | No | Postgres connection string. Unset means the app runs without a DB. | +| `WHITELIST` | No | Comma-separated list of allowed usernames. | +| `GIST_WHITELIST` | No | Comma-separated list of allowed gist ids. | +| `EXCLUDE_REPO` | No | Comma-separated list of repositories to exclude. | +| `FETCH_MULTI_PAGE_STARS` | No | Enables multi-page star counting. | + +## Health check + +The image ships a `HEALTHCHECK` that polls `/api/status/up`. You can inspect it +with `docker ps` (the `STATUS` column shows `healthy` once the server is up). + +## Future: static SVG generation + +The image is structured so a static-generation mode can be added later as an +alternative command, without changing the build stages. Such a mode would render +cards to `.svg` files that can be served by any static file host. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e0b678f26820..feeda8e6dc45a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,6 +92,9 @@ importers: axios: specifier: catalog:default version: 1.18.1 + express: + specifier: 5.2.1 + version: 5.2.1 pg: specifier: ^8.22.0 version: 8.22.0 @@ -99,9 +102,6 @@ importers: axios-mock-adapter: specifier: 2.1.0 version: 2.1.0(axios@1.18.1) - express: - specifier: 5.2.1 - version: 5.2.1 jsdom: specifier: catalog:default version: 29.1.1