Skip to content

Commit ccb2569

Browse files
committed
perf(cli): split deployed image dependencies and code into separate layers
The final image stage copied all of /app in one layer, so node_modules was re-pushed and re-pulled on every deploy even when dependencies were unchanged. Copy node_modules as its own layer and the bundled code (via a stage that strips node_modules) separately, so unchanged dependencies produce an identical blob that registries and workers already have.
1 parent 90e8bd5 commit ccb2569

3 files changed

Lines changed: 59 additions & 6 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"trigger.dev": patch
3+
---
4+
5+
Deployed images now ship dependencies and bundled task code as separate layers. Repeat deploys with unchanged dependencies push and pull far less data, making deploys and worker image pulls faster.

‎packages/cli-v3/src/deploy/buildImage.test.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,32 @@ describe("generateContainerfile", () => {
2525

2626
expect(containerfile).toContain(`FROM ${image} AS base`);
2727
});
28+
29+
it.each(["node", "bun"] as BuildRuntime[])(
30+
"splits node_modules and app code into separate layers for %s",
31+
async (runtime) => {
32+
const containerfile = await generateContainerfile({
33+
runtime,
34+
build: {},
35+
image: undefined,
36+
indexScript: "index.js",
37+
entrypoint: "entrypoint.js",
38+
});
39+
40+
const user = runtime === "bun" ? "bun:bun" : "node:node";
41+
42+
expect(containerfile).toContain("FROM build AS code");
43+
expect(containerfile).toContain("RUN rm -rf node_modules");
44+
expect(containerfile).toContain(
45+
`COPY --from=build --chown=${user} /app/node_modules ./node_modules`
46+
);
47+
expect(containerfile).toContain(`COPY --from=code --chown=${user} /app ./`);
48+
// The final stage must not copy all of /app from the build stage anymore,
49+
// or node_modules would be duplicated across two layers
50+
expect(containerfile).not.toContain(`COPY --from=build --chown=${user} /app ./`);
51+
52+
// node_modules must exist even for projects with zero external dependencies
53+
expect(containerfile).toContain("mkdir -p node_modules");
54+
}
55+
);
2856
});

‎packages/cli-v3/src/deploy/buildImage.ts‎

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -772,14 +772,20 @@ ${buildArgs}
772772
${buildEnvVars}
773773
774774
COPY --chown=bun:bun package.json ./
775-
RUN bun install --production --no-save
775+
# mkdir guards against bun not creating node_modules when there are no dependencies
776+
RUN bun install --production --no-save && mkdir -p node_modules
776777
777778
# Now copy all the files
778779
# IMPORTANT: Do this after running npm install because npm i will wipe out the node_modules directory
779780
COPY --chown=bun:bun . .
780781
781782
${postInstallCommands}
782783
784+
# App files without node_modules, so the final stage can layer them separately
785+
FROM build AS code
786+
787+
RUN rm -rf node_modules
788+
783789
FROM build AS indexer
784790
785791
USER bun
@@ -831,8 +837,12 @@ ENV TRIGGER_PROJECT_ID=\${TRIGGER_PROJECT_ID} \
831837
NODE_EXTRA_CA_CERTS=\${NODE_EXTRA_CA_CERTS} \
832838
NODE_ENV=production
833839
834-
# Copy the files from the build stage
835-
COPY --from=build --chown=bun:bun /app ./
840+
# Dependencies as their own layer: unchanged deps produce an identical blob
841+
# that registries and workers already have, so repeat deploys skip it
842+
COPY --from=build --chown=bun:bun /app/node_modules ./node_modules
843+
844+
# Copy the app files (without node_modules) from the code stage
845+
COPY --from=code --chown=bun:bun /app ./
836846
837847
# Copy the index.json file from the indexer stage
838848
COPY --from=indexer --chown=bun:bun /app/index.json ./
@@ -877,7 +887,8 @@ ENV NODE_ENV=production
877887
ENV NPM_CONFIG_UPDATE_NOTIFIER=false
878888
879889
COPY --chown=node:node package.json ./
880-
RUN npm i --no-audit --no-fund --no-save --no-package-lock
890+
# mkdir guards against npm not creating node_modules when there are no dependencies
891+
RUN npm i --no-audit --no-fund --no-save --no-package-lock && mkdir -p node_modules
881892
882893
# Now copy all the files
883894
# IMPORTANT: Do this after running npm install because npm i will wipe out the node_modules directory
@@ -888,6 +899,11 @@ ${postInstallCommands}
888899
# IMPORTANT: Doing this again to fix an issue with prisma generate removing the files in node_modules/trigger.dev for some reason...
889900
COPY --chown=node:node . .
890901
902+
# App files without node_modules, so the final stage can layer them separately
903+
FROM build AS code
904+
905+
RUN rm -rf node_modules
906+
891907
FROM build AS indexer
892908
893909
USER node
@@ -941,8 +957,12 @@ ENV TRIGGER_PROJECT_ID=\${TRIGGER_PROJECT_ID} \
941957
NODE_EXTRA_CA_CERTS=\${NODE_EXTRA_CA_CERTS} \
942958
NODE_ENV=production
943959
944-
# Copy the files from the install stage
945-
COPY --from=build --chown=node:node /app ./
960+
# Dependencies as their own layer: unchanged deps produce an identical blob
961+
# that registries and workers already have, so repeat deploys skip it
962+
COPY --from=build --chown=node:node /app/node_modules ./node_modules
963+
964+
# Copy the app files (without node_modules) from the code stage
965+
COPY --from=code --chown=node:node /app ./
946966
947967
# Copy the index.json file from the indexer stage
948968
COPY --from=indexer --chown=node:node /app/index.json ./

0 commit comments

Comments
 (0)