FROM oven/bun:1.4.4-alpine AS base WORKDIR /app # --- Install dependencies --- # A single root install creates /app/node_modules with hoisted deps and # populates the workspace symlinks for backend/, frontend/, and packages/db/. # The root install materializes the workspace links and hoisted dependencies; # keeping a single install here also makes the image reproducible when Bun # evaluates `workspace:*` dependencies from a package directory. FROM base AS deps COPY package.json bun.lock* ./ COPY vendor/deepmerge ./vendor/deepmerge COPY backend/package.json ./backend/ COPY frontend/package.json ./frontend/ COPY packages/db/package.json ./packages/db/ COPY packages/sdk/package.json ./packages/sdk/ COPY packages/cli/package.json ./packages/cli/ COPY packages/mcp-server/package.json ./packages/mcp-server/ ENV BUN_INSTALL_CONCURRENCY=1 RUN --mount=type=cache,target=/root/.bun/install/cache bun install --frozen-lockfile # --- Build --- # Carry node_modules (workspace symlinks) from the deps stage, then layer # backend source and the workspace package source (packages/db) on top. We # pull packages/db from the build context because the deps stage only has # its package.json (just enough for `bun install` to discover it). FROM base AS build COPY package.json tsconfig.json ./ COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/vendor ./vendor COPY --from=deps /app/backend/node_modules ./backend/node_modules # Backend imports the public SDK/MCP exports. Materialize their workspace links # and source so its build:server prerequisite works in a clean image too. COPY --from=deps /app/packages/sdk/node_modules ./packages/sdk/node_modules COPY --from=deps /app/packages/mcp-server/node_modules ./packages/mcp-server/node_modules COPY packages/sdk/ ./packages/sdk/ COPY packages/mcp-server/ ./packages/mcp-server/ COPY packages/db/ ./packages/db/ COPY backend/src ./backend/src COPY backend/tsconfig.json ./backend/ COPY backend/package.json ./backend/ RUN cd backend && bun run build # --- Migration job --- # This target contains only the DB workspace and its migration runner. It is # used by docker-compose's one-shot `migrate` service and deliberately runs # with MIGRATION_DATABASE_URL (the owner role), never DATABASE_URL. FROM base AS migration COPY package.json tsconfig.json ./ COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/vendor ./vendor COPY packages/db/ ./packages/db/ ENV NODE_ENV=production WORKDIR /app/packages/db CMD ["bun", "run", "scripts/migrate.ts"] # --- Runtime --- # Slim image: only the compiled dist + the root workspace dependency graph. FROM base AS runtime # Create the runtime identity before copying artifacts so BuildKit can assign # ownership while materializing each layer. A recursive chown of the workspace # dependency tree is prohibitively slow on overlay filesystems. RUN addgroup -S app && adduser -S app -G app COPY --chown=app:app --from=deps /app/node_modules ./node_modules COPY --chown=app:app --from=deps /app/vendor ./vendor COPY --chown=app:app --from=deps /app/backend/node_modules ./backend/node_modules COPY --chown=app:app --from=build /app/backend/dist ./backend/dist COPY --chown=app:app backend/package.json ./backend/ ENV NODE_ENV=production ENV API_PORT=50700 WORKDIR /app/backend USER app HEALTHCHECK --interval=30s CMD wget -qO- http://localhost:50700/api/health || exit 1 EXPOSE 50700 CMD ["bun", "run", "dist/index.js"]