diff --git a/.changeset/split-image-dependency-layers.md b/.changeset/split-image-dependency-layers.md new file mode 100644 index 00000000000..472017ee293 --- /dev/null +++ b/.changeset/split-image-dependency-layers.md @@ -0,0 +1,5 @@ +--- +"trigger.dev": patch +--- + +Deployed images now ship dependencies and bundled task code as separate layers. Repeat deploys with unchanged dependencies typically push and pull far less data, making deploys and worker image pulls faster. diff --git a/packages/cli-v3/src/deploy/buildImage.test.ts b/packages/cli-v3/src/deploy/buildImage.test.ts index 58ac10b229c..cbeb58c083c 100644 --- a/packages/cli-v3/src/deploy/buildImage.test.ts +++ b/packages/cli-v3/src/deploy/buildImage.test.ts @@ -25,4 +25,53 @@ describe("generateContainerfile", () => { expect(containerfile).toContain(`FROM ${image} AS base`); }); + + it.each(["node", "bun"] as BuildRuntime[])( + "splits node_modules and app code into separate layers for %s", + async (runtime) => { + const containerfile = await generateContainerfile({ + runtime, + build: {}, + image: undefined, + indexScript: "index.js", + entrypoint: "entrypoint.js", + }); + + const user = runtime === "bun" ? "bun:bun" : "node:node"; + + expect(containerfile).toContain("FROM build AS code"); + expect(containerfile).toContain( + `COPY --from=build --chown=${user} /app/node_modules ./node_modules` + ); + expect(containerfile).toContain(`COPY --from=code --chown=${user} /app ./`); + // copying all of /app from build would duplicate node_modules across two layers + expect(containerfile).not.toContain(`COPY --from=build --chown=${user} /app ./`); + } + ); + + it.each(["node", "bun"] as BuildRuntime[])( + "orders post-install commands, the node_modules guard, and the code stage for %s", + async (runtime) => { + const containerfile = await generateContainerfile({ + runtime, + build: { commands: ["echo post-install"] }, + image: undefined, + indexScript: "index.js", + entrypoint: "entrypoint.js", + }); + + const postInstall = containerfile.indexOf("RUN echo post-install"); + // guard after post-install so a command that prunes node_modules can't break the COPY + const mkdirGuard = containerfile.indexOf("RUN mkdir -p node_modules"); + const codeStage = containerfile.indexOf("FROM build AS code"); + const rmNodeModules = containerfile.indexOf( + "RUN chmod -R u+rwX node_modules && rm -rf node_modules" + ); + + expect(postInstall).toBeGreaterThan(-1); + expect(mkdirGuard).toBeGreaterThan(postInstall); + expect(codeStage).toBeGreaterThan(mkdirGuard); + expect(rmNodeModules).toBeGreaterThan(codeStage); + } + ); }); diff --git a/packages/cli-v3/src/deploy/buildImage.ts b/packages/cli-v3/src/deploy/buildImage.ts index 8e393a70ddc..210a70be348 100644 --- a/packages/cli-v3/src/deploy/buildImage.ts +++ b/packages/cli-v3/src/deploy/buildImage.ts @@ -780,6 +780,14 @@ COPY --chown=bun:bun . . ${postInstallCommands} +# node_modules may not exist when there are no dependencies to install +RUN mkdir -p node_modules + +FROM build AS code + +# u+rwX first: non-root rm fails on read-only or non-traversable directories +RUN chmod -R u+rwX node_modules && rm -rf node_modules + FROM build AS indexer USER bun @@ -831,8 +839,10 @@ ENV TRIGGER_PROJECT_ID=\${TRIGGER_PROJECT_ID} \ NODE_EXTRA_CA_CERTS=\${NODE_EXTRA_CA_CERTS} \ NODE_ENV=production -# Copy the files from the build stage -COPY --from=build --chown=bun:bun /app ./ +# Unchanged dependencies produce an identical layer that repeat deploys skip +COPY --from=build --chown=bun:bun /app/node_modules ./node_modules + +COPY --from=code --chown=bun:bun /app ./ # Copy the index.json file from the indexer stage COPY --from=indexer --chown=bun:bun /app/index.json ./ @@ -888,6 +898,14 @@ ${postInstallCommands} # IMPORTANT: Doing this again to fix an issue with prisma generate removing the files in node_modules/trigger.dev for some reason... COPY --chown=node:node . . +# node_modules may not exist when there are no dependencies to install +RUN mkdir -p node_modules + +FROM build AS code + +# u+rwX first: non-root rm fails on read-only or non-traversable directories +RUN chmod -R u+rwX node_modules && rm -rf node_modules + FROM build AS indexer USER node @@ -941,8 +959,10 @@ ENV TRIGGER_PROJECT_ID=\${TRIGGER_PROJECT_ID} \ NODE_EXTRA_CA_CERTS=\${NODE_EXTRA_CA_CERTS} \ NODE_ENV=production -# Copy the files from the install stage -COPY --from=build --chown=node:node /app ./ +# Unchanged dependencies produce an identical layer that repeat deploys skip +COPY --from=build --chown=node:node /app/node_modules ./node_modules + +COPY --from=code --chown=node:node /app ./ # Copy the index.json file from the indexer stage COPY --from=indexer --chown=node:node /app/index.json ./