Skip to content

Commit bd8ce4a

Browse files
authored
feat(deployments): split project dependencies and code into separate layers (#4551)
Deploy images previously shipped node_modules and the bundled task code in a single layer, so every deploy re-pushed and re-pulled the full dependency tree even when nothing in it changed. The generated Containerfile now copies `/app/node_modules` as its own layer and the app files separately. With unchanged dependencies the dependency layer is identical across deploys, so registries and workers already have it and only the code layer moves.
1 parent c00fb9c commit bd8ce4a

3 files changed

Lines changed: 78 additions & 4 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 typically push and pull far less data, making deploys and worker image pulls faster.

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,53 @@ 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(
44+
`COPY --from=build --chown=${user} /app/node_modules ./node_modules`
45+
);
46+
expect(containerfile).toContain(`COPY --from=code --chown=${user} /app ./`);
47+
// copying all of /app from build would duplicate node_modules across two layers
48+
expect(containerfile).not.toContain(`COPY --from=build --chown=${user} /app ./`);
49+
}
50+
);
51+
52+
it.each(["node", "bun"] as BuildRuntime[])(
53+
"orders post-install commands, the node_modules guard, and the code stage for %s",
54+
async (runtime) => {
55+
const containerfile = await generateContainerfile({
56+
runtime,
57+
build: { commands: ["echo post-install"] },
58+
image: undefined,
59+
indexScript: "index.js",
60+
entrypoint: "entrypoint.js",
61+
});
62+
63+
const postInstall = containerfile.indexOf("RUN echo post-install");
64+
// guard after post-install so a command that prunes node_modules can't break the COPY
65+
const mkdirGuard = containerfile.indexOf("RUN mkdir -p node_modules");
66+
const codeStage = containerfile.indexOf("FROM build AS code");
67+
const rmNodeModules = containerfile.indexOf(
68+
"RUN chmod -R u+rwX node_modules && rm -rf node_modules"
69+
);
70+
71+
expect(postInstall).toBeGreaterThan(-1);
72+
expect(mkdirGuard).toBeGreaterThan(postInstall);
73+
expect(codeStage).toBeGreaterThan(mkdirGuard);
74+
expect(rmNodeModules).toBeGreaterThan(codeStage);
75+
}
76+
);
2877
});

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

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,14 @@ COPY --chown=bun:bun . .
780780
781781
${postInstallCommands}
782782
783+
# node_modules may not exist when there are no dependencies to install
784+
RUN mkdir -p node_modules
785+
786+
FROM build AS code
787+
788+
# u+rwX first: non-root rm fails on read-only or non-traversable directories
789+
RUN chmod -R u+rwX node_modules && rm -rf node_modules
790+
783791
FROM build AS indexer
784792
785793
USER bun
@@ -831,8 +839,10 @@ ENV TRIGGER_PROJECT_ID=\${TRIGGER_PROJECT_ID} \
831839
NODE_EXTRA_CA_CERTS=\${NODE_EXTRA_CA_CERTS} \
832840
NODE_ENV=production
833841
834-
# Copy the files from the build stage
835-
COPY --from=build --chown=bun:bun /app ./
842+
# Unchanged dependencies produce an identical layer that repeat deploys skip
843+
COPY --from=build --chown=bun:bun /app/node_modules ./node_modules
844+
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 ./
@@ -888,6 +898,14 @@ ${postInstallCommands}
888898
# IMPORTANT: Doing this again to fix an issue with prisma generate removing the files in node_modules/trigger.dev for some reason...
889899
COPY --chown=node:node . .
890900
901+
# node_modules may not exist when there are no dependencies to install
902+
RUN mkdir -p node_modules
903+
904+
FROM build AS code
905+
906+
# u+rwX first: non-root rm fails on read-only or non-traversable directories
907+
RUN chmod -R u+rwX node_modules && rm -rf node_modules
908+
891909
FROM build AS indexer
892910
893911
USER node
@@ -941,8 +959,10 @@ ENV TRIGGER_PROJECT_ID=\${TRIGGER_PROJECT_ID} \
941959
NODE_EXTRA_CA_CERTS=\${NODE_EXTRA_CA_CERTS} \
942960
NODE_ENV=production
943961
944-
# Copy the files from the install stage
945-
COPY --from=build --chown=node:node /app ./
962+
# Unchanged dependencies produce an identical layer that repeat deploys skip
963+
COPY --from=build --chown=node:node /app/node_modules ./node_modules
964+
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)