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 # 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"]