File size: 5,889 Bytes
0984f9f 5b03d64 0984f9f 729138f 0984f9f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | # 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"]
|