Dockerfile 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. # Stage 1: Install dependencies (isolated layout, same as local dev)
  2. FROM node:22-bookworm AS deps
  3. RUN corepack enable && corepack prepare pnpm@10.24.0 --activate
  4. RUN apt-get update && \
  5. apt-get install -y --no-install-recommends ffmpeg python3 python3-pip && \
  6. rm -rf /var/lib/apt/lists/*
  7. RUN pip3 install --break-system-packages faster-whisper
  8. WORKDIR /app
  9. COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./
  10. COPY apps/cli/package.json apps/cli/
  11. COPY apps/web/package.json apps/web/
  12. COPY packages/shared/package.json packages/shared/
  13. COPY packages/core/package.json packages/core/
  14. COPY packages/tts/package.json packages/tts/
  15. COPY packages/templates/package.json packages/templates/
  16. COPY packages/collect/package.json packages/collect/
  17. RUN pnpm install --frozen-lockfile
  18. # Stage 2: Build
  19. FROM deps AS build
  20. COPY . .
  21. # `next start` only reads .next/{server,static,types,*.json}; the 189MB
  22. # .next/cache is a webpack/swc build cache used solely by `next build`/dev.
  23. # Drop it here so it never enters the runtime COPY (and never gets re-duplicated
  24. # by the writable-dirs step below).
  25. RUN pnpm build && rm -rf apps/web/.next/cache
  26. # Stage 3: Runtime — fresh prod install with hoisted layout
  27. FROM node:22-bookworm-slim AS runtime
  28. RUN corepack enable && corepack prepare pnpm@10.24.0 --activate
  29. RUN apt-get update && \
  30. apt-get install -y --no-install-recommends \
  31. ffmpeg python3 python3-pip fontconfig \
  32. libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \
  33. libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \
  34. libgbm1 libpango-1.0-0 libcairo2 libasound2 && \
  35. rm -rf /var/lib/apt/lists/*
  36. RUN pip3 install --break-system-packages faster-whisper
  37. WORKDIR /app
  38. # Copy workspace manifests + lockfile so pnpm install can resolve workspaces
  39. COPY --from=build /app/package.json /app/pnpm-lock.yaml /app/pnpm-workspace.yaml /app/turbo.json ./
  40. COPY --from=build /app/apps/cli/package.json apps/cli/
  41. COPY --from=build /app/apps/web/package.json apps/web/
  42. COPY --from=build /app/packages/shared/package.json packages/shared/
  43. COPY --from=build /app/packages/core/package.json packages/core/
  44. COPY --from=build /app/packages/tts/package.json packages/tts/
  45. COPY --from=build /app/packages/templates/package.json packages/templates/
  46. COPY --from=build /app/packages/collect/package.json packages/collect/
  47. # Install production deps with hoisted layout (everything flat at root node_modules)
  48. RUN pnpm install --frozen-lockfile --prod --config.node-linker=hoisted
  49. # Re-create workspace symlinks at root so any package can resolve @pipeline/* by walking up
  50. RUN mkdir -p node_modules/@pipeline && \
  51. ln -s ../../packages/shared node_modules/@pipeline/shared && \
  52. ln -s ../../packages/core node_modules/@pipeline/core && \
  53. ln -s ../../packages/tts node_modules/@pipeline/tts && \
  54. ln -s ../../packages/templates node_modules/@pipeline/templates && \
  55. ln -s ../../packages/collect node_modules/@pipeline/collect
  56. # Copy build artifacts (dist, .next, template src for Remotion entry, etc.)
  57. COPY --from=build /app/apps/cli/dist apps/cli/dist/
  58. COPY --from=build --chown=node:node /app/apps/web/.next apps/web/.next/
  59. COPY --from=build /app/apps/web/next.config.mjs apps/web/
  60. COPY --from=build /app/apps/web/src apps/web/src/
  61. COPY --from=build /app/packages/shared/dist packages/shared/dist/
  62. COPY --from=build /app/packages/core/dist packages/core/dist/
  63. COPY --from=build /app/packages/tts/dist packages/tts/dist/
  64. COPY --from=build /app/packages/tts/scripts packages/tts/scripts/
  65. COPY --from=build /app/packages/templates/dist packages/templates/dist/
  66. COPY --from=build /app/packages/templates/src packages/templates/src/
  67. COPY --from=build /app/packages/collect/dist packages/collect/dist/
  68. COPY --from=build /app/assets assets/
  69. COPY --from=build /app/config config/
  70. # Install bundled Noto Sans SC as system font so Chrome can render CJK via fontconfig fallback
  71. RUN mkdir -p /usr/share/fonts/truetype/noto && \
  72. cp assets/fonts/NotoSansSC-Regular.ttf assets/fonts/NotoSansSC-Bold.ttf /usr/share/fonts/truetype/noto/ && \
  73. fc-cache -f
  74. # Bundle Remotion's headless Chrome so rendering needs NO network at runtime.
  75. # Remotion normally auto-downloads chrome-headless-shell from Google's CDN
  76. # (storage.googleapis.com / remotion.media) on first render. Fetch the SAME
  77. # binary at build time from the official Chrome for Testing public bucket and
  78. # lay it out exactly as Remotion's BrowserFetcher expects, so
  79. # ensureBrowser() finds it present (revision.local && existsSync(executablePath))
  80. # and skips the download. Version is read from the installed @remotion/renderer
  81. # so this auto-tracks Remotion upgrades instead of hardcoding a number.
  82. RUN set -eux; \
  83. SRC=/app/node_modules/@remotion/renderer/dist/browser/get-chrome-download-url.js; \
  84. VERSION=$(grep -oE "TESTED_VERSION = '[0-9.]+'" "$SRC" | grep -oE "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" | head -1); \
  85. test -n "$VERSION"; \
  86. CACHE=/app/node_modules/.remotion/chrome-headless-shell; \
  87. mkdir -p "$CACHE/linux64"; \
  88. URL="https://storage.googleapis.com/chrome-for-testing-public/${VERSION}/linux64/chrome-headless-shell-linux64.zip"; \
  89. node -e "const fs=require('fs');fetch(process.argv[1]).then(r=>r.arrayBuffer()).then(b=>fs.writeFileSync(process.argv[2],Buffer.from(b)))" "$URL" /tmp/chs.zip; \
  90. python3 -m zipfile -e /tmp/chs.zip "$CACHE/linux64/"; \
  91. rm -f /tmp/chs.zip; \
  92. BIN="$CACHE/linux64/chrome-headless-shell-linux64/chrome-headless-shell"; \
  93. chmod +x "$BIN"; \
  94. printf '%s' "$VERSION" > "$CACHE/VERSION"
  95. # Run as the non-root `node` user (uid/gid 1000, shipped in the base image) so
  96. # the image is Pod Security "restricted"-friendly. We only chown the SMALL,
  97. # empty runtime-writable dirs (output, HOME, webpack cache). We deliberately do
  98. # NOT `chown -R` over .next or the Remotion chrome tree: in overlayfs a
  99. # recursive chown copies every touched file into a fresh layer, which here
  100. # duplicated ~470MB (.next + chrome) for no functional benefit.
  101. # - .next is already node-owned via `COPY --chown` above.
  102. # - The Remotion chrome under .remotion is root-owned but world a+rx (default
  103. # umask 022 plus the explicit `chmod +x` on the binary), so the node user
  104. # can read and execute it. We only chmod the .remotion *directory* 777 so a
  105. # stray write (none expected once chrome is pre-placed) can still succeed.
  106. ENV HOME=/home/node
  107. RUN mkdir -p /app/output /home/node \
  108. /app/node_modules/.remotion \
  109. /app/packages/templates/node_modules/.cache && \
  110. chown -R node:node /app/output /home/node \
  111. /app/packages/templates/node_modules/.cache && \
  112. chmod 777 /app/node_modules/.remotion
  113. USER node
  114. EXPOSE 3000
  115. # Docker Compose / `docker run` health; Kubernetes uses its own probes against
  116. # the same endpoints. global fetch is available on Node 22.
  117. HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
  118. CMD node -e "fetch('http://127.0.0.1:3000/api/health/live').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
  119. WORKDIR /app/apps/web
  120. CMD ["/app/node_modules/.bin/next", "start"]