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)