The Dokploy build failed at "Collecting page data for /api/assistant" with "LOYALY_API_BASE is required in production". The guard was right; where it ran was not. const BASE = resolveBase(); // evaluated on import next build imports every route module to collect page data, the Docker builder stage sets NODE_ENV=production, and LOYALY_API_BASE is a RUNTIME value that is not present while an image is being built. So the check fired against the build instead of against a misconfigured server. The previous comment claimed this matched tokenStore's stance on AUTH_SECRET; it did not - tokenStore.key() is a function, called per use. It is now genuinely the same shape: resolved on first use and memoised, so building needs no platform host and serving still refuses to guess one. The value is also validated rather than merely present. It used to accept any string, so platform.loyaly.ai - which serves THIS console, not the API - was taken and only failed later as a contract_mismatch at first login. Production is now an allowlist of exactly one origin, and the known-wrong host is rejected everywhere with the reason attached, because "rejected" alone sends somebody hunting for a firewall when the fix is one word in a variable. Development stays permissive (LAN, tunnel, container host) minus that same host - nothing a dev machine reaches is production. production https://mcp.loyaly.ai only; missing, http://, platform.loyaly.ai, any other origin, a bare hostname and a non-http scheme all throw development loopback and friends, or unset -> http://127.0.0.1:8088 An invalid or missing value is never cached, so a misconfigured process fails identically on every request rather than once and then differently. AUTH_SECRET is no longer an ENV line in the Dockerfile. A session-signing key in git means anyone who can read the repo can forge a cookie for any user, and every built image carried it in a layer `docker history` will print; Docker's own linter flags the pattern. Both AUTH_SECRET and LOYALY_API_BASE are now supplied by the orchestrator at runtime, and the file says so. ROTATE the old AUTH_SECRET - it remains in this repository's history. Verified: docker build --no-cache with neither variable set compiles, passes TypeScript and collects page data. The built container starts without them, serves /login, and answers the first API call with the configuration error naming the variable. With LOYALY_API_BASE set it reaches the real platform. tsc clean; lint unchanged at the existing baseline. REQUIRED in Dokploy before this deploys: LOYALY_API_BASE=https://mcp.loyaly.ai AUTH_SECRET=<openssl rand -base64 48> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161AMotQ8FxGPZ9gFGb5wiK
76 lines
2.8 KiB
Docker
76 lines
2.8 KiB
Docker
# syntax=docker/dockerfile:1
|
|
|
|
# Stage 1: Install dependencies
|
|
FROM node:22-alpine AS deps
|
|
RUN apk add --no-cache libc6-compat
|
|
WORKDIR /app
|
|
|
|
COPY package.json package-lock.json ./
|
|
# devDeps are required to build (typescript, tailwind, eslint-config-next).
|
|
# This whole stage is discarded — none of it reaches the runner.
|
|
RUN npm ci --no-audit --no-fund
|
|
|
|
# Stage 2: Build the Next.js application
|
|
FROM node:22-alpine AS builder
|
|
WORKDIR /app
|
|
COPY --from=deps /app/node_modules ./node_modules
|
|
COPY . .
|
|
|
|
ENV NEXT_TELEMETRY_DISABLED=1
|
|
ENV NODE_ENV=production
|
|
# Each Docker build starts from a clean layer, so Turbopack's .next/cache is
|
|
# written but never restored. Skipping it cuts ~20% of build CPU (the metric
|
|
# that matters on a 1-vCPU host) and 116 MB off this layer.
|
|
ENV CI_BUILD=1
|
|
|
|
RUN npm run build
|
|
|
|
# Stage 3: Production runner with Next.js Standalone
|
|
FROM node:22-alpine AS runner
|
|
RUN apk add --no-cache libc6-compat
|
|
WORKDIR /app
|
|
|
|
ENV NODE_ENV=production
|
|
ENV NEXT_TELEMETRY_DISABLED=1
|
|
ENV PORT=3000
|
|
ENV HOSTNAME="0.0.0.0"
|
|
|
|
# ── Runtime configuration: supplied by the orchestrator, never baked in ──
|
|
#
|
|
# Two variables are REQUIRED at runtime and are deliberately absent from this
|
|
# image. Set them as Dokploy environment variables / secrets:
|
|
#
|
|
# AUTH_SECRET signs the session cookie and encrypts the platform token
|
|
# bundle. Generate with: openssl rand -base64 48
|
|
# LOYALY_API_BASE the Behavision API origin — https://mcp.loyaly.ai
|
|
# (NOT platform.loyaly.ai, which serves this console)
|
|
#
|
|
# AUTH_SECRET used to be an ENV line here with a literal value, which put a
|
|
# session-forging key in git: anyone who could read the repo could mint a
|
|
# cookie for any user, and every built image carried it in a layer that
|
|
# `docker history` prints. Docker's own linter flags the pattern
|
|
# (SecretsUsedInArgOrEnv). It is gone; rotate the old value.
|
|
#
|
|
# Neither is needed to BUILD. LOYALY_API_BASE is resolved on first use rather
|
|
# than at module load (see apiClient.ts), and sessionToken/tokenStore derive
|
|
# their key per call, so page-data collection never reads either one. Both are
|
|
# read on the first request that needs them, and a missing one fails loudly
|
|
# there instead of silently guessing a host or a key.
|
|
|
|
# Run as a non-root user; nextjs owns nothing it does not need to write.
|
|
RUN addgroup -g 1001 -S nodejs && adduser -u 1001 -S nextjs -G nodejs
|
|
|
|
# Copy public static assets and standalone build output.
|
|
# These three paths are the ENTIRE runtime payload (~57 MB). Never copy the
|
|
# whole .next/ directory here — .next/dev and .next/cache are build-host-only
|
|
# and account for ~1.96 GB.
|
|
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
|
|
|
USER nextjs
|
|
|
|
EXPOSE 3000
|
|
|
|
CMD ["node", "server.js"]
|