Commit Β·
0984f9f
0
Parent(s):
Initial commit: Mixly HF Space deploy config
Browse filesModelled on Host-Tierly. Adapted after verifying against Mixly's
actual source (scripts/restore-db.mjs, src/lib/backup.ts, src/lib/s3.ts,
src/instrumentation.ts, src/lib/env.ts, shopify.app.toml):
- DB path /data/mixly.db, bucket Mixly-storage, snapshot prefix mixly-
- Space solora-hq/Mixly, GitHub source phamdung2209/Mixly
- Kept Prisma Studio proxy (Mixly has src/lib/ops/studio-proxy.ts)
- Dropped Crisp build secret (no Crisp integration in Mixly)
- Dropped --config=production (only a default shopify.app.toml exists)
- Scopes/API version taken from Mixly's own shopify.app.toml
- .gitattributes +35 -0
- DEPLOY.md +155 -0
- Dockerfile +96 -0
- README.md +22 -0
.gitattributes
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
DEPLOY.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Mixly on Hugging Face Spaces β deploy & operations
|
| 2 |
+
|
| 3 |
+
The Space (`solora-hq/Mixly`) is a **Docker** host. Its `Dockerfile` clones the
|
| 4 |
+
**private** GitHub repo `phamdung2209/Mixly` at build time and runs the Next.js app.
|
| 5 |
+
|
| 6 |
+
Public URL: `https://solora-hq-mixly.hf.space`
|
| 7 |
+
|
| 8 |
+
> β οΈ **Data isolation.** `solora-hq` also hosts **Tierly**, which is LIVE with real
|
| 9 |
+
> merchant data. Mixly's `DATABASE_URL`, `S3_BUCKET`, and Space name are a
|
| 10 |
+
> completely separate set of identifiers from Tierly's β never copy a value from
|
| 11 |
+
> Tierly's `DEPLOY.md` here. Concretely: DB path is `/data/mixly.db` (not
|
| 12 |
+
> `tierly.db`), bucket is `Mixly-storage` (not `Tierly-storage`), Space is
|
| 13 |
+
> `solora-hq/Mixly` (not `solora-hq/Tierly`). Pointing Mixly at any of Tierly's
|
| 14 |
+
> identifiers risks corrupting or leaking live merchant data.
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## 0. First-time setup (do this once β the Space doesn't exist yet)
|
| 19 |
+
|
| 20 |
+
1. **Create the Space**: https://huggingface.co/new-space β Owner `solora-hq`,
|
| 21 |
+
Space name `Mixly`, **SDK: Docker**, visibility your choice, no template.
|
| 22 |
+
2. **Add the remote and push this repo**:
|
| 23 |
+
```
|
| 24 |
+
git remote add origin https://huggingface.co/spaces/solora-hq/Mixly
|
| 25 |
+
git push -u origin main
|
| 26 |
+
```
|
| 27 |
+
The first build will fail (or crash-loop) until the secrets in Β§2 are set β
|
| 28 |
+
that's expected, HF just needs a repo to attach the build to.
|
| 29 |
+
3. **Create the S3 storage bucket**: HF β your `solora-hq` org β **Storage** β
|
| 30 |
+
**New bucket** β name it exactly `Mixly-storage` (bare name, no slashes). Double
|
| 31 |
+
check it is not `Tierly-storage`.
|
| 32 |
+
4. **Set every secret in Β§2 below** (Space β Settings β Variables and secrets),
|
| 33 |
+
then Space β Settings β **Factory rebuild**.
|
| 34 |
+
|
| 35 |
+
---
|
| 36 |
+
|
| 37 |
+
## 1. GitHub token (required β the repo is private)
|
| 38 |
+
|
| 39 |
+
Cloning a private repo inside the Docker build needs a token. Create a
|
| 40 |
+
**fine-grained Personal Access Token**, scoped to just this repo, read-only:
|
| 41 |
+
|
| 42 |
+
1. GitHub β **Settings β Developer settings β Personal access tokens β Fine-grained tokens β Generate new**.
|
| 43 |
+
2. **Resource owner**: `phamdung2209`. **Repository access**: *Only select repositories* β `Mixly`.
|
| 44 |
+
3. **Permissions β Repository β Contents: Read-only** (that's all it needs).
|
| 45 |
+
4. Expiration: 90 days (rotate on expiry). Generate β copy the `github_pat_β¦`.
|
| 46 |
+
|
| 47 |
+
Then add it to the Space as a **build secret**:
|
| 48 |
+
- Space β **Settings β Variables and secrets β New secret**
|
| 49 |
+
- Name: `GITHUB_TOKEN` Β· Value: the `github_pat_β¦`
|
| 50 |
+
|
| 51 |
+
> The Dockerfile reads it via `--mount=type=secret,id=GITHUB_TOKEN`, so it is
|
| 52 |
+
> **never baked into an image layer**.
|
| 53 |
+
|
| 54 |
+
---
|
| 55 |
+
|
| 56 |
+
## 2. Space secrets (runtime) β Settings β Variables and secrets
|
| 57 |
+
|
| 58 |
+
π Paste values in the HF UI, never into code/git. **Secrets** (hidden) vs
|
| 59 |
+
**Variables** (visible) β put all credentials as *Secrets*.
|
| 60 |
+
|
| 61 |
+
### Required
|
| 62 |
+
| Name | Value / where from |
|
| 63 |
+
|---|---|
|
| 64 |
+
| `GITHUB_TOKEN` | build secret from Β§1 |
|
| 65 |
+
| `SHOPIFY_API_KEY` | Partner Dashboard β Mixly β API credentials |
|
| 66 |
+
| `SHOPIFY_API_SECRET` | β³ (secret) |
|
| 67 |
+
| `NEXT_PUBLIC_SHOPIFY_API_KEY` | **same value as** `SHOPIFY_API_KEY` |
|
| 68 |
+
| `SHOPIFY_SCOPES` | `read_orders,read_products,write_discounts` (match `shopify.app.toml`) |
|
| 69 |
+
| `SHOPIFY_APP_URL` | `https://solora-hq-mixly.hf.space` |
|
| 70 |
+
| `SHOPIFY_API_VERSION` | `2026-07` |
|
| 71 |
+
| `DATABASE_URL` | `file:/data/mixly.db` (persistent mount β see Β§3) |
|
| 72 |
+
| `SESSION_ENCRYPTION_KEY` | `openssl rand -hex 32`. **Set once, never change** (rotating it forces every store to re-auth). |
|
| 73 |
+
|
| 74 |
+
### Ops Console + DB access (`/ops`, `/ops/studio`)
|
| 75 |
+
| Name | Value |
|
| 76 |
+
|---|---|
|
| 77 |
+
| `OPS_ALLOWLIST` | comma-separated emails allowed into `/ops` |
|
| 78 |
+
| `AUTH_SECRET` | `npx auth secret`. Keep stable (else your `/ops` sessions drop). |
|
| 79 |
+
| `AUTH_GOOGLE_ID` | Google Cloud Console β OAuth client ID |
|
| 80 |
+
| `AUTH_GOOGLE_SECRET` | β³ (redirect URI: `https://solora-hq-mixly.hf.space/api/auth/callback/google`) |
|
| 81 |
+
|
| 82 |
+
### DB backup (Β§4) β HF Storage Bucket via S3 API
|
| 83 |
+
| Name | Value |
|
| 84 |
+
|---|---|
|
| 85 |
+
| `S3_ENDPOINT` | `https://s3.hf.co/solora-hq` (gateway scoped to the namespace) |
|
| 86 |
+
| `S3_BUCKET` | `Mixly-storage` (bare bucket name β **not** `Tierly-storage`) |
|
| 87 |
+
| `S3_ACCESS_KEY_ID` | `HFAKβ¦` β HF **Access Tokens** β token (Write) β dropdown β **Generate S3 credentials** |
|
| 88 |
+
| `S3_SECRET_ACCESS_KEY` | β³ (shown once) |
|
| 89 |
+
| `HF_TOKEN` | HF bearer token β used to rebuild/restart the Space from `/ops/backup` after a DB restore (Β§4), and by `scripts/sync-env.mjs` (Β§5), NOT the S3 creds |
|
| 90 |
+
|
| 91 |
+
### Optional (features degrade gracefully when unset)
|
| 92 |
+
`SENTRY_DSN`, `HF_SPACE_ID` (defaults to `solora-hq/Mixly` in code β only set if
|
| 93 |
+
the Space is ever renamed), `GITHUB_REPO` (overrides the clone source; the
|
| 94 |
+
Dockerfile's `ARG GITHUB_REPO` build arg already defaults to the right repo).
|
| 95 |
+
|
| 96 |
+
Mixly has **no** Crisp chat, QStash, or email (Resend) integration β unlike
|
| 97 |
+
Tierly, none of `NEXT_PUBLIC_CRISP_WEBSITE_ID`, `QSTASH_*`, `RESEND_API_KEY`,
|
| 98 |
+
`EMAIL_FROM`, or `CRON_SECRET` apply here; don't set them.
|
| 99 |
+
|
| 100 |
+
---
|
| 101 |
+
|
| 102 |
+
## 3. Persistent storage
|
| 103 |
+
|
| 104 |
+
Space β **Settings β Persistent storage** β enable (Small is plenty). It mounts
|
| 105 |
+
at `/data`, which is where `DATABASE_URL=file:/data/mixly.db` points. Without it,
|
| 106 |
+
`/data` resets on every rebuild β which is exactly why Β§4 exists.
|
| 107 |
+
|
| 108 |
+
> Do **not** mount the Storage Bucket as a data volume. Backup talks to it over the
|
| 109 |
+
> S3 API (Β§4); a bucket *mount* is a schedule-time dependency and, if HF can't attach
|
| 110 |
+
> it, the Space fails with "Scheduling failure: unable to schedule".
|
| 111 |
+
|
| 112 |
+
---
|
| 113 |
+
|
| 114 |
+
## 4. Database backup / restore (HF Storage Bucket, S3 API)
|
| 115 |
+
|
| 116 |
+
The app backs the SQLite DB up to a **Storage Bucket over the S3 API** and restores
|
| 117 |
+
it on boot if `/data` is empty β so data survives a rebuild even on the free tier.
|
| 118 |
+
Network-only (no mount), so it never blocks Space scheduling.
|
| 119 |
+
|
| 120 |
+
- **What's backed up:** only the SQLite DB file (`/data/mixly.db`). It holds
|
| 121 |
+
everything stateful: Shopify sessions (tokens encrypted at rest), bundle offers,
|
| 122 |
+
bundle configs, shop profiles, settings. Migrations live in git; nothing else
|
| 123 |
+
needs backing up.
|
| 124 |
+
- **The one thing that is NOT in the bucket:** `SESSION_ENCRYPTION_KEY`. It lives
|
| 125 |
+
only in Space secrets. Keep it β a restored DB with a lost/rotated key can't
|
| 126 |
+
decrypt stored Shopify tokens (the app then just re-exchanges them, no data loss,
|
| 127 |
+
but avoid churn).
|
| 128 |
+
- **Bucket:** `solora-hq/Mixly-storage`. Objects: `latest.db` (overwritten in place β
|
| 129 |
+
no history growth) + `snapshots/mixly-YYYY-MM-DD.db` (daily, pruned after 30 days).
|
| 130 |
+
- **Schedule:** in-process β an initial backup ~1 min after boot, then hourly, plus a
|
| 131 |
+
debounced write after each DB change and a final flush on SIGTERM (see
|
| 132 |
+
`src/lib/backup.ts` + `src/lib/s3.ts`, started from `src/instrumentation.ts`).
|
| 133 |
+
- **Operator-driven backup/restore:** unlike a plain snapshot loop, Mixly ships an
|
| 134 |
+
`/ops/backup` panel (Google-authenticated, see Β§2) that can trigger an immediate
|
| 135 |
+
backup, upload a snapshot to restore from, and inspect it (table/row checks)
|
| 136 |
+
before committing. Restoring requires typing `overwrite` to confirm, snapshots
|
| 137 |
+
the *current* DB as a `pre-restore-β¦db` rollback key first, then calls
|
| 138 |
+
`restartSpace()` (needs `HF_TOKEN`) to restart the Space onto the restored file.
|
| 139 |
+
|
| 140 |
+
---
|
| 141 |
+
|
| 142 |
+
## 5. Deploy / rebuild
|
| 143 |
+
|
| 144 |
+
- **Redeploy the web app:** push to GitHub `main`, then in the Space use
|
| 145 |
+
**Settings β Factory rebuild** (re-clones latest `main`).
|
| 146 |
+
- **Push local env vars as Space secrets:** `HF_TOKEN=β¦ node scripts/sync-env.mjs
|
| 147 |
+
[.env.production.local] [solora-hq/Mixly]` (both args optional, those are the
|
| 148 |
+
defaults) β reads `KEY=value` lines and pushes each as a secret via the HF API.
|
| 149 |
+
Changes apply on the next rebuild/restart.
|
| 150 |
+
- **Deploy the Shopify Function / extensions:** that is a *separate* target, run from
|
| 151 |
+
your machine (needs the Shopify CLI + Partner auth) β `npm run app:deploy`. The
|
| 152 |
+
container also runs this non-fatally during a Space build (see Dockerfile), gated
|
| 153 |
+
on the `SHOPIFY_CLI_PARTNERS_TOKEN` build secret.
|
| 154 |
+
|
| 155 |
+
> After changing scopes or the app URL, re-open the app in the store to re-consent.
|
Dockerfile
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# syntax=docker/dockerfile:1
|
| 2 |
+
#
|
| 3 |
+
# Hugging Face Space (Docker SDK) for Mixly.
|
| 4 |
+
#
|
| 5 |
+
# The app source lives in the PRIVATE GitHub repo, not in this Space repo β this
|
| 6 |
+
# Dockerfile clones it at build time using the GITHUB_TOKEN *build secret*, builds
|
| 7 |
+
# the Next.js app, then runs it. Keeping the Space tiny means a rebuild always
|
| 8 |
+
# pulls the latest `main` from GitHub.
|
| 9 |
+
#
|
| 10 |
+
# The SQLite DB lives on the /data mount. On boot it is restored from the HF Storage
|
| 11 |
+
# Bucket (S3 API) if /data is empty, so data survives even without paid Persistent
|
| 12 |
+
# Storage (see scripts/restore-db.mjs).
|
| 13 |
+
|
| 14 |
+
# ---- builder ----
|
| 15 |
+
FROM node:24-slim AS builder
|
| 16 |
+
WORKDIR /app
|
| 17 |
+
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \
|
| 18 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 19 |
+
|
| 20 |
+
# Which repo/branch to deploy. Override GITHUB_REF (Space variable) to pin a release.
|
| 21 |
+
ARG GITHUB_REPO=github.com/phamdung2209/Mixly.git
|
| 22 |
+
ARG GITHUB_REF=main
|
| 23 |
+
|
| 24 |
+
# Clone with the token mounted as a build secret β it is never written into an
|
| 25 |
+
# image layer. Add GITHUB_TOKEN in Space β Settings β Secrets.
|
| 26 |
+
RUN --mount=type=secret,id=GITHUB_TOKEN,required=true \
|
| 27 |
+
git clone --depth 1 --branch "${GITHUB_REF}" \
|
| 28 |
+
"https://x-access-token:$(cat /run/secrets/GITHUB_TOKEN)@${GITHUB_REPO}" .
|
| 29 |
+
|
| 30 |
+
ENV NEXT_TELEMETRY_DISABLED=1
|
| 31 |
+
# Placeholder values ONLY so env validation passes during `next build` (some server
|
| 32 |
+
# modules read env at import). The app reads the REAL values from HF secrets at
|
| 33 |
+
# runtime; none of these are NEXT_PUBLIC, so nothing is baked into the output.
|
| 34 |
+
ENV SHOPIFY_API_KEY=build SHOPIFY_API_SECRET=build SHOPIFY_SCOPES=read_products \
|
| 35 |
+
SHOPIFY_APP_URL=https://build.invalid SHOPIFY_API_VERSION=2026-07 DATABASE_URL=file:/tmp/build.db
|
| 36 |
+
# NEXT_PUBLIC_* is inlined into the client bundle at BUILD time, so it must exist
|
| 37 |
+
# during `npm run build` β a runtime secret is too late. The Shopify API key is
|
| 38 |
+
# public (it ships in the browser), so mounting the secret just for the build is safe.
|
| 39 |
+
RUN --mount=type=secret,id=NEXT_PUBLIC_SHOPIFY_API_KEY \
|
| 40 |
+
export NEXT_PUBLIC_SHOPIFY_API_KEY="$(cat /run/secrets/NEXT_PUBLIC_SHOPIFY_API_KEY 2>/dev/null)" && \
|
| 41 |
+
npm ci && npx prisma generate && npm run build && mkdir -p public
|
| 42 |
+
|
| 43 |
+
# Deploy the Shopify Function + app config as part of the Space build, so ONE rebuild
|
| 44 |
+
# ships the web app AND the extensions together. Only runs on a real rebuild (not on
|
| 45 |
+
# restart/wake), so no version churn. NON-FATAL: a Shopify hiccup never breaks the web
|
| 46 |
+
# deploy. Needs the SHOPIFY_CLI_PARTNERS_TOKEN Space secret (else it just skips).
|
| 47 |
+
RUN --mount=type=secret,id=SHOPIFY_CLI_PARTNERS_TOKEN \
|
| 48 |
+
if [ -s /run/secrets/SHOPIFY_CLI_PARTNERS_TOKEN ]; then \
|
| 49 |
+
export SHOPIFY_CLI_PARTNERS_TOKEN="$(cat /run/secrets/SHOPIFY_CLI_PARTNERS_TOKEN)" CI=true; \
|
| 50 |
+
for d in extensions/*/; do [ -f "${d}package.json" ] && (cd "$d" && npm install --no-audit --no-fund || true); done; \
|
| 51 |
+
npx shopify app deploy --allow-updates || echo "WARN: shopify app deploy failed (non-fatal)"; \
|
| 52 |
+
else echo "SHOPIFY_CLI_PARTNERS_TOKEN not set β skipping Shopify deploy"; fi
|
| 53 |
+
|
| 54 |
+
# ---- runner ----
|
| 55 |
+
FROM node:24-slim AS runner
|
| 56 |
+
WORKDIR /app
|
| 57 |
+
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 PORT=3000 HOSTNAME=0.0.0.0
|
| 58 |
+
|
| 59 |
+
# tini as PID 1: forwards SIGTERM to the app and reaps zombies. Without it (e.g.
|
| 60 |
+
# `npm start` as PID 1) npm swallows SIGTERM and never forwards it to the Node
|
| 61 |
+
# server, so the shutdown DB backup (src/lib/backup.ts) never runs on a rebuild.
|
| 62 |
+
RUN apt-get update && apt-get install -y --no-install-recommends tini \
|
| 63 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 64 |
+
|
| 65 |
+
# node:24-slim already ships a `node` user at UID 1000 (which HF mounts /data for) β
|
| 66 |
+
# reuse it; creating a second UID-1000 user fails with "UID 1000 is not unique".
|
| 67 |
+
COPY --from=builder --chown=node /app/node_modules ./node_modules
|
| 68 |
+
COPY --from=builder --chown=node /app/.next ./.next
|
| 69 |
+
COPY --from=builder --chown=node /app/public ./public
|
| 70 |
+
COPY --from=builder --chown=node /app/package.json ./package.json
|
| 71 |
+
COPY --from=builder --chown=node /app/next.config.ts ./next.config.ts
|
| 72 |
+
COPY --from=builder --chown=node /app/prisma ./prisma
|
| 73 |
+
COPY --from=builder --chown=node /app/prisma.config.ts ./prisma.config.ts
|
| 74 |
+
COPY --from=builder --chown=node /app/scripts ./scripts
|
| 75 |
+
|
| 76 |
+
# Make /data writable with OR without Persistent Storage. If enabled, HF mounts
|
| 77 |
+
# /data (uid 1000) over this. If not, this ephemeral dir is used and data still
|
| 78 |
+
# survives rebuilds via the S3 bucket restore on boot (scripts/restore-db.mjs).
|
| 79 |
+
RUN mkdir -p /data && chown node:node /data
|
| 80 |
+
USER node
|
| 81 |
+
|
| 82 |
+
EXPOSE 3000
|
| 83 |
+
# 1) restore the DB from the HF Storage Bucket (S3) if /data is empty, 2) apply migrations,
|
| 84 |
+
# 3) start. Restore MUST run before migrate β migrate would otherwise create an
|
| 85 |
+
# empty DB and the restore would think data already exists.
|
| 86 |
+
# `exec next` (NOT `npm start`) so the Node server itself becomes the process that
|
| 87 |
+
# receives SIGTERM on rebuild/sleep β that's what triggers the final DB backup
|
| 88 |
+
# (src/lib/backup.ts). npm would fork the server as a child and swallow the signal.
|
| 89 |
+
# tini (ENTRYPOINT) forwards the signal and reaps zombies.
|
| 90 |
+
ENTRYPOINT ["tini", "--"]
|
| 91 |
+
# Prisma Studio runs in the background on port 5555. NOTE: prisma studio v7 binds
|
| 92 |
+
# 0.0.0.0 (no hostname flag) β it is NOT localhost-only. It stays private because HF
|
| 93 |
+
# routes ONLY port 3000 as ingress and network-isolates the container; the DB is
|
| 94 |
+
# reached via the operator + same-origin gated route handlers (src/lib/ops/studio-proxy)
|
| 95 |
+
# at /ops/studio. Do NOT reuse this where 5555 is published (e.g. docker -p 5555:5555).
|
| 96 |
+
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"]
|
README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Mixly
|
| 3 |
+
emoji: π¦
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 3000
|
| 8 |
+
pinned: false
|
| 9 |
+
license: mit
|
| 10 |
+
short_description: Product bundles that discount what's already in the cart. No new SKUs, no inventory drift.
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# Mixly β Hugging Face Space
|
| 14 |
+
|
| 15 |
+
Docker host for the [Mixly](https://github.com/phamdung2209/Mixly) Shopify app.
|
| 16 |
+
This Space repo holds only the `Dockerfile`; it clones the app source from the
|
| 17 |
+
private GitHub repo at build time.
|
| 18 |
+
|
| 19 |
+
**Deploy / operations guide β [`DEPLOY.md`](./DEPLOY.md)** (GitHub token, secrets,
|
| 20 |
+
persistent storage, DB backup/restore, rebuilds).
|
| 21 |
+
|
| 22 |
+
Config reference: https://huggingface.co/docs/hub/spaces-config-reference
|