| # syntax=docker/dockerfile:1 | |
| # | |
| # Hugging Face Space (Docker SDK) for Mixly. | |
| # | |
| # The app source lives in the PRIVATE GitHub repo, not in this Space repo β this | |
| # Dockerfile clones it at build time using the GITHUB_TOKEN *build secret*, builds | |
| # the Next.js app, then runs it. Keeping the Space tiny means a rebuild always | |
| # pulls the latest `main` from GitHub. | |
| # | |
| # The SQLite DB lives on the /data mount. On boot it is restored from the HF Storage | |
| # Bucket (S3 API) if /data is empty, so data survives even without paid Persistent | |
| # Storage (see scripts/restore-db.mjs). | |
| # ---- builder ---- | |
| FROM node:24-slim AS builder | |
| WORKDIR /app | |
| RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \ | |
| && rm -rf /var/lib/apt/lists/* | |
| # Which repo/branch to deploy. Override GITHUB_REF (Space variable) to pin a release. | |
| ARG GITHUB_REPO=github.com/phamdung2209/Mixly.git | |
| ARG GITHUB_REF=main | |
| # Clone with the token mounted as a build secret β it is never written into an | |
| # image layer. Add GITHUB_TOKEN in Space β Settings β Secrets. | |
| RUN --mount=type=secret,id=GITHUB_TOKEN,required=true \ | |
| git clone --depth 1 --branch "${GITHUB_REF}" \ | |
| "https://x-access-token:$(cat /run/secrets/GITHUB_TOKEN)@${GITHUB_REPO}" . | |
| ENV NEXT_TELEMETRY_DISABLED=1 | |
| # Placeholder values ONLY so env validation passes during `next build` (some server | |
| # modules read env at import). The app reads the REAL values from HF secrets at | |
| # runtime; none of these are NEXT_PUBLIC, so nothing is baked into the output. | |
| ENV SHOPIFY_API_KEY=build SHOPIFY_API_SECRET=build SHOPIFY_SCOPES=read_products \ | |
| SHOPIFY_APP_URL=https://build.invalid SHOPIFY_API_VERSION=2026-07 DATABASE_URL=file:/tmp/build.db | |
| # NEXT_PUBLIC_* is inlined into the client bundle at BUILD time, so it must exist | |
| # during `npm run build` β a runtime secret is too late. The Shopify API key is | |
| # public (it ships in the browser), so mounting the secret just for the build is safe. | |
| RUN --mount=type=secret,id=NEXT_PUBLIC_SHOPIFY_API_KEY \ | |
| --mount=type=secret,id=NEXT_PUBLIC_CRISP_WEBSITE_ID \ | |
| export NEXT_PUBLIC_SHOPIFY_API_KEY="$(cat /run/secrets/NEXT_PUBLIC_SHOPIFY_API_KEY 2>/dev/null)" \ | |
| NEXT_PUBLIC_CRISP_WEBSITE_ID="$(cat /run/secrets/NEXT_PUBLIC_CRISP_WEBSITE_ID 2>/dev/null)" && \ | |
| npm ci && npx prisma generate && npm run build && mkdir -p public | |
| # Deploy the Shopify Function + app config as part of the Space build, so ONE rebuild | |
| # ships the web app AND the extensions together. Only runs on a real rebuild (not on | |
| # restart/wake), so no version churn. NON-FATAL: a Shopify hiccup never breaks the web | |
| # deploy. Needs the SHOPIFY_CLI_PARTNERS_TOKEN Space secret (else it just skips). | |
| RUN --mount=type=secret,id=SHOPIFY_CLI_PARTNERS_TOKEN \ | |
| if [ -s /run/secrets/SHOPIFY_CLI_PARTNERS_TOKEN ]; then \ | |
| export SHOPIFY_CLI_PARTNERS_TOKEN="$(cat /run/secrets/SHOPIFY_CLI_PARTNERS_TOKEN)" CI=true; \ | |
| for d in extensions/*/; do [ -f "${d}package.json" ] && (cd "$d" && npm install --no-audit --no-fund || true); done; \ | |
| npx shopify app deploy --config=production --allow-updates || echo "WARN: shopify app deploy failed (non-fatal)"; \ | |
| else echo "SHOPIFY_CLI_PARTNERS_TOKEN not set β skipping Shopify deploy"; fi | |
| # ---- runner ---- | |
| FROM node:24-slim AS runner | |
| WORKDIR /app | |
| ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 PORT=3000 HOSTNAME=0.0.0.0 | |
| # tini as PID 1: forwards SIGTERM to the app and reaps zombies. Without it (e.g. | |
| # `npm start` as PID 1) npm swallows SIGTERM and never forwards it to the Node | |
| # server, so the shutdown DB backup (src/lib/backup.ts) never runs on a rebuild. | |
| RUN apt-get update && apt-get install -y --no-install-recommends tini \ | |
| && rm -rf /var/lib/apt/lists/* | |
| # node:24-slim already ships a `node` user at UID 1000 (which HF mounts /data for) β | |
| # reuse it; creating a second UID-1000 user fails with "UID 1000 is not unique". | |
| COPY --from=builder --chown=node /app/node_modules ./node_modules | |
| COPY --from=builder --chown=node /app/.next ./.next | |
| COPY --from=builder --chown=node /app/public ./public | |
| COPY --from=builder --chown=node /app/package.json ./package.json | |
| COPY --from=builder --chown=node /app/next.config.ts ./next.config.ts | |
| COPY --from=builder --chown=node /app/prisma ./prisma | |
| COPY --from=builder --chown=node /app/prisma.config.ts ./prisma.config.ts | |
| COPY --from=builder --chown=node /app/scripts ./scripts | |
| # Make /data writable with OR without Persistent Storage. If enabled, HF mounts | |
| # /data (uid 1000) over this. If not, this ephemeral dir is used and data still | |
| # survives rebuilds via the S3 bucket restore on boot (scripts/restore-db.mjs). | |
| RUN mkdir -p /data && chown node:node /data | |
| USER node | |
| EXPOSE 3000 | |
| # 1) restore the DB from the HF Storage Bucket (S3) if /data is empty, 2) apply migrations, | |
| # 3) start. Restore MUST run before migrate β migrate would otherwise create an | |
| # empty DB and the restore would think data already exists. | |
| # `exec next` (NOT `npm start`) so the Node server itself becomes the process that | |
| # receives SIGTERM on rebuild/sleep β that's what triggers the final DB backup | |
| # (src/lib/backup.ts). npm would fork the server as a child and swallow the signal. | |
| # tini (ENTRYPOINT) forwards the signal and reaps zombies. | |
| ENTRYPOINT ["tini", "--"] | |
| # Prisma Studio runs in the background on port 5555. NOTE: prisma studio v7 binds | |
| # 0.0.0.0 (no hostname flag) β it is NOT localhost-only. It stays private because HF | |
| # routes ONLY port 3000 as ingress and network-isolates the container; the DB is | |
| # reached via the operator + same-origin gated route handlers (src/lib/ops/studio-proxy) | |
| # at /ops/studio. Do NOT reuse this where 5555 is published (e.g. docker -p 5555:5555). | |
| CMD ["sh", "-c", "node scripts/restore-db.mjs && node_modules/.bin/prisma migrate deploy && (node_modules/.bin/prisma studio --port 5555 --browser none &) && exec node_modules/.bin/next start"] | |