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.
This commit is contained in:
Saadi Myftija
2026-08-10 14:44:11 +02:00
committed by GitHub
parent c00fb9c36c
commit bd8ce4a50f
3 changed files with 78 additions and 4 deletions
@@ -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.
@@ -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);
}
);
});
+24 -4
View File
@@ -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 ./