Found by running the delivery flow against the actual built container rather
than a local dev process.
DEPLOY-02 (Critical) — ADMIN LOGIN RETURNED 500 FOR EVERY USER IN DOCKER.
docker-compose.yml passes each optional variable as ${X:-}, so an unconfigured
one arrives as "" rather than undefined. The code read them with
`process.env.X ?? 'default'`, which only falls back on undefined, so the empty
string reached jsonwebtoken and it threw
"expiresIn" should be a number of seconds or string representing a timespan
on every sign. Invisible to every existing suite, all of which run against a
local process where the variable is genuinely absent.
Added src/common/env.ts (envOr / envOrNull), which treat blank as unset, and
applied it to every auth, port, upload and SMS read. Three were not cosmetic:
a blank UPLOAD_DIR would have served static files from the FILESYSTEM ROOT, a
blank PORT would have bound a random ephemeral port (Number('') === 0), and a
blank JWT_SECRET would have become the verifying key. Recorded as D25, with a
standing rule to use these readers instead of `??`.
Pinned by src/common/env.spec.ts (9 tests), which reproduces the original throw
against the real jsonwebtoken and asserts the reader can no longer produce it.
DEPLOY-03 (process, documented) — the API image is `COPY release/ ./`, so it
ships a bundle pre-built on the developer machine. `docker compose build` will
happily produce an image from a STALE release/app.js and report success; at the
start of this run that bundle was four days old and the rebuilt container was
missing every route added since. DELIVERY.md's deploy section now leads with
`pnpm build:release`.
New: scripts/test-compose-e2e.cjs — the whole delivery flow against the built
container, over HTTP only, as a black box: a sales expert and an ordinary
customer both sign in with the master code, both place an order, both orders
appear in the admin grid and open there, and the three token boundaries still
hold in the deployed image. 32/32.
Verified: compose e2e 32/32 · jest 54/54 · build 5/5 ·
against the CONTAINER: complete-profile 38/38, report 40/40, expert login 36/36 ·
locally: apps smoke 43/43, admin smoke 43/43, B2B scan 34 ok/0 broken.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
109 lines
3.6 KiB
YAML
109 lines
3.6 KiB
YAML
version: '3.8'
|
|
|
|
services:
|
|
db:
|
|
image: docker.arvancloud.ir/postgres:15-alpine
|
|
container_name: goldbank_db
|
|
environment:
|
|
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
|
POSTGRES_DB: ${POSTGRES_DB:-goldbank}
|
|
ports:
|
|
- "${EXTERNAL_DB_PORT:-5432}:5432"
|
|
volumes:
|
|
- postgres_data:/var/lib/postgresql/data
|
|
restart: unless-stopped
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-goldbank}"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 12
|
|
|
|
api:
|
|
build:
|
|
context: ./apps/api
|
|
dockerfile: Dockerfile
|
|
container_name: goldbank_api
|
|
# Prisma connects on module init, so a not-yet-ready DB crash-loops the API.
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
environment:
|
|
- DATABASE_URL=${DATABASE_URL:-postgresql://postgres:postgres@db:5432/goldbank?schema=public}
|
|
- PORT=${PORT:-3000}
|
|
# Auth. Without these the API falls back to a well-known development
|
|
# secret, so set them in the deployment environment.
|
|
- JWT_SECRET=${JWT_SECRET:-}
|
|
- JWT_EXPIRES_IN=${JWT_EXPIRES_IN:-}
|
|
- JWT_CUSTOMER_SECRET=${JWT_CUSTOMER_SECRET:-}
|
|
- JWT_CUSTOMER_EXPIRES_IN=${JWT_CUSTOMER_EXPIRES_IN:-}
|
|
# ⚠ DELIVERY ONLY — a fixed OTP that signs in ANY active customer.
|
|
# Empty/unset disables it, which is the correct state for production.
|
|
# See ISSUES REL-01. Must be exactly 4 digits to be typable in the apps.
|
|
- OTP_MASTER_CODE=${OTP_MASTER_CODE:-}
|
|
# SMS. All blank = messages are logged, not sent; the API still boots (D20).
|
|
- SMS_PROVIDER=${SMS_PROVIDER:-}
|
|
- SMS_TREZ_URL=${SMS_TREZ_URL:-}
|
|
- SMS_TREZ_USERNAME=${SMS_TREZ_USERNAME:-}
|
|
- SMS_TREZ_PASSWORD=${SMS_TREZ_PASSWORD:-}
|
|
- SMS_TREZ_SENDER_LINE=${SMS_TREZ_SENDER_LINE:-}
|
|
# Admin mobiles told when a customer completes their profile (D22).
|
|
# Blank = nobody is notified; the upload still succeeds.
|
|
- PROFILE_ADMIN_MOBILES=${PROFILE_ADMIN_MOBILES:-}
|
|
# No host ports — Dokploy/Traefik routes by domain to container port 3000
|
|
expose:
|
|
- "3000"
|
|
# Uploaded files (product images, etc.) land in storage/uploads inside the
|
|
# container. Without a volume they vanish on every recreate/redeploy, and
|
|
# every UploadedFile/ProductImage row created before that would then point
|
|
# at a URL that 404s.
|
|
volumes:
|
|
- api_uploads:/app/storage/uploads
|
|
restart: unless-stopped
|
|
# The bare name "api" is ambiguous on Dokploy's shared network (other stacks
|
|
# register it too), which made nginx dial the wrong containers. Use a unique alias.
|
|
networks:
|
|
default:
|
|
aliases:
|
|
- goldbank-api
|
|
|
|
admin-panel:
|
|
build:
|
|
context: ./apps/admin-panel
|
|
dockerfile: Dockerfile
|
|
container_name: goldbank_admin_panel
|
|
# No host ports — Dokploy/Traefik routes by domain to container port 80
|
|
expose:
|
|
- "80"
|
|
restart: unless-stopped
|
|
depends_on:
|
|
- api
|
|
|
|
user-app:
|
|
build:
|
|
context: ./apps/user-app
|
|
dockerfile: Dockerfile
|
|
container_name: goldbank_user_app
|
|
# No host ports — Dokploy/Traefik routes by domain to container port 80
|
|
expose:
|
|
- "80"
|
|
restart: unless-stopped
|
|
depends_on:
|
|
- api
|
|
|
|
# Same copy-only nginx pattern as user-app; map a separate Dokploy domain to this service.
|
|
user-app-v2:
|
|
build:
|
|
context: ./apps/user-app-v2
|
|
dockerfile: Dockerfile
|
|
container_name: goldbank_user_app_v2
|
|
expose:
|
|
- "80"
|
|
restart: unless-stopped
|
|
depends_on:
|
|
- api
|
|
|
|
volumes:
|
|
postgres_data:
|
|
api_uploads:
|