From 9bb0e8abe0a4aaf1270f9fd387f09efa88c8c279 Mon Sep 17 00:00:00 2001 From: Prerak Yadav Date: Sun, 28 Jun 2026 10:31:53 +0530 Subject: [PATCH] fix(docker): resolve self-hosting build and OAuth troubleshooting issues Align Docker with CI using Node 22 and pinned pnpm 11.9, copy pnpm-workspace.yaml for non-interactive build scripts, add native build tools, and improve auth logging for self-hosted deployments. --- Dockerfile | 19 ++++++------- README.md | 2 ++ docs/self-hosting.md | 22 +++++++++++++++ package.json | 19 ++----------- src/lib/auth-config.ts | 61 ++++++++++++++++++++++++++++++++++++++++++ src/lib/auth.ts | 14 ++++++++-- 6 files changed, 109 insertions(+), 28 deletions(-) create mode 100644 src/lib/auth-config.ts diff --git a/Dockerfile b/Dockerfile index 800716ddc..354f8ec99 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,16 @@ -FROM node:20-alpine AS base +FROM node:22-alpine AS base +RUN corepack enable && corepack prepare pnpm@11.9.0 --activate # Install dependencies only when needed FROM base AS deps -RUN apk add --no-cache libc6-compat +RUN apk add --no-cache libc6-compat python3 make g++ WORKDIR /app # Install dependencies based on the preferred package manager -COPY package.json package-lock.json* pnpm-lock.yaml* ./ +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ RUN \ - if [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ + if [ -f pnpm-lock.yaml ]; then pnpm i --frozen-lockfile; \ elif [ -f package-lock.json ]; then npm ci; \ else echo "Lockfile not found." && exit 1; \ fi @@ -18,12 +19,12 @@ RUN \ FROM base AS development WORKDIR /app -RUN apk add --no-cache libc6-compat +RUN apk add --no-cache libc6-compat python3 make g++ -COPY package.json package-lock.json* pnpm-lock.yaml* ./ +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ RUN \ - if [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm install; \ + if [ -f pnpm-lock.yaml ]; then pnpm install; \ elif [ -f package-lock.json ]; then npm install; \ else echo "Lockfile not found." && exit 1; \ fi @@ -48,7 +49,7 @@ COPY . . ENV NEXT_TELEMETRY_DISABLED=1 RUN \ - if [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ + if [ -f pnpm-lock.yaml ]; then pnpm run build; \ elif [ -f package-lock.json ]; then npm run build; \ else echo "Lockfile not found." && exit 1; \ fi @@ -76,4 +77,4 @@ EXPOSE 3000 ENV PORT=3000 ENV HOSTNAME=0.0.0.0 -CMD ["node", "server.js"] \ No newline at end of file +CMD ["node", "server.js"] diff --git a/README.md b/README.md index 256484eb4..ff6a33cd0 100644 --- a/README.md +++ b/README.md @@ -378,6 +378,8 @@ View container logs: docker compose logs -f ``` +Common self-hosting build issues (pnpm/Corepack mismatch, `ERR_PNPM_IGNORED_BUILDS`, missing native build tools, GitHub OAuth `error=github`) are documented in [docs/self-hosting.md](docs/self-hosting.md#docker-build-troubleshooting). Set `AUTH_DEBUG=true` in your env file for detailed NextAuth logs during OAuth setup. + --- ## Roadmap diff --git a/docs/self-hosting.md b/docs/self-hosting.md index 1b38cf93b..130b65f4a 100644 --- a/docs/self-hosting.md +++ b/docs/self-hosting.md @@ -53,6 +53,7 @@ DevTrack uses `@supabase/supabase-js` which relies on the Supabase REST API, so | `WAKATIME_CLIENT_ID` | Optional | WakaTime OAuth Client ID (for WakaTime integration) | `waka_...` | | `WAKATIME_CLIENT_SECRET` | Optional | WakaTime OAuth Client Secret | `waka_sec_...` | | `ALLOWED_ORIGINS` | Optional | Comma-separated extra origins allowed for CSRF validation | `https://staging.example.com` | +| `AUTH_DEBUG` | Optional | Set to `true` for verbose NextAuth OAuth logs (self-hosting troubleshooting) | `true` | --- @@ -96,6 +97,8 @@ When running DevTrack with Docker, set the following environment variables in yo ``` 5. DevTrack will be available at `http://localhost:3000`. +> **Note:** The bundled `docker-compose.yml` targets the `development` stage and runs `npm run dev` with hot reload. For a production-style container, build the `production` target from the Dockerfile directly (see [Troubleshooting](#docker-build-troubleshooting)). + --- ## 2. Railway @@ -129,6 +132,25 @@ DevTrack includes a `render.yaml` Blueprint for easy deployment on Render's free ## 🔧 Troubleshooting +### Docker build troubleshooting + +- **`pnpm` / Corepack version mismatch during `docker compose build`**: + The Dockerfile uses Node 22 with pnpm pinned via the `packageManager` field in `package.json` (`pnpm@11.9.0`). Pull the latest code and rebuild with `docker compose build --no-cache`. Do not rely on unpinned Corepack downloads. + +- **`ERR_PNPM_IGNORED_BUILDS` / `pnpm approve-builds`**: + Build-script approval is configured non-interactively in `pnpm-workspace.yaml` (`allowBuilds`). The Dockerfile copies this file before `pnpm install`. If you see this error on an older clone, update and rebuild with `--no-cache`. Do not run interactive `pnpm approve-builds` inside CI or Docker builds. + +- **Native module build failures (`sharp`, `esbuild`, etc.)**: + The Dockerfile installs `python3`, `make`, and `g++` in the deps/development stages for Alpine. Rebuild with `docker compose build --no-cache` after pulling fixes. + +- **GitHub OAuth redirects with `error=github`**: + 1. Confirm `GITHUB_ID` and `GITHUB_SECRET` match your GitHub OAuth app. + 2. Set `NEXTAUTH_URL` to your public URL with no trailing slash (e.g. `https://devtrack.example.com`). + 3. Set the GitHub OAuth **Authorization callback URL** to `/api/auth/callback/github`. + 4. Enable verbose auth logging: set `AUTH_DEBUG=true` in your `.env` and inspect container logs (`docker compose logs -f`). Look for `[auth]` and `[nextauth]` lines. + +### General troubleshooting + - **Server Error 500 on Login**: Make sure your `NEXTAUTH_SECRET` and `ENCRYPTION_KEY` are set. If `ENCRYPTION_KEY` is missing or the wrong length (must be 32 bytes / 64 hex chars), the OAuth callback will crash when attempting to encrypt the GitHub token. - **Login Redirects back to Home Page infinitely**: diff --git a/package.json b/package.json index 8a86eae67..839c5c64c 100644 --- a/package.json +++ b/package.json @@ -100,8 +100,9 @@ "@opentelemetry/resources": ">=2.8.0", "js-yaml": ">=4.1.0" }, + "packageManager": "pnpm@11.9.0", "engines": { - "node": "20.x" + "node": "20.x || 22.x" }, "optionalDependencies": { "@esbuild/linux-x64": "^0.28.0", @@ -111,21 +112,5 @@ "@parcel/watcher-linux-x64-glibc": "^2.5.6", "@parcel/watcher-linux-x64-musl": "^2.5.6", "@swc/core-linux-x64-gnu": "^1.15.41" - }, - "pnpm": { - "onlyBuiltDependencies": [ - "@parcel/watcher", - "@scarf/scarf", - "@sentry/cli", - "@swc/core", - "@tree-sitter-grammars/tree-sitter-yaml", - "core-js-pure", - "core-js", - "esbuild", - "sharp", - "tree-sitter-json", - "tree-sitter", - "unrs-resolver" - ] } } diff --git a/src/lib/auth-config.ts b/src/lib/auth-config.ts new file mode 100644 index 000000000..6203c9066 --- /dev/null +++ b/src/lib/auth-config.ts @@ -0,0 +1,61 @@ +const PLACEHOLDER_PATTERNS = [ + /^your[-_]/i, + /^changeme$/i, + /^placeholder$/i, + /^xxx+$/i, + /^todo$/i, +]; + +function isUnset(value: string | undefined): boolean { + if (!value || value.trim() === "") return true; + return PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(value.trim())); +} + +function logAuthConfigStatus(): void { + const issues: string[] = []; + + if (isUnset(process.env.GITHUB_ID)) { + issues.push("GITHUB_ID is unset — GitHub sign-in will fail"); + } + if (isUnset(process.env.GITHUB_SECRET)) { + issues.push("GITHUB_SECRET is unset — GitHub sign-in will fail"); + } + if (isUnset(process.env.NEXTAUTH_SECRET)) { + issues.push("NEXTAUTH_SECRET is unset — sessions cannot be signed"); + } + if (isUnset(process.env.NEXTAUTH_URL)) { + issues.push( + "NEXTAUTH_URL is unset — OAuth callbacks may redirect incorrectly" + ); + } else if (process.env.NEXTAUTH_URL?.endsWith("/")) { + issues.push( + "NEXTAUTH_URL has a trailing slash — remove it (e.g. https://example.com)" + ); + } + + if (issues.length > 0) { + console.warn( + "[auth] Self-hosting configuration issues:\n - " + issues.join("\n - ") + ); + } else if (process.env.AUTH_DEBUG === "true") { + console.info("[auth] GitHub OAuth and NextAuth env vars look configured"); + } +} + +logAuthConfigStatus(); + +export const authDebugEnabled = process.env.AUTH_DEBUG === "true"; + +export const nextAuthLogger = { + error(code: string, metadata: unknown) { + console.error("[nextauth]", code, metadata); + }, + warn(code: string) { + console.warn("[nextauth]", code); + }, + debug(code: string, metadata: unknown) { + if (authDebugEnabled) { + console.debug("[nextauth]", code, metadata); + } + }, +}; diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 66b24a92a..ce9fa5903 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,5 +1,6 @@ import { type NextAuthOptions } from "next-auth"; import GitHubProvider from "next-auth/providers/github"; +import { authDebugEnabled, nextAuthLogger } from "./auth-config"; import { syncGitHubAchievementsForUser } from "./github-achievements"; import { supabaseAdmin } from "./supabase"; @@ -14,6 +15,8 @@ const GITHUB_API = "https://api.github.com"; const TOKEN_VALIDATION_INTERVAL_MS = 24 * 60 * 60 * 1000; export const authOptions: NextAuthOptions = { + debug: authDebugEnabled, + logger: nextAuthLogger, // Playwright runs on plain HTTP (127.0.0.1) and relies on the default // `next-auth.session-token` cookie name. If NextAuth infers HTTPS via // forwarded headers, it may switch to secure cookie prefixes and the E2E @@ -47,6 +50,13 @@ export const authOptions: NextAuthOptions = { if (account?.provider === "github" && profile) { const p = profile as { id: number; login: string; email?: string }; + if (authDebugEnabled) { + console.debug("[auth] GitHub signIn callback", { + login: p.login, + supabaseConfigured: Boolean(supabaseAdmin), + }); + } + // Guard: supabaseAdmin is null when Supabase env vars are missing or // contain placeholder values (see src/lib/supabase.ts). Calling .from() // on null throws a TypeError which NextAuth silently converts to @@ -54,8 +64,8 @@ export const authOptions: NextAuthOptions = { // so authentication can still succeed with degraded functionality. if (!supabaseAdmin) { console.warn( - "signIn: supabaseAdmin is not configured; skipping DB upsert. " + - "Set NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY in .env.local." + "[auth] supabaseAdmin is not configured; skipping DB upsert. " + + "Set NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY." ); return true; }