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:
@@ -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);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 ./
|
||||
|
||||
Reference in New Issue
Block a user