# 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"]