Files
loyaly-merchant/.env
Aravind 0dc865ba66 fix(deploy): drop the healthcheck that reproduced the 502 it was meant to end
The HEALTHCHECK added an hour ago recreated the exact symptom. Dokploy runs
applications as Docker Swarm services, and Swarm does not merely report an
unhealthy task — it removes it from the service load balancer and reschedules
it. /api/health answers 503 while a required variable is missing, so:

  AUTH_SECRET unset -> /api/health 503 -> task unhealthy -> pulled out of the
  load balancer -> Traefik has no backend -> 502 Bad Gateway on every url.

The container was up and serving a 503 that names the fault, and nothing could
reach it to read that 503. "A broken deploy must not look healthy" is a real
concern, but enforcing it in the orchestrator destroys the diagnostics, and an
outage whose reason cannot be seen is the more expensive failure. The container
now stays in rotation whenever it can serve HTTP at all.

Also, two things that make a secret that was SET look like one that was not:

- Recommend `openssl rand -hex 32` everywhere instead of `openssl rand -base64
  48`. A base64 value ends in '=' and may contain '+' and '/'; pasted into a
  dashboard field or a KEY=VALUE editor that splits on the first '=', it can be
  stored truncated or empty, which is indistinguishable from never setting it.
  Hex is [0-9a-f] only, so there is nothing for a parser to mangle.
- Treat a whitespace-only AUTH_SECRET as missing, and print the secret's LENGTH
  (never its value) in the boot log. `openssl rand -hex 32` is 64 characters, so
  a much shorter number there is a value that arrived truncated — which
  otherwise presents as sessions that do not verify, with nothing to explain it.

Verified on the rebuilt standalone payload: absent -> container alive, 503
`x-loyaly-config: misconfigured` on /, /login and /api/sites; whitespace -> the
same; a real hex secret -> / 307, /login 200, /favicon.ico 200, /api/health 200
and `auth secret set (64 chars)` in the log. tsc --noEmit and eslint clean,
production build exits 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 23:08:03 +05:30

62 lines
3.5 KiB
Bash

# ---------------------------------------------------------------------------
# Production runtime configuration. COMMITTED ON PURPOSE — carries no secret.
# ---------------------------------------------------------------------------
#
# This file is the production environment. It is read by `next build` and, more
# importantly, by the standalone `server.js` at boot (Next calls loadEnvConfig
# on the server's working directory), so the deployed container knows the
# platform host without anyone remembering to type it into a dashboard.
#
# ── Precedence, exactly as @next/env resolves it ────────────────────────────
#
# 1. real process.env (Dokploy / docker -e / systemd) ← always wins
# 2. .env.production.local
# 3. .env.local ← LOCAL DEV ONLY. Never enters the image.
# 4. .env.production
# 5. .env ← this file, the floor everything falls back to
#
# A value already present in process.env is never overwritten by a file, so
# setting LOYALY_API_BASE in Dokploy still overrides this — nothing here locks
# the deployment in. It only removes "unset" as a possible state.
#
# ── Working on this locally? ────────────────────────────────────────────────
# Put your overrides in `.env.local` (gitignored, loaded ahead of this file).
# Without one, `npm run dev` will talk to the PRODUCTION platform, because that
# is what this file says. `.env.example` has the local values to copy.
# The one shared Loyaly platform API (Behavision). Server-side only and
# deliberately NOT NEXT_PUBLIC: publishing the host would let a browser bypass
# the BFF, which is what keeps the access token out of JavaScript.
#
# NOT platform.loyaly.ai — that host serves THIS console, not the API. Pointing
# the variable there makes the BFF call its own origin, which fails in a way
# that looks like a broken login form rather than a misconfiguration.
# apiClient.ts rejects that hostname by name for exactly this reason.
LOYALY_API_BASE=https://mcp.loyaly.ai
# Browser → this app's own BFF routes, which are same-origin. Empty is correct
# and is what makes the console work on any hostname it is served from:
# requests go to /api/... on whatever origin loaded the page (localhost:3100 in
# dev, platform.loyaly.ai in production) and the server hop above reaches the
# platform. Setting this to the platform host would send the browser straight
# at the API with no session cookie and no token — do not.
#
# It is NEXT_PUBLIC, so it is inlined at BUILD time, not read at runtime.
# Changing it in Dokploy's environment panel would do nothing without a rebuild.
NEXT_PUBLIC_API_BASE=
# AUTH_SECRET is deliberately NOT in this file.
#
# It signs the session cookie and encrypts the platform token bundle, so a
# value committed here is a session-forging key in git — anyone who can read
# the repo could mint a cookie for any user. It was already removed from the
# Dockerfile once for that reason; do not reintroduce it here.
#
# Set it as a Dokploy environment variable / secret. Production refuses to sign
# sessions without it. Generate with: openssl rand -hex 32
#
# Hex, not base64: a base64 value ends in '=' and can contain '+' and '/', and
# an environment editor that splits a line on the first '=' can store that
# truncated or empty. A silently-empty AUTH_SECRET looks exactly like an unset
# one, which is a slow afternoon. Hex has nothing a parser can mangle.