Prod came up with nobody able to log in, in two separate ways.
1. No sign-in account exists. `prisma migrate deploy` creates tables, never
rows, and nothing in the deploy path seeds one — deliberately, since making
an administrator should not be a side effect of shipping code. But
apps/api/scripts was not in the runtime image either, so the only way to
create the first account was to run the script from a developer machine
against a production DATABASE_URL. Ship scripts/ in the image so it can be
run on the host with docker exec. Still never run automatically.
2. Login could not establish a session at all. cookie.secure followed NODE_ENV,
the image sets NODE_ENV=production, and the app is served over plain HTTP —
express-session then silently emits NO Set-Cookie header. POST /auth/login
still answered 200 with the full user object, no session was created, every
later request 403'd, and the UI would have looped back to /login. It reads
as an auth bug and is really a transport mismatch.
The flag is now driven by SESSION_COOKIE_SECURE, still defaulting to
NODE_ENV. An EMPTY value counts as unset rather than false, because compose
turns an absent `${SESSION_COOKIE_SECURE:-}` into the empty string and the
naive check would have quietly dropped Secure on any deployment that merely
passed the variable through.
galactus sets it to "false". That is acceptable ONLY because the host is
reachable exclusively over Tailscale, so WireGuard already encrypts the
wire. It must go back to "true" when the app is served over TLS or exposed
off-tailnet; behind a TLS-terminating proxy, set trust proxy instead.
Verified against live prod: seeded an admin, POST /auth/login returns 200 with
full ADMIN abilities, a wrong password is rejected with 401, and no Set-Cookie
was present before this change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
93 lines
4.4 KiB
Docker
93 lines
4.4 KiB
Docker
FROM node:20-alpine AS base
|
|
WORKDIR /repo
|
|
# Pin pnpm 9 to match pnpm-lock.yaml (lockfileVersion 9.0). pnpm 9 runs
|
|
# dependency build scripts automatically (the v10 build-allowlist gating does
|
|
# not apply), so argon2's native addon + prisma engines build without extra
|
|
# approval config.
|
|
RUN corepack enable && corepack prepare pnpm@9.15.9 --activate
|
|
|
|
FROM base AS deps
|
|
# argon2's native addon has no musl prebuild -> compiles from source here.
|
|
# openssl so `prisma generate` in the build stage sees the same platform the
|
|
# runtime stage does (see the binaryTargets note in schema.prisma).
|
|
RUN apk add --no-cache python3 make g++ openssl
|
|
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
|
COPY apps/api/package.json apps/api/package.json
|
|
COPY apps/web/package.json apps/web/package.json
|
|
COPY packages/database/package.json packages/database/package.json
|
|
# node-linker=hoisted flattens the store into a single npm-style /repo/node_modules
|
|
# so the runtime stage can copy one tree (pnpm's default symlinked layout would
|
|
# break across COPY stages).
|
|
RUN pnpm install --frozen-lockfile --config.node-linker=hoisted
|
|
|
|
FROM deps AS build
|
|
COPY packages/database packages/database
|
|
COPY apps/api apps/api
|
|
RUN pnpm --filter @jorgecuadros/database generate
|
|
RUN pnpm --filter @jorgecuadros/api build
|
|
|
|
FROM node:20-alpine AS runtime
|
|
WORKDIR /repo
|
|
ENV NODE_ENV=production
|
|
|
|
# DB-ops toolchain baked in so the "Operaciones" admin panel can run backups
|
|
# (mysqldump), restores (mysql), and the re-import pipeline (python + mdbtools)
|
|
# from inside the API container. Build deps are installed in a throwaway virtual
|
|
# package so pandas/pyarrow build on musl, then dropped from the final layer.
|
|
# openssl is NOT optional: Prisma's query engine resolves its binary target at
|
|
# runtime (linux-musl-openssl-3.0.x) and aborts with "Please manually install
|
|
# OpenSSL" without it. Node bundles its own OpenSSL, so nothing else in this
|
|
# image pulls the system package in.
|
|
RUN apk add --no-cache python3 mdbtools mysql-client openssl \
|
|
&& apk add --no-cache --virtual .pybuild python3-dev build-base \
|
|
&& rm -rf /var/cache/apk/*
|
|
|
|
COPY --from=build /repo/node_modules node_modules
|
|
COPY --from=build /repo/packages/database packages/database
|
|
COPY --from=build /repo/apps/api/dist apps/api/dist
|
|
COPY --from=build /repo/apps/api/package.json apps/api/package.json
|
|
# Operational scripts, run on demand — never automatically. seed-user.mjs is the
|
|
# only way to create the first sign-in account on a fresh database, and without
|
|
# it in the image that had to be done from a developer's machine against a
|
|
# production DATABASE_URL. Run it with:
|
|
# docker exec <api> node apps/api/scripts/seed-user.mjs
|
|
# honouring SEED_EMAIL / SEED_PASSWORD / SEED_NAME. It upserts, so re-running is
|
|
# safe — but note it RESETS the password of an existing account.
|
|
COPY --from=build /repo/apps/api/scripts apps/api/scripts
|
|
# node-linker=hoisted flattens EXTERNAL deps into /repo/node_modules, but the
|
|
# workspace dependency is still linked per-package:
|
|
# apps/api/node_modules/@jorgecuadros/database -> ../../../../packages/database
|
|
# Copying only /repo/node_modules therefore drops it and the API dies at boot
|
|
# with "Cannot find module '@jorgecuadros/database'". Copy just the scope dir —
|
|
# the rest of apps/api/node_modules is devDependencies (typescript) we don't
|
|
# want in the runtime layer. The relative link resolves because packages/database
|
|
# is copied to the same place above.
|
|
COPY --from=build /repo/apps/api/node_modules/@jorgecuadros apps/api/node_modules/@jorgecuadros
|
|
|
|
# Migration scripts + their own Python venv (ops.service.ts prefers this venv).
|
|
COPY migration migration
|
|
RUN python3 -m venv migration/.venv \
|
|
&& migration/.venv/bin/pip install --no-cache-dir -r migration/requirements.txt \
|
|
&& apk del .pybuild
|
|
|
|
# Ingest (uploaded Access files) and backups live on mounted volumes.
|
|
ENV MIGRATION_DIR=/repo/migration \
|
|
INGEST_DIR=/data/ingest \
|
|
BACKUP_DIR=/data/backups \
|
|
MIGRATION_ENV=dev
|
|
RUN mkdir -p /data/ingest /data/backups
|
|
|
|
# Build/version metadata baked in at image build time (see .gitea/workflows/build.yml).
|
|
# APP_VERSION is the metadata-action primary tag (semver tag, branch, or sha);
|
|
# GIT_SHA/BUILD_DATE pin the exact commit + build instant. Exposed as ENV so a
|
|
# running container can self-report what is deployed (e.g. a /version endpoint).
|
|
ARG APP_VERSION=dev
|
|
ARG GIT_SHA=unknown
|
|
ARG BUILD_DATE=unknown
|
|
ENV APP_VERSION=$APP_VERSION \
|
|
GIT_SHA=$GIT_SHA \
|
|
BUILD_DATE=$BUILD_DATE
|
|
|
|
EXPOSE 3001
|
|
CMD ["node", "apps/api/dist/main.js"]
|