# Multi-stage build: build the web bundle + server, then serve them from a # slim runtime image as a single self-hosted container (PLAN.md ยง7). FROM node:22-alpine AS build WORKDIR /app # Build tools for native modules (better-sqlite3) in the build stage. RUN apk add --no-cache python3 make g++ COPY package.json pnpm-workspace.yaml pnpm-lock.yaml tsconfig.base.json ./ COPY packages ./packages COPY apps ./apps COPY server ./server RUN corepack enable && corepack prepare pnpm@9.15.9 --activate \ && pnpm install --frozen-lockfile \ && pnpm build FROM node:22-alpine AS runtime WORKDIR /app ENV NODE_ENV=production # Build tools for native modules (better-sqlite3, argon2) + su-exec for the # entrypoint to drop privileges to the non-root user at runtime. RUN apk add --no-cache python3 make g++ su-exec # Non-root user. RUN addgroup --system --gid 1001 pubscript \ && adduser --system --uid 1001 pubscript # Source + lockfile from the build stage (apps/web/dist carries the built web bundle). COPY --from=build /app/package.json /app/pnpm-workspace.yaml /app/pnpm-lock.yaml ./ COPY --from=build /app/packages ./packages COPY --from=build /app/apps ./apps COPY --from=build /app/server ./server # Install production deps only (native modules compile here). RUN corepack enable && corepack prepare pnpm@9.15.9 --activate \ && pnpm install --prod --frozen-lockfile COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh # Data volume for the SQLite file. VOLUME ["/app/data"] EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:8080/api/health || exit 1 ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] CMD ["sh", "-c", "cd /app/server && exec node --import tsx src/index.ts"]