| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149 |
- # Stage 1: Install dependencies (isolated layout, same as local dev)
- FROM node:22-bookworm AS deps
- # 使用阿里云镜像源
- RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources
- ENV COREPACK_NPM_REGISTRY=https://registry.npmmirror.com
- RUN corepack enable && corepack prepare pnpm@10.24.0 --activate
- RUN apt-get update && \
- apt-get install -y --no-install-recommends ffmpeg python3 python3-pip && \
- rm -rf /var/lib/apt/lists/*
- RUN pip3 install --break-system-packages -i https://mirrors.aliyun.com/pypi/simple/ faster-whisper
- WORKDIR /app
- COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./
- COPY apps/cli/package.json apps/cli/
- COPY apps/web/package.json apps/web/
- COPY packages/shared/package.json packages/shared/
- COPY packages/core/package.json packages/core/
- COPY packages/tts/package.json packages/tts/
- COPY packages/templates/package.json packages/templates/
- COPY packages/collect/package.json packages/collect/
- RUN pnpm config set registry https://registry.npmmirror.com && \
- pnpm install --frozen-lockfile
- # Stage 2: Build
- FROM deps AS build
- COPY . .
- RUN pnpm build
- # Stage 3: Runtime — fresh prod install with hoisted layout
- FROM node:22-bookworm-slim AS runtime
- RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources
- ENV COREPACK_NPM_REGISTRY=https://registry.npmmirror.com
- RUN corepack enable && corepack prepare pnpm@10.24.0 --activate
- RUN apt-get update && \
- apt-get install -y --no-install-recommends \
- ffmpeg python3 python3-pip fontconfig \
- libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \
- libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \
- libgbm1 libpango-1.0-0 libcairo2 libasound2 && \
- rm -rf /var/lib/apt/lists/*
- RUN pip3 install --break-system-packages -i https://mirrors.aliyun.com/pypi/simple/ faster-whisper
- WORKDIR /app
- # Copy workspace manifests + lockfile so pnpm install can resolve workspaces
- COPY --from=build /app/package.json /app/pnpm-lock.yaml /app/pnpm-workspace.yaml /app/turbo.json ./
- COPY --from=build /app/apps/cli/package.json apps/cli/
- COPY --from=build /app/apps/web/package.json apps/web/
- COPY --from=build /app/packages/shared/package.json packages/shared/
- COPY --from=build /app/packages/core/package.json packages/core/
- COPY --from=build /app/packages/tts/package.json packages/tts/
- COPY --from=build /app/packages/templates/package.json packages/templates/
- COPY --from=build /app/packages/collect/package.json packages/collect/
- # Install production deps with hoisted layout (everything flat at root node_modules)
- RUN pnpm config set registry https://registry.npmmirror.com && \
- pnpm install --frozen-lockfile --prod --config.node-linker=hoisted
- # Re-create workspace symlinks at root so any package can resolve @pipeline/* by walking up
- RUN mkdir -p node_modules/@pipeline && \
- ln -s ../../packages/shared node_modules/@pipeline/shared && \
- ln -s ../../packages/core node_modules/@pipeline/core && \
- ln -s ../../packages/tts node_modules/@pipeline/tts && \
- ln -s ../../packages/templates node_modules/@pipeline/templates && \
- ln -s ../../packages/collect node_modules/@pipeline/collect
- # Copy build artifacts (dist, .next, template src for Remotion entry, etc.)
- COPY --from=build /app/apps/cli/dist apps/cli/dist/
- COPY --from=build /app/apps/web/.next apps/web/.next/
- COPY --from=build /app/apps/web/next.config.mjs apps/web/
- COPY --from=build /app/apps/web/src apps/web/src/
- COPY --from=build /app/packages/shared/dist packages/shared/dist/
- COPY --from=build /app/packages/core/dist packages/core/dist/
- COPY --from=build /app/packages/tts/dist packages/tts/dist/
- COPY --from=build /app/packages/tts/scripts packages/tts/scripts/
- COPY --from=build /app/packages/templates/dist packages/templates/dist/
- COPY --from=build /app/packages/templates/src packages/templates/src/
- COPY --from=build /app/packages/collect/dist packages/collect/dist/
- COPY --from=build /app/assets assets/
- COPY --from=build /app/config config/
- # Install bundled Noto Sans SC as system font so Chrome can render CJK via fontconfig fallback
- RUN mkdir -p /usr/share/fonts/truetype/noto && \
- cp assets/fonts/NotoSansSC-Regular.ttf assets/fonts/NotoSansSC-Bold.ttf /usr/share/fonts/truetype/noto/ && \
- fc-cache -f
- # Bundle Remotion's headless Chrome so rendering needs NO network at runtime.
- # Remotion normally auto-downloads chrome-headless-shell from Google's CDN
- # (storage.googleapis.com / remotion.media) on first render, but that is
- # ~5KB/s from CN networks and stalls indefinitely. Fetch the SAME binary
- # Remotion wants from the Aliyun-backed npmmirror binary mirror at build time
- # and lay it out exactly as Remotion's BrowserFetcher expects, so
- # ensureBrowser() finds it present (revision.local && existsSync(executablePath))
- # and skips the download. Version is read from the installed @remotion/renderer
- # so this auto-tracks Remotion upgrades instead of hardcoding a number.
- RUN set -eux; \
- SRC=/app/node_modules/@remotion/renderer/dist/browser/get-chrome-download-url.js; \
- VERSION=$(grep -oE "TESTED_VERSION = '[0-9.]+'" "$SRC" | grep -oE "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" | head -1); \
- test -n "$VERSION"; \
- CACHE=/app/node_modules/.remotion/chrome-headless-shell; \
- mkdir -p "$CACHE/linux64"; \
- URL="https://registry.npmmirror.com/-/binary/chrome-for-testing/${VERSION}/linux64/chrome-headless-shell-linux64.zip"; \
- 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; \
- python3 -m zipfile -e /tmp/chs.zip "$CACHE/linux64/"; \
- rm -f /tmp/chs.zip; \
- BIN="$CACHE/linux64/chrome-headless-shell-linux64/chrome-headless-shell"; \
- chmod +x "$BIN"; \
- printf '%s' "$VERSION" > "$CACHE/VERSION"
- # Run as the non-root `node` user (uid/gid 1000, shipped in the base image) so
- # the image is Pod Security "restricted"-friendly. Only the writable paths are
- # chowned to keep the layer small: the PVC-backed output, Next.js' .next cache,
- # and a HOME for Remotion's browser / faster-whisper model download cache.
- ENV HOME=/home/node
- # Remotion downloads its headless Chrome into <projectRoot>/node_modules/.remotion
- # (see @remotion/renderer browser/get-download-destination.js — it walks up to the
- # nearest package.json, here /app, then uses node_modules/.remotion), and the
- # bundler's webpack writes a cache to packages/templates/node_modules/.cache. Both
- # node_modules trees are root-owned from the build, but the container runs as the
- # non-root `node` user, so these runtime-writable cache dirs must be pre-created
- # and chowned — otherwise rendering aborts with
- # `EACCES: permission denied, mkdir '/app/node_modules/.remotion'`.
- RUN mkdir -p /app/output /home/node \
- /app/node_modules/.remotion \
- /app/packages/templates/node_modules/.cache && \
- chown -R node:node /app/output /home/node /app/apps/web/.next \
- /app/node_modules/.remotion /app/packages/templates/node_modules/.cache
- USER node
- EXPOSE 3000
- # Docker Compose / `docker run` health; Kubernetes uses its own probes against
- # the same endpoints. global fetch is available on Node 22.
- HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
- 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))"
- WORKDIR /app/apps/web
- CMD ["/app/node_modules/.bin/next", "start"]
|