# ───────────────────────────────────────────── # Stage 1: Build the Vite / React frontend # ───────────────────────────────────────────── FROM node:22-alpine AS frontend-builder # pnpm via corepack — version pinned in root package.json ("packageManager") RUN corepack enable WORKDIR /app COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ # patches/ es necesario: pnpm-workspace.yaml referencia un patchedDependency # (react-native-css-interop) y `pnpm install --frozen-lockfile` falla con ENOENT # si el archivo del patch no está en el contexto. COPY patches/ ./patches/ COPY apps/web/package.json apps/web/ COPY packages/core/package.json packages/core/ RUN pnpm install --frozen-lockfile COPY apps/web/ apps/web/ COPY packages/core/ packages/core/ # Empty → pocketbase.js falls back to window.location.origin ARG VITE_POCKETBASE_URL="" ENV VITE_POCKETBASE_URL=$VITE_POCKETBASE_URL # Empty in dev (uses Vite proxy). In prod: absolute URL of the AI API service. ARG VITE_AI_API_URL="" ENV VITE_AI_API_URL=$VITE_AI_API_URL # VAPID public key for Web Push subscriptions (baked into frontend JS) ARG VITE_VAPID_PUBLIC_KEY="" ENV VITE_VAPID_PUBLIC_KEY=$VITE_VAPID_PUBLIC_KEY # Release de Sentry, única por deploy: `calistenia-app@+`. # Es solo un identificador, no un secreto, así que ARG normal. Vacío en un build # local → vite.config.js cae al semver de package.json. ARG SENTRY_RELEASE="" ENV SENTRY_RELEASE=$SENTRY_RELEASE # El token va como SECRET de buildkit, NO como ARG: un build-arg queda escrito # en los metadatos de la imagen y `docker history` lo enseña. Montado así solo # existe durante este RUN y no toca ninguna capa. # # `|| true` porque en un build sin el secreto (local, o un PR de un fork) el # fichero no existe: el build debe seguir, y sentryVitePlugin se apaga solo al # no ver token. Subir source maps NUNCA debe poder tumbar un deploy. RUN --mount=type=secret,id=sentry_auth_token \ SENTRY_AUTH_TOKEN="$(cat /run/secrets/sentry_auth_token 2>/dev/null || true)" \ pnpm --filter @calistenia/web build # ───────────────────────────────────────────── # Stage 2: Download PocketBase # ───────────────────────────────────────────── FROM alpine:3.19 AS pb-downloader # Mantener en sync con Dockerfile.pb-dev y el job e2e-smoke de ci.yml — el # smoke E2E corre contra esta misma versión de PocketBase. ARG PB_VERSION=0.36.8 RUN apk add --no-cache curl unzip \ && curl -fsSL --retry 5 --retry-all-errors --retry-delay 3 \ "https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_amd64.zip" \ -o /tmp/pocketbase.zip \ && unzip /tmp/pocketbase.zip -d /tmp/pb \ && chmod +x /tmp/pb/pocketbase # ───────────────────────────────────────────── # Stage 3: Final runtime image # ───────────────────────────────────────────── FROM alpine:3.19 RUN apk add --no-cache ca-certificates wget RUN adduser -D -u 1001 pbuser WORKDIR /app COPY --from=pb-downloader /tmp/pb/pocketbase ./pocketbase COPY pb_migrations/ ./pb_migrations/ COPY pb_hooks/ ./pb_hooks/ COPY --from=frontend-builder /app/apps/web/dist ./pb_public RUN mkdir -p /app/pb_data && chown -R pbuser:pbuser /app USER pbuser EXPOSE 8090 HEALTHCHECK --interval=10s --timeout=3s --start-period=5s \ CMD wget -qO- http://localhost:8090/api/health || exit 1 CMD ["./pocketbase", "serve", \ "--http=0.0.0.0:8090", \ "--dir=/app/pb_data", \ "--migrationsDir=/app/pb_migrations", \ "--hooksDir=/app/pb_hooks", \ "--publicDir=/app/pb_public"]