From 7f1ff7fd86cf37b693fe734cbff305c06b4f62e0 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 25 Oct 2025 09:59:57 -1000 Subject: [PATCH 1/2] fix(billing): should allow restoring subscription (#1728) * fix(already-cancelled-sub): UI should allow restoring subscription * restore functionality fixed * fix --- apps/sim/app/api/billing/portal/route.ts | 7 +- .../cancel-subscription.tsx | 106 +++++++++--------- .../components/subscription/subscription.tsx | 1 + apps/sim/lib/billing/core/billing.ts | 12 ++ apps/sim/stores/subscription/types.ts | 1 + 5 files changed, 70 insertions(+), 57 deletions(-) diff --git a/apps/sim/app/api/billing/portal/route.ts b/apps/sim/app/api/billing/portal/route.ts index 017fbb8bd7b..959a83cd7f4 100644 --- a/apps/sim/app/api/billing/portal/route.ts +++ b/apps/sim/app/api/billing/portal/route.ts @@ -1,6 +1,6 @@ import { db } from '@sim/db' import { subscription as subscriptionTable, user } from '@sim/db/schema' -import { and, eq } from 'drizzle-orm' +import { and, eq, or } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { requireStripeClient } from '@/lib/billing/stripe-client' @@ -38,7 +38,10 @@ export async function POST(request: NextRequest) { .where( and( eq(subscriptionTable.referenceId, organizationId), - eq(subscriptionTable.status, 'active') + or( + eq(subscriptionTable.status, 'active'), + eq(subscriptionTable.cancelAtPeriodEnd, true) + ) ) ) .limit(1) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/cancel-subscription/cancel-subscription.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/cancel-subscription/cancel-subscription.tsx index 1f5ea569aaf..fd81cec55ec 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/cancel-subscription/cancel-subscription.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/cancel-subscription/cancel-subscription.tsx @@ -12,7 +12,6 @@ import { AlertDialogTitle, } from '@/components/ui/alert-dialog' import { Button } from '@/components/ui/button' -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { useSession, useSubscription } from '@/lib/auth-client' import { createLogger } from '@/lib/logs/console/logger' import { getBaseUrl } from '@/lib/urls/utils' @@ -30,6 +29,7 @@ interface CancelSubscriptionProps { } subscriptionData?: { periodEnd?: Date | null + cancelAtPeriodEnd?: boolean } } @@ -127,35 +127,48 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub const subscriptionStatus = getSubscriptionStatus() const activeOrgId = activeOrganization?.id - // For team/enterprise plans, get the subscription ID from organization store - if ((subscriptionStatus.isTeam || subscriptionStatus.isEnterprise) && activeOrgId) { - const orgSubscription = useOrganizationStore.getState().subscriptionData + if (isCancelAtPeriodEnd) { + if (!betterAuthSubscription.restore) { + throw new Error('Subscription restore not available') + } + + let referenceId: string + let subscriptionId: string | undefined + + if ((subscriptionStatus.isTeam || subscriptionStatus.isEnterprise) && activeOrgId) { + const orgSubscription = useOrganizationStore.getState().subscriptionData + referenceId = activeOrgId + subscriptionId = orgSubscription?.id + } else { + // For personal subscriptions, use user ID and let better-auth find the subscription + referenceId = session.user.id + subscriptionId = undefined + } + + logger.info('Restoring subscription', { referenceId, subscriptionId }) - if (orgSubscription?.id && orgSubscription?.cancelAtPeriodEnd) { - // Restore the organization subscription - if (!betterAuthSubscription.restore) { - throw new Error('Subscription restore not available') - } - - const result = await betterAuthSubscription.restore({ - referenceId: activeOrgId, - subscriptionId: orgSubscription.id, - }) - logger.info('Organization subscription restored successfully', result) + // Build restore params - only include subscriptionId if we have one (team/enterprise) + const restoreParams: any = { referenceId } + if (subscriptionId) { + restoreParams.subscriptionId = subscriptionId } + + const result = await betterAuthSubscription.restore(restoreParams) + + logger.info('Subscription restored successfully', result) } - // Refresh state and close await refresh() if (activeOrgId) { await loadOrganizationSubscription(activeOrgId) await refreshOrganization().catch(() => {}) } + setIsDialogOpen(false) } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Failed to keep subscription' + const errorMessage = error instanceof Error ? error.message : 'Failed to restore subscription' setError(errorMessage) - logger.error('Failed to keep subscription', { error }) + logger.error('Failed to restore subscription', { error }) } finally { setIsLoading(false) } @@ -190,19 +203,15 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub const periodEndDate = getPeriodEndDate() // Check if subscription is set to cancel at period end - const isCancelAtPeriodEnd = (() => { - const subscriptionStatus = getSubscriptionStatus() - if (subscriptionStatus.isTeam || subscriptionStatus.isEnterprise) { - return useOrganizationStore.getState().subscriptionData?.cancelAtPeriodEnd === true - } - return false - })() + const isCancelAtPeriodEnd = subscriptionData?.cancelAtPeriodEnd === true return ( <>
- Manage Subscription + + {isCancelAtPeriodEnd ? 'Restore Subscription' : 'Manage Subscription'} + {isCancelAtPeriodEnd && (

You'll keep access until {formatDate(periodEndDate)} @@ -217,10 +226,12 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub 'h-8 rounded-[8px] font-medium text-xs transition-all duration-200', error ? 'border-red-500 text-red-500 dark:border-red-500 dark:text-red-500' - : 'text-muted-foreground hover:border-red-500 hover:bg-red-500 hover:text-white dark:hover:border-red-500 dark:hover:bg-red-500' + : isCancelAtPeriodEnd + ? 'text-muted-foreground hover:border-green-500 hover:bg-green-500 hover:text-white dark:hover:border-green-500 dark:hover:bg-green-500' + : 'text-muted-foreground hover:border-red-500 hover:bg-red-500 hover:text-white dark:hover:border-red-500 dark:hover:bg-red-500' )} > - {error ? 'Error' : 'Manage'} + {error ? 'Error' : isCancelAtPeriodEnd ? 'Restore' : 'Manage'}

@@ -228,11 +239,11 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub - {isCancelAtPeriodEnd ? 'Manage' : 'Cancel'} {subscription.plan} subscription? + {isCancelAtPeriodEnd ? 'Restore' : 'Cancel'} {subscription.plan} subscription? {isCancelAtPeriodEnd - ? 'Your subscription is set to cancel at the end of the billing period. You can reactivate it or manage other settings.' + ? 'Your subscription is set to cancel at the end of the billing period. Would you like to keep your subscription active?' : `You'll be redirected to Stripe to manage your subscription. You'll keep access until ${formatDate( periodEndDate )}, then downgrade to free plan.`}{' '} @@ -260,38 +271,23 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub setIsDialogOpen(false) : handleKeep} disabled={isLoading} > - Keep Subscription + {isCancelAtPeriodEnd ? 'Cancel' : 'Keep Subscription'} {(() => { const subscriptionStatus = getSubscriptionStatus() - if ( - subscriptionStatus.isPaid && - (activeOrganization?.id - ? useOrganizationStore.getState().subscriptionData?.cancelAtPeriodEnd - : false) - ) { + if (subscriptionStatus.isPaid && isCancelAtPeriodEnd) { return ( - - - -
- - Continue - -
-
- -

Subscription will be cancelled at end of billing period

-
-
-
+ + {isLoading ? 'Restoring...' : 'Restore Subscription'} + ) } return ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx index 9ac78581ef8..b69b499aff9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx @@ -523,6 +523,7 @@ export function Subscription({ onOpenChange }: SubscriptionProps) { }} subscriptionData={{ periodEnd: subscriptionData?.periodEnd || null, + cancelAtPeriodEnd: subscriptionData?.cancelAtPeriodEnd, }} />
diff --git a/apps/sim/lib/billing/core/billing.ts b/apps/sim/lib/billing/core/billing.ts index 48085eb8212..55b6a207f75 100644 --- a/apps/sim/lib/billing/core/billing.ts +++ b/apps/sim/lib/billing/core/billing.ts @@ -220,6 +220,7 @@ export async function getSimplifiedBillingSummary( metadata: any stripeSubscriptionId: string | null periodEnd: Date | string | null + cancelAtPeriodEnd?: boolean // Usage details usage: { current: number @@ -318,6 +319,7 @@ export async function getSimplifiedBillingSummary( metadata: subscription.metadata || null, stripeSubscriptionId: subscription.stripeSubscriptionId || null, periodEnd: subscription.periodEnd || null, + cancelAtPeriodEnd: subscription.cancelAtPeriodEnd || undefined, // Usage details usage: { current: usageData.currentUsage, @@ -393,6 +395,7 @@ export async function getSimplifiedBillingSummary( metadata: subscription?.metadata || null, stripeSubscriptionId: subscription?.stripeSubscriptionId || null, periodEnd: subscription?.periodEnd || null, + cancelAtPeriodEnd: subscription?.cancelAtPeriodEnd || undefined, // Usage details usage: { current: currentUsage, @@ -450,5 +453,14 @@ function getDefaultBillingSummary(type: 'individual' | 'organization') { lastPeriodCost: 0, daysRemaining: 0, }, + ...(type === 'organization' && { + organizationData: { + seatCount: 0, + memberCount: 0, + totalBasePrice: 0, + totalCurrentUsage: 0, + totalOverage: 0, + }, + }), } } diff --git a/apps/sim/stores/subscription/types.ts b/apps/sim/stores/subscription/types.ts index c0de147d45d..643694b7959 100644 --- a/apps/sim/stores/subscription/types.ts +++ b/apps/sim/stores/subscription/types.ts @@ -29,6 +29,7 @@ export interface SubscriptionData { metadata: any | null stripeSubscriptionId: string | null periodEnd: Date | null + cancelAtPeriodEnd?: boolean usage: UsageData billingBlocked?: boolean } From e190c85b07273a4419766ffa4f041d9b58955a06 Mon Sep 17 00:00:00 2001 From: Connor Mulholland Date: Wed, 19 Nov 2025 14:25:44 -0500 Subject: [PATCH 2/2] added blacksmith optimizations to workflows and dockerfiles to enhance performance. please review before pushing to production --- .github/workflows/ci.yml | 6 +++++ .github/workflows/docs-embeddings.yml | 13 ++++++++- .github/workflows/i18n.yml | 24 ++++++++++++++++- .github/workflows/images.yml | 6 +++++ .github/workflows/migrations.yml | 13 ++++++++- .github/workflows/publish-cli.yml | 13 ++++++++- .github/workflows/publish-ts-sdk.yml | 13 ++++++++- .github/workflows/test-build.yml | 11 ++++++++ .github/workflows/trigger-deploy.yml | 13 ++++++++- docker/app.Dockerfile | 34 +++++++++++++++++------- docker/db.Dockerfile | 22 +++++++++++----- docker/realtime.Dockerfile | 38 ++++++++++++++++++++------- 12 files changed, 175 insertions(+), 31 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5262ef8212f..b1b15f36135 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,6 +107,8 @@ jobs: tags: ${{ steps.meta.outputs.tags }} provenance: false sbom: false + cache-from: ${{ github.ref == 'refs/heads/main' && format('type=registry,ref={0}:latest-amd64', matrix.ghcr_image) || '' }} + cache-to: type=inline # Build ARM64 images for GHCR (main branch only, runs in parallel) build-ghcr-arm64: @@ -158,6 +160,10 @@ jobs: tags: ${{ steps.meta.outputs.tags }} provenance: false sbom: false + cache-from: | + type=registry,ref=${{ matrix.image }}:latest-arm64 + type=registry,ref=${{ matrix.image }}:${{ github.sha }}-arm64 + cache-to: type=inline # Create GHCR multi-arch manifests (only for main, after both builds) create-ghcr-manifests: diff --git a/.github/workflows/docs-embeddings.yml b/.github/workflows/docs-embeddings.yml index 5f51d4b0cc7..e01024a0e67 100644 --- a/.github/workflows/docs-embeddings.yml +++ b/.github/workflows/docs-embeddings.yml @@ -24,8 +24,19 @@ jobs: with: node-version: latest + - name: Cache Bun dependencies + uses: actions/cache@v4 + with: + path: | + ~/.bun/install/cache + node_modules + **/node_modules + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + - name: Install dependencies - run: bun install + run: bun install --frozen-lockfile - name: Process docs embeddings working-directory: ./apps/sim diff --git a/.github/workflows/i18n.yml b/.github/workflows/i18n.yml index d71cdffd617..e98fc6922b5 100644 --- a/.github/workflows/i18n.yml +++ b/.github/workflows/i18n.yml @@ -28,6 +28,17 @@ jobs: with: bun-version: 1.2.22 + - name: Cache Bun dependencies + uses: actions/cache@v4 + with: + path: | + ~/.bun/install/cache + node_modules + **/node_modules + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + - name: Run Lingo.dev translations env: LINGODOTDEV_API_KEY: ${{ secrets.LINGODOTDEV_API_KEY }} @@ -117,10 +128,21 @@ jobs: with: bun-version: 1.2.22 + - name: Cache Bun dependencies + uses: actions/cache@v4 + with: + path: | + ~/.bun/install/cache + node_modules + **/node_modules + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + - name: Install dependencies run: | cd apps/docs - bun install + bun install --frozen-lockfile - name: Build documentation to verify translations run: | diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml index ff6344c78ad..46b3b30bf30 100644 --- a/.github/workflows/images.yml +++ b/.github/workflows/images.yml @@ -97,6 +97,8 @@ jobs: tags: ${{ steps.meta.outputs.tags }} provenance: false sbom: false + cache-from: ${{ github.ref == 'refs/heads/main' && format('type=registry,ref={0}:latest-amd64', matrix.ghcr_image) || '' }} + cache-to: type=inline build-ghcr-arm64: name: Build ARM64 (GHCR Only) @@ -143,6 +145,10 @@ jobs: tags: ${{ steps.meta.outputs.tags }} provenance: false sbom: false + cache-from: | + type=registry,ref=${{ matrix.image }}:latest-arm64 + type=registry,ref=${{ matrix.image }}:${{ github.sha }}-arm64 + cache-to: type=inline create-ghcr-manifests: name: Create GHCR Manifests diff --git a/.github/workflows/migrations.yml b/.github/workflows/migrations.yml index 2bfb6ca1c66..cc1b471a866 100644 --- a/.github/workflows/migrations.yml +++ b/.github/workflows/migrations.yml @@ -18,8 +18,19 @@ jobs: with: bun-version: 1.2.22 + - name: Cache Bun dependencies + uses: actions/cache@v4 + with: + path: | + ~/.bun/install/cache + node_modules + **/node_modules + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + - name: Install dependencies - run: bun install + run: bun install --frozen-lockfile - name: Apply migrations working-directory: ./packages/db diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index 3e48ac6dc23..163a2ecd822 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -24,9 +24,20 @@ jobs: node-version: '18' registry-url: 'https://registry.npmjs.org/' + - name: Cache Bun dependencies + uses: actions/cache@v4 + with: + path: | + ~/.bun/install/cache + node_modules + **/node_modules + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + - name: Install dependencies working-directory: packages/cli - run: bun install + run: bun install --frozen-lockfile - name: Build package working-directory: packages/cli diff --git a/.github/workflows/publish-ts-sdk.yml b/.github/workflows/publish-ts-sdk.yml index 5158705fb31..9d8b3b44aaa 100644 --- a/.github/workflows/publish-ts-sdk.yml +++ b/.github/workflows/publish-ts-sdk.yml @@ -24,8 +24,19 @@ jobs: node-version: '22' registry-url: 'https://registry.npmjs.org/' + - name: Cache Bun dependencies + uses: actions/cache@v4 + with: + path: | + ~/.bun/install/cache + node_modules + **/node_modules + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + - name: Install dependencies - run: bun install + run: bun install --frozen-lockfile - name: Run tests working-directory: packages/ts-sdk diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 3a7a69f9652..fa10b0dd44b 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -23,6 +23,17 @@ jobs: with: node-version: latest + - name: Cache Bun dependencies + uses: actions/cache@v4 + with: + path: | + ~/.bun/install/cache + node_modules + **/node_modules + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + - name: Install dependencies run: bun install --frozen-lockfile diff --git a/.github/workflows/trigger-deploy.yml b/.github/workflows/trigger-deploy.yml index 88a059b282e..a3fed7619eb 100644 --- a/.github/workflows/trigger-deploy.yml +++ b/.github/workflows/trigger-deploy.yml @@ -29,8 +29,19 @@ jobs: with: bun-version: 1.2.22 + - name: Cache Bun dependencies + uses: actions/cache@v4 + with: + path: | + ~/.bun/install/cache + node_modules + **/node_modules + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + - name: Install dependencies - run: bun install + run: bun install --frozen-lockfile - name: Deploy to Trigger.dev (Staging) if: github.ref == 'refs/heads/staging' diff --git a/docker/app.Dockerfile b/docker/app.Dockerfile index 466c5a6747d..a3555ea5173 100644 --- a/docker/app.Dockerfile +++ b/docker/app.Dockerfile @@ -10,13 +10,16 @@ FROM base AS deps RUN apk add --no-cache libc6-compat WORKDIR /app -# Install turbo globally +# Install turbo globally (cached separately, changes infrequently) RUN bun install -g turbo -COPY package.json bun.lock ./ -RUN mkdir -p apps +# Copy package files first (these change less frequently than source code) +COPY package.json bun.lock turbo.json ./ +RUN mkdir -p apps packages COPY apps/sim/package.json ./apps/sim/package.json +COPY packages/db/package.json ./packages/db/package.json +# Install dependencies (this layer will be cached if package files don't change) RUN bun install --omit dev --ignore-scripts # ======================================== @@ -25,14 +28,26 @@ RUN bun install --omit dev --ignore-scripts FROM base AS builder WORKDIR /app -# Install turbo globally in builder stage +# Install turbo globally (cached separately, changes infrequently) RUN bun install -g turbo +# Copy node_modules from deps stage (cached if dependencies don't change) COPY --from=deps /app/node_modules ./node_modules -COPY . . -# Installing with full context to prevent missing dependencies error -RUN bun install --omit dev --ignore-scripts +# Copy package configuration files (needed for build) +COPY package.json bun.lock turbo.json ./ +COPY apps/sim/package.json ./apps/sim/package.json +COPY packages/db/package.json ./packages/db/package.json + +# Copy workspace configuration files (needed for turbo) +COPY apps/sim/next.config.ts ./apps/sim/next.config.ts +COPY apps/sim/tsconfig.json ./apps/sim/tsconfig.json +COPY apps/sim/tailwind.config.ts ./apps/sim/tailwind.config.ts +COPY apps/sim/postcss.config.mjs ./apps/sim/postcss.config.mjs + +# Copy source code (changes most frequently - placed last to maximize cache hits) +COPY apps/sim ./apps/sim +COPY packages ./packages # Required for standalone nextjs build WORKDIR /app/apps/sim @@ -63,15 +78,16 @@ RUN bun run build FROM base AS runner WORKDIR /app -# Install Python and dependencies for guardrails PII detection +# Install Python and dependencies for guardrails PII detection (cached separately) RUN apk add --no-cache python3 py3-pip bash ENV NODE_ENV=production -# Create non-root user and group +# Create non-root user and group (cached separately) RUN addgroup -g 1001 -S nodejs && \ adduser -S nextjs -u 1001 +# Copy application artifacts from builder (these change on every build) COPY --from=builder --chown=nextjs:nodejs /app/apps/sim/public ./apps/sim/public COPY --from=builder --chown=nextjs:nodejs /app/apps/sim/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/apps/sim/.next/static ./apps/sim/.next/static diff --git a/docker/db.Dockerfile b/docker/db.Dockerfile index 32c8f3addcf..31366eb3624 100644 --- a/docker/db.Dockerfile +++ b/docker/db.Dockerfile @@ -1,29 +1,39 @@ +# ======================================== +# Base Stage: Alpine Linux with Bun +# ======================================== +FROM oven/bun:1.2.22-alpine AS base + # ======================================== # Dependencies Stage: Install Dependencies # ======================================== -FROM oven/bun:1.2.22-alpine AS deps +FROM base AS deps WORKDIR /app -# Copy only package files needed for migrations +# Copy only package files needed for migrations (these change less frequently) COPY package.json bun.lock turbo.json ./ +RUN mkdir -p packages/db COPY packages/db/package.json ./packages/db/package.json -# Install dependencies +# Install dependencies (this layer will be cached if package files don't change) RUN bun install --ignore-scripts # ======================================== # Runner Stage: Production Environment # ======================================== -FROM oven/bun:1.2.22-alpine AS runner +FROM base AS runner WORKDIR /app -# Create non-root user and group +# Create non-root user and group (cached separately) RUN addgroup -g 1001 -S nodejs && \ adduser -S nextjs -u 1001 -# Copy only the necessary files from deps +# Copy only the necessary files from deps (cached if dependencies don't change) COPY --from=deps --chown=nextjs:nodejs /app/node_modules ./node_modules + +# Copy package configuration files (needed for migrations) COPY --chown=nextjs:nodejs packages/db/drizzle.config.ts ./packages/db/drizzle.config.ts + +# Copy database package source code (changes most frequently - placed last) COPY --chown=nextjs:nodejs packages/db ./packages/db # Switch to non-root user diff --git a/docker/realtime.Dockerfile b/docker/realtime.Dockerfile index 8488d6b32c0..539a9c0a9e9 100644 --- a/docker/realtime.Dockerfile +++ b/docker/realtime.Dockerfile @@ -10,23 +10,35 @@ FROM base AS deps RUN apk add --no-cache libc6-compat WORKDIR /app -# Install turbo globally +# Install turbo globally (cached separately, changes infrequently) RUN bun install -g turbo -COPY package.json bun.lock ./ -RUN mkdir -p apps +# Copy package files first (these change less frequently than source code) +COPY package.json bun.lock turbo.json ./ +RUN mkdir -p apps packages COPY apps/sim/package.json ./apps/sim/package.json +COPY packages/db/package.json ./packages/db/package.json +# Install dependencies (this layer will be cached if package files don't change) RUN bun install --omit dev --ignore-scripts # ======================================== -# Builder Stage: Build the Application +# Builder Stage: Prepare source code # ======================================== FROM base AS builder WORKDIR /app +# Copy node_modules from deps stage (cached if dependencies don't change) COPY --from=deps /app/node_modules ./node_modules -COPY . . + +# Copy package configuration files (needed for build) +COPY package.json bun.lock turbo.json ./ +COPY apps/sim/package.json ./apps/sim/package.json +COPY packages/db/package.json ./packages/db/package.json + +# Copy source code (changes most frequently - placed last to maximize cache hits) +COPY apps/sim ./apps/sim +COPY packages ./packages # ======================================== # Runner Stage: Run the Socket Server @@ -36,16 +48,22 @@ WORKDIR /app ENV NODE_ENV=production -# Create non-root user and group +# Create non-root user and group (cached separately) RUN addgroup -g 1001 -S nodejs && \ adduser -S nextjs -u 1001 -# Copy the sim app and the shared db package needed by socket-server -COPY --from=builder --chown=nextjs:nodejs /app/apps/sim ./apps/sim -COPY --from=builder --chown=nextjs:nodejs /app/packages/db ./packages/db -COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules +# Copy package.json first (changes less frequently) COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json +# Copy node_modules from builder (cached if dependencies don't change) +COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules + +# Copy db package (needed by socket-server) +COPY --from=builder --chown=nextjs:nodejs /app/packages/db ./packages/db + +# Copy sim app (changes most frequently - placed last) +COPY --from=builder --chown=nextjs:nodejs /app/apps/sim ./apps/sim + # Switch to non-root user USER nextjs