MolinoPro

instantiate-cloud-deploy-script

Master Codebase Guidebook
Markdown + HTML Dev-Docs Renderer - Frontend Client Module

Default Index
Open README.md
Root: README.mdinstantiate-copy-app
Milestones
H1cloud-run-deployment.skills.md

version: 2026.1 status: active scope: deployment / nextjs / prisma / cloud-run / cloud-build / apps-script-trigger

H2Overview

Production deployment system for Molino Index on Google Cloud Run. Goal:

  • deterministic deploys
  • zero-downtime revisions
  • aligned with Molino authority model
Code → Build → Image → Deploy → Revision → Live URL

⸻

When to Use

* deploying Next.js app (standalone)
* enabling auto CI/CD
* triggering deploys from external systems (Apps Script)
* managing Prisma migrations safely

⸻

Core Flow

git push
→ Cloud Build trigger
→ Docker build
→ Artifact Registry
→ Cloud Run deploy (new revision)
→ optional migration job
→ revalidate + live

⸻

Implementation

1. next.config.js

module.exports = {
  output: "standalone",
};

⸻

2. Dockerfile (production-safe)

# =========================
# Base
# =========================
FROM node:20-alpine AS base
WORKDIR /app
RUN apk add --no-cache libc6-compat
# =========================
# Dependencies
# =========================
FROM base AS deps
COPY package.json package-lock.json* ./
RUN npm ci
# =========================
# Build
# =========================
FROM base AS builder
WORKDIR /app
ARG DATABASE_URL="prisma+postgres://accelerate.prisma-data.net/?api_key=dummy"
ARG DIRECT_URL="postgresql://user:password@localhost:5432/db"
ENV DATABASE_URL=$DATABASE_URL
ENV DIRECT_URL=$DIRECT_URL
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npx prisma generate
RUN npm run build
# =========================
# Runtime
# =========================
FROM node:20-alpine AS runner
WORKDIR /app
RUN apk add --no-cache libc6-compat
ENV NODE_ENV=production
ENV PORT=3000
ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
COPY --from=builder /app/prisma ./prisma
EXPOSE 3000
CMD ["node", "server.js"]

⸻

3. cloudbuild.yaml (CI/CD trigger)

steps:
  # Build image
  - name: 'gcr.io/cloud-builders/docker'
    args: [
      'build',
      '-t',
      'europe-west1-docker.pkg.dev/$PROJECT_ID/coop-repo/molino:$COMMIT_SHA',
      '.'
    ]
  # Push image
  - name: 'gcr.io/cloud-builders/docker'
    args: [
      'push',
      'europe-west1-docker.pkg.dev/$PROJECT_ID/coop-repo/molino:$COMMIT_SHA'
    ]
  # Deploy to Cloud Run
  - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
    entrypoint: gcloud
    args:
      [
        'run','deploy','molino',
        '--image=europe-west1-docker.pkg.dev/$PROJECT_ID/coop-repo/molino:$COMMIT_SHA',
        '--region=europe-west1',
        '--platform=managed',
        '--allow-unauthenticated'
      ]
images:
  - 'europe-west1-docker.pkg.dev/$PROJECT_ID/coop-repo/molino:$COMMIT_SHA'

⸻

4. Apps Script → Trigger Deploy

function triggerCloudBuild() {
  const projectId = "i-granada";
  const triggerId = "YOUR_TRIGGER_ID"; // from Cloud Build
  const url = `https://cloudbuild.googleapis.com/v1/projects/${projectId}/triggers/${triggerId}:run`;
  const token = ScriptApp.getOAuthToken();
  const res = UrlFetchApp.fetch(url, {
    method: "post",
    headers: {
      Authorization: "Bearer " + token,
      "Content-Type": "application/json"
    },
    payload: JSON.stringify({
      branchName: "main"
    }),
    muteHttpExceptions: true
  });
  Logger.log(res.getContentText());
}

⸻

Migration Strategy (Prisma)

App deploy
→ separate Cloud Run Job
→ prisma migrate deploy

Rules:

* NEVER run migrations in request lifecycle
* ALWAYS isolate in job
* ALWAYS pass DATABASE_URL (+ DIRECT_URL if needed)

⸻

Domain Setup

Option A — Direct Mapping (fast)

gcloud beta run domain-mappings create \
  --service molino \
  --domain yourdomain.com \
  --region europe-west1

Option B — Load Balancer (recommended later)

Internet
→ Load Balancer
→ SSL
→ Serverless NEG
→ Cloud Run

⸻

Rules

* Cloud Run = deployment authority
* Docker image = immutable artifact
* Cloud Build = pipeline authority
* Apps Script = trigger only (no logic)

Aligned with Molino:

UI → intent
Actions → mutation
Cloud Build → deployment action
Cloud Run → projection surface

⸻

Next Steps

1. Add cloudbuild.yaml to repo
2. Create Cloud Build trigger (main branch)
3. Store secrets in Cloud Run env vars
4. Create migration job (once)
5. Add Apps Script trigger (optional)
6. Map domain

⸻

Verification

* docker build works locally
* image pushed to Artifact Registry
* Cloud Run service updates revision
* app loads correctly
* Prisma migrations executed
* domain resolves with SSL
* Apps Script trigger fires build

⸻

Integration Notes

* Fits Molino execution model:  
* Section/UI layer remains unaffected:  
* Deployment is external projection layer (non-authoritative)

⸻

TLDR

Next standalone → Docker → Cloud Build → Cloud Run
CI/CD = Cloud Build trigger
Manual = deploy script
External trigger = Apps Script
Migrations = separate job
Domain = mapping or load balancer
---
### Notes / Improvements
- Removed redundant local deploy scripts → replaced with Cloud Build canonical pipeline
- Prisma handled via job → avoids cold start / race conditions
- Apps Script aligned to “execution only” (no orchestration leakage)
- Fully compliant with Molino authority model (deployment treated as projection layer)


Great questions — and yes, GitHub being in the picture actually **completes the CI/CD loop** nicely. Let me address all of this clearly.

---

## GitHub Unlocks the Full Automatic Pipeline

Having your code on GitHub means your Cloud Build trigger can watch your repo directly. The complete flow becomes:

git push to main → Cloud Build trigger fires automatically → builds Docker image → pushes to Artifact Registry → deploys new revision to Cloud Run → your live URL updates with zero downtime


You never have to touch the GCP console for day-to-day deploys. Just `git push` and it's done. This is the equivalent of what people use Vercel for, but entirely within your existing GCP stack.

---

## Does Next.js Have Time Triggers Like Apps Script?

This is a really important architectural question. The honest answer is: **Next.js itself has no built-in scheduling** — it's a web framework, not a runtime orchestrator. But the *way you think about it* matters:

**Apps Script's mental model:**

Time trigger → runs a function → calls an API


**Next.js's equivalent mental model:**

External scheduler → hits an API route → your function runs


The difference is that in Next.js, the *scheduler lives outside the app* and just calls an HTTP endpoint. Your Next.js app exposes the logic as a route, and something external pings it on schedule. On GCP, that "something external" is **Cloud Scheduler** — and it's actually a direct equivalent to Apps Script's time triggers, just more powerful.

---

## The GCP Equivalent: Cloud Scheduler → Cloud Run

You can use Cloud Scheduler to securely trigger a Cloud Run service on a schedule — similar to using cron jobs. You implement the job logic in your service, specify the cron frequency, and Cloud Scheduler calls your service's URL at that interval.

Cloud Scheduler offers reliable, at-least-once delivery of a job to the target, fault-tolerant execution with automatic retries, and supports the familiar Unix cron format — all in a fully managed serverless fashion with no infrastructure to provision.

So the pattern in your Next.js app is:

```ts
// app/api/scheduled/daily-cleanup/route.ts
export async function POST(req: Request) {
  // Verify this came from Cloud Scheduler (check auth header)
  
  // Your business logic here
  await cleanupOldRecords();
  await sendDigestEmails();
  
  return Response.json({ ok: true });
}

Then in GCP, you create a Cloud Scheduler job pointing to https://your-app.run.app/api/scheduled/daily-cleanup with a cron expression like 0 9 * * * (9am daily). That's it. No library needed inside Next.js.


Where Does Scheduling Logic Live in a Next.js Project?

Your instinct about lib/ and actions/ is right — here's the convention:

/app
  /api
    /scheduled
      /daily-report/route.ts   ← HTTP entry point (thin, just calls lib)
      /weekly-sync/route.ts
  /actions
    emailActions.ts             ← Server Actions (user-triggered, not scheduled)
/lib
  /jobs
    dailyReport.ts              ← actual business logic
    weeklySync.ts
  /scheduler
    index.ts                    ← if using an in-process scheduler (node-cron etc.)

The key principle: keep logic in lib/jobs/, keep the route thin. The route just authenticates the request and calls the library function. This way the same logic can be called from a route, a test, or any other trigger.


Your Four Options for Scheduling, Ranked for Your Setup

1. Cloud Scheduler (recommended — you already have it)

Cloud Scheduler supports any crontab-compatible string. For example, 0 */3 * * * runs the job every 3 hours. You specify the fully qualified URL of your Cloud Run service, the HTTP method, and optionally a request body.

Free tier covers 3 jobs. Perfect for most use cases.

2. Apps Script calling your Next.js API (you already know this!)

Since you're comfortable with Apps Script, you can absolutely keep using it as the scheduler and just have it call your Next.js API routes via UrlFetchApp. This is perfectly valid and means you get the Apps Script UI for managing triggers. The downside is a dependency on Google Workspace.

3. GitHub Actions on a schedule

H1.github/workflows/daily-job.yml

on: schedule: - cron: '0 9 * * *' # 9am daily UTC jobs: run: runs-on: ubuntu-latest steps: - run: curl -X POST https://your-app.run.app/api/scheduled/daily-report


Free, version-controlled alongside your code. Good for things tied to your repo.

**4. In-process node-cron (only for non-serverless)**

Node-Cron uses GNU crontab syntax and is lightweight and ideal for running tasks at regular intervals. However, since Cloud Run scales to zero between requests, an in-process scheduler won't reliably fire unless your container is already running. Better to keep the scheduler external and the logic internal.

---

## The Mental Model Comparison

| Apps Script | Your GCP/Next.js Equivalent |
|---|---|
| Time trigger | Cloud Scheduler job |
| `function myJob() {}` | `/app/api/scheduled/my-job/route.ts` |
| Business logic inline | `lib/jobs/myJob.ts` |
| `UrlFetchApp.fetch()` | `fetch()` in Server Action or route |
| Apps Script dashboard | GCP Cloud Scheduler console |
| Manual trigger button | Cloud Scheduler "Force run" or direct URL call |

You can also keep Apps Script for orchestration and have it call your Next.js endpoints — that's actually a nice hybrid since you get Apps Script's excellent Google Sheets/Docs integration as the trigger context, with your Next.js app doing the heavy lifting.

Would you like help writing a specific scheduled job end-to-end — e.g. the Next.js route, the `lib/jobs/` function, and the Cloud Scheduler setup command?