Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| # syntax=docker/dockerfile:1 | |
| # | |
| # Combined image for the Hugging Face Space. One container runs two processes | |
| # under supervisord: | |
| # - router (:8080, internal) | |
| # - web (:7860, the port HF exposes) | |
| # | |
| # The database is Postgres on a VPS (DATABASE_URL secret, over a Cloudflare | |
| # tunnel). Generated audio is logged to /audio (a persistent bucket). | |
| FROM oven/bun:1.1.42-debian AS base | |
| WORKDIR /app | |
| # ββ deps ββ | |
| FROM base AS deps | |
| # git + ca-certificates for cloning the private providers over HTTPS; build | |
| # toolchain for native modules (better-sqlite3) when no prebuilt binary matches. | |
| RUN apt-get update \ | |
| && apt-get install -y --no-install-recommends \ | |
| git ca-certificates python3 make g++ \ | |
| && rm -rf /var/lib/apt/lists/* | |
| COPY package.json bun.lock ./ | |
| COPY apps/web/package.json apps/web/package.json | |
| COPY apps/router/package.json apps/router/package.json | |
| # Copy every workspace manifest (providers grow over time, so copy the tree's | |
| # package.json files rather than enumerating each). | |
| COPY packages/ packages/ | |
| # Pull the private providers straight into the workspace using a build-time | |
| # secret (a read-only GH token Space secret). This keeps the private source out | |
| # of BOTH the Space repo and the build context β the Space can be public without | |
| # exposing it. The token is mounted only for this layer and never persisted. | |
| # PRIVATE_PROVIDERS_REPO defaults to the known repo; override via build arg. | |
| # If the secret is absent the build still succeeds (no private providers). | |
| ARG PRIVATE_PROVIDERS_REPO=TTS-AGI/private-providers | |
| # Cache-buster: the `git clone` below is a fixed command, so Docker would reuse | |
| # a stale layer and silently keep an OLD snapshot of the private repo forever | |
| # (new private providers never appear). The deploy passes the private repo's | |
| # current HEAD SHA here; when it changes, this layer's cache key changes and the | |
| # clone re-runs. Defaults to "dev" for local builds. | |
| ARG PRIVATE_PROVIDERS_REF=6ce6680683f6fc856df478b86596389cdcb916d9 | |
| RUN --mount=type=secret,id=PRIVATE_PROVIDERS_TOKEN,mode=0444,required=false \ | |
| echo "private providers ref: ${PRIVATE_PROVIDERS_REF}"; \ | |
| if [ -s /run/secrets/PRIVATE_PROVIDERS_TOKEN ]; then \ | |
| tok="$(cat /run/secrets/PRIVATE_PROVIDERS_TOKEN)"; \ | |
| git clone --depth 1 \ | |
| "https://x-access-token:${tok}@github.com/${PRIVATE_PROVIDERS_REPO}.git" \ | |
| packages/providers/_private \ | |
| && rm -rf packages/providers/_private/.git \ | |
| && echo "cloned private providers into workspace" \ | |
| || { echo "ERROR: token present but private provider clone failed" >&2; exit 1; }; \ | |
| else \ | |
| echo "no PRIVATE_PROVIDERS_TOKEN secret; building without private providers"; \ | |
| fi; \ | |
| # Ensure the dir always exists so later COPY --from=deps never fails. A stub | |
| # package.json (private, not a workspace match) keeps it inert when empty. | |
| if [ ! -e packages/providers/_private/package.json ]; then \ | |
| mkdir -p packages/providers/_private; \ | |
| echo '{"name":"@ttsa-private/_placeholder","private":true,"version":"0.0.0"}' \ | |
| > packages/providers/_private/package.json; \ | |
| fi | |
| # Install after the private package is in place so bun links it as a workspace | |
| # (node_modules/@ttsa-private/providers), resolvable by the router. | |
| RUN bun install | |
| # ββ build the web app ββ | |
| FROM base AS build | |
| COPY --from=deps /app/node_modules ./node_modules | |
| COPY . . | |
| RUN cd apps/web && bun run build | |
| # ββ runtime ββ | |
| FROM base AS runtime | |
| ENV NODE_ENV=production | |
| # System deps: supervisord, ffmpeg (router audio normalization), ca-certificates | |
| # (verify TLS to the Postgres host). The app connects directly to the VPS | |
| # Postgres over a TLS connection (DATABASE_URL) β no tunnel client in the Space. | |
| RUN apt-get update \ | |
| && apt-get install -y --no-install-recommends \ | |
| supervisor ffmpeg ca-certificates \ | |
| && rm -rf /var/lib/apt/lists/* | |
| # App: built web + full source for the router (run from source via bun). | |
| COPY --from=deps /app/node_modules ./node_modules | |
| COPY --from=build /app/apps/web/.next/standalone ./standalone | |
| COPY --from=build /app/apps/web/.next/static ./standalone/apps/web/.next/static | |
| COPY --from=build /app/apps/web/public ./standalone/apps/web/public | |
| COPY apps/router ./apps/router | |
| COPY apps/web/drizzle ./apps/web/drizzle | |
| COPY apps/web/drizzle.config.ts ./apps/web/drizzle.config.ts | |
| COPY apps/web/src/server/db ./apps/web/src/server/db | |
| # Public packages from the repo, then the private providers cloned in `deps` | |
| # (kept out of the repo + build context). The node_modules copied from `deps` | |
| # already contains bun's workspace symlink node_modules/@ttsa-private/providers | |
| # -> packages/providers/_private, so the router resolves it via PROVIDER_PLUGINS. | |
| COPY packages ./packages | |
| COPY --from=deps /app/packages/providers/_private ./packages/providers/_private | |
| # The prompt corpus (Git LFS). Baked into the image so it ships with the deploy | |
| # (the source URL is not guaranteed to stay up). PROMPTS_FILE points the app at | |
| # it; the loader memory-maps it via a line-offset index, never loading it whole. | |
| COPY apps/web/data/combined_prompts.txt /app/data/combined_prompts.txt | |
| ENV PROMPTS_FILE=/app/data/combined_prompts.txt | |
| # Entrypoint + supervisor config. | |
| COPY deploy/space/entrypoint.sh /entrypoint.sh | |
| COPY deploy/space/supervisord.conf /etc/supervisor/conf.d/tts-arena.conf | |
| RUN chmod +x /entrypoint.sh | |
| # HF mounts /data and /audio as persistent buckets; create mountpoints. | |
| RUN mkdir -p /data /audio | |
| EXPOSE 7860 | |
| ENTRYPOINT ["/entrypoint.sh"] | |