FROM node:20-alpine AS base RUN corepack enable && corepack prepare pnpm@9.15.4 --activate WORKDIR /app # --- Dependencies --- FROM base AS deps COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ COPY apps/api/package.json apps/api/ COPY packages/database/package.json packages/database/ COPY packages/proto/package.json packages/proto/ COPY packages/shared/package.json packages/shared/ COPY packages/ui/package.json packages/ui/ # pnpm creates no node_modules for a workspace package that has no # dependencies of its own, and @source/shared has none. The COPY lines below # name that path, so give them an empty directory to find instead of failing # the build on a path pnpm never made. RUN pnpm install --frozen-lockfile --prod=false && mkdir -p packages/shared/node_modules # --- Type check --- # Not an artifact producer: tsconfig.base.json sets noEmit, so this stage only # proves the sources compile. The runtime stages below run TypeScript directly. FROM base AS typecheck COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/apps/api/node_modules ./apps/api/node_modules COPY --from=deps /app/packages/database/node_modules ./packages/database/node_modules COPY --from=deps /app/packages/shared/node_modules ./packages/shared/node_modules COPY . . RUN pnpm --filter @source/shared build && \ pnpm --filter @source/database build && \ pnpm --filter @source/api build # --- Migrate + seed (one-shot) --- # Schema comes from `drizzle-kit push` against src/schema, then the repo's # data migrations, then the idempotent seed. All three are safe to re-run, so # this container can start on every `docker compose up`. FROM base AS migrate WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/packages/database/node_modules ./packages/database/node_modules COPY --from=deps /app/packages/shared/node_modules ./packages/shared/node_modules COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ COPY packages/shared ./packages/shared COPY packages/database ./packages/database WORKDIR /app/packages/database CMD ["sh", "-c", "pnpm exec drizzle-kit push --force && pnpm exec tsx src/migrate.ts && pnpm exec tsx src/seed.ts"] # --- Production --- # # Runs the TypeScript sources through tsx rather than a compiled bundle, the # same way the migrate stage above already does. # # The workspace packages are consumed as TypeScript: every @source/* package # points `main` at ./src/index.ts, which is what lets `pnpm dev` and Vite read # them without a build step. A compiled entry point would resolve those bare # imports to TypeScript files Node cannot load, and @source/proto derives the # path of daemon.proto from its own module URL — a rule written for # `/src/index.ts`. Following that decision here keeps one resolution model for # development and production instead of two that disagree. FROM base AS production WORKDIR /app ENV NODE_ENV=production COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/apps/api/node_modules ./apps/api/node_modules COPY --from=deps /app/packages/database/node_modules ./packages/database/node_modules COPY --from=deps /app/packages/shared/node_modules ./packages/shared/node_modules COPY pnpm-workspace.yaml package.json ./ COPY apps/api ./apps/api COPY packages/database ./packages/database COPY packages/proto ./packages/proto COPY packages/shared ./packages/shared EXPOSE 3000 # 127.0.0.1, not localhost: musl resolves localhost to ::1 first and the # server binds IPv4, so the probe was refused on every run and the # container never left the unhealthy state. HEALTHCHECK --interval=30s --timeout=5s --start-period=10s CMD wget -qO- http://127.0.0.1:3000/api/health || exit 1 # From the package directory, the same way `pnpm dev` runs it: pnpm's isolated # node_modules puts tsx in apps/api/node_modules/.bin, not in the workspace # root, so `pnpm exec` only finds it here. WORKDIR /app/apps/api CMD ["pnpm", "exec", "tsx", "src/index.ts"]