diff --git a/Dockerfile b/Dockerfile index e1dd0d8..698ba1e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,9 +15,14 @@ COPY . . ARG NEXT_PUBLIC_SUPABASE_URL ARG NEXT_PUBLIC_SUPABASE_ANON_KEY ARG NEXT_PUBLIC_APP_URL +# Server-only internal Supabase URL (e.g. http://kong-prod:8000). Provided at +# build time so Next inlines it into the Edge middleware bundle; also read at +# runtime by the Node server components. Optional — falls back to the public URL. +ARG SUPABASE_INTERNAL_URL ENV NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL \ NEXT_PUBLIC_SUPABASE_ANON_KEY=$NEXT_PUBLIC_SUPABASE_ANON_KEY \ - NEXT_PUBLIC_APP_URL=$NEXT_PUBLIC_APP_URL + NEXT_PUBLIC_APP_URL=$NEXT_PUBLIC_APP_URL \ + SUPABASE_INTERNAL_URL=$SUPABASE_INTERNAL_URL RUN npm run build FROM node:24.16.0-alpine AS runner diff --git a/docker-compose.yml b/docker-compose.yml index e1fa4d2..5dae572 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,8 @@ # supplied via env vars resolved from the project-dir ".env" file # (in prod and dev that is a symlink -> .env.production). The defaults below are # the PRODUCTION values, so prod works with no extra vars; the dev worktree's -# .env.production sets WEB_PROJECT / WEB_IMAGE / WEB_CONTAINER / WEB_NETWORK. +# .env.production sets WEB_PROJECT / WEB_IMAGE / WEB_CONTAINER / WEB_NETWORK / +# WEB_ALIAS (and SUPABASE_INTERNAL_URL for the internal API hostname). name: ${WEB_PROJECT:-web} services: web: @@ -15,6 +16,7 @@ services: NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL} NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY} NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL} + SUPABASE_INTERNAL_URL: ${SUPABASE_INTERNAL_URL:-} image: ${WEB_IMAGE:-web:1.0.0} container_name: ${WEB_CONTAINER:-web} restart: unless-stopped @@ -29,6 +31,9 @@ services: networks: default: aliases: + # PROD defaults to web-prod; dev's .env.production sets WEB_ALIAS=web-dev + # so the dev container does NOT also claim the web-prod alias (which + # would make Caddy round-robin prod traffic onto the dev build). - ${WEB_ALIAS:-web-prod} healthcheck: test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/"] diff --git a/lib/supabase/admin.ts b/lib/supabase/admin.ts index e902d3c..9ef0cd0 100644 --- a/lib/supabase/admin.ts +++ b/lib/supabase/admin.ts @@ -33,7 +33,10 @@ const adminDispatcher = new Agent({ export function getSupabaseAdmin(): SupabaseClient { if (_admin) return _admin; - const url = process.env.NEXT_PUBLIC_SUPABASE_URL; + // Reach Supabase over the internal Docker network when configured, bypassing + // the Caddy edge/rate limiter; fall back to the public URL otherwise. + const url = + process.env.SUPABASE_INTERNAL_URL ?? process.env.NEXT_PUBLIC_SUPABASE_URL; const key = process.env.SUPABASE_SERVICE_ROLE_KEY; if (!url || !key) { throw new Error('Supabase admin env not configured'); @@ -57,7 +60,9 @@ export function getSupabaseAdmin(): SupabaseClient { } export function getSupabaseAnon(): SupabaseClient { - const url = process.env.NEXT_PUBLIC_SUPABASE_URL; + // Server-side anon client also prefers the internal Docker URL. + const url = + process.env.SUPABASE_INTERNAL_URL ?? process.env.NEXT_PUBLIC_SUPABASE_URL; const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; if (!url || !key) { throw new Error('Supabase anon env not configured'); diff --git a/lib/supabase/server.ts b/lib/supabase/server.ts index 768e428..09fe272 100644 --- a/lib/supabase/server.ts +++ b/lib/supabase/server.ts @@ -4,7 +4,9 @@ import { cookies } from 'next/headers'; export function createSupabaseServerClient() { const cookieStore = cookies(); return createServerClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, + // Prefer the internal Docker URL so server-side calls skip the Caddy edge + // (and its rate limiter); fall back to the public URL when unset. + process.env.SUPABASE_INTERNAL_URL ?? process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { diff --git a/middleware.ts b/middleware.ts index 752bf63..0ae3fd2 100644 --- a/middleware.ts +++ b/middleware.ts @@ -25,7 +25,13 @@ export async function middleware(request: NextRequest) { let response = NextResponse.next({ request }); const supabase = createServerClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, + // Server-side requests reach Supabase directly over the Docker network + // (SUPABASE_INTERNAL_URL, e.g. http://kong-prod:8000) so they bypass + // Caddy's per-IP `/auth/*` rate limiter; all server traffic would + // otherwise share a single NATed gateway IP and saturate that budget, + // intermittently 429ing getUser() and bouncing users to /login. Falls + // back to the public URL when the internal one is not configured. + process.env.SUPABASE_INTERNAL_URL ?? process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: {