# Builder stage: install dependencies and build the AWS package from source FROM node:20-slim AS builder WORKDIR /workspace # Use corepack/pnpm to manage dependencies RUN corepack enable RUN npm i -g pnpm # Copy workspace manifest files first to leverage layer caching COPY package.json pnpm-workspace.yaml ./ # Copy the rest of the repository COPY . . # Install dependencies for the monorepo RUN pnpm install # Build the OpenNext AWS package from source RUN pnpm --filter @opennextjs/aws run build # Runtime stage: we only ship the built package and its dependencies FROM node:20-slim WORKDIR /workspace # Copy built artifacts and dependencies from builder COPY --from=builder /workspace/packages/open-next/dist ./packages/open-next/dist COPY --from=builder /workspace/node_modules ./node_modules # Optionally copy the package.json for the AWS package (useful for runtime tooling) COPY --from=builder /workspace/packages/open-next/package.json ./packages/open-next/package.json # Default command: run the OpenNext AWS CLI with node on the built dist CMD ["node", "packages/open-next/dist/index.js"]
# Docker build context exclusions .git .gitignore node_modules dist build coverage .next .env* *.log pnpm-lock.yaml *.md docs/ examples/ .github/ README.md CHANGELOG.md .DS_Store
- Failing step (command/step): [builder 4/8] RUN npm i -g pnpm (Dockerfile: line 8) - Exact error message and exit code: ERROR: process "/bin/sh -c npm i -g pnpm" did not complete successfully: exit code: 1 - Missing packages or files mentioned: - EEXIST: file already exists - File exists: /usr/local/bin/pnpm - Suggested fix: Remove the existing file and try again, or run npm with --force to overwrite files recklessly. - Related log path: A complete log of this run can be found in: /root/.npm/_logs/2026-03-21T20_47_43_760Z-debug-0.log - Version mismatch / notices: - New major version of npm available! 10.8.2 -> 11.12.0 - Additional context: - The failure is due to an existing PNPM binary at /usr/local/bin/pnpm preventing npm i -g pnpm from completing.
# Builder stage: install dependencies and build the AWS package from source FROM node:20-slim AS builder WORKDIR /workspace # Copy the entire repository first to leverage build cache COPY . . # Use corepack/pnpm to manage dependencies RUN corepack enable # Install dependencies for the monorepo RUN pnpm install # Build the OpenNext AWS package from source RUN pnpm --filter @opennextjs/aws run build # Runtime stage: ship the built package and dependencies FROM node:20-slim WORKDIR /workspace # Copy built artifacts and dependencies from builder COPY --from=builder /workspace/packages/open-next/dist ./packages/open-next/dist COPY --from=builder /workspace/node_modules ./node_modules # Copy the AWS package.json for reference (optional runtime tooling) COPY --from=builder /workspace/packages/open-next/package.json ./packages/open-next/package.json # Default command: run the OpenNext AWS CLI with node on the built dist CMD ["node", "packages/open-next/dist/index.js"]
- Exact error message and exit code - src/overrides/wrappers/express-dev.ts(75,10): error TS2345: Argument of type '"error"' is not assignable to parameter of type '"mount"'. - ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL @opennextjs/aws@3.9.16 build: `tsc && tsc-alias` - Exit status 2 - Failing command/step - RUN pnpm --filter @opennextjs/aws run build (Dockerfile: line 16) - Missing packages or files mentioned - WARN: Failed to create bin at /workspace/packages/tests-unit/node_modules/.bin/open-next. ENOENT: no such file or directory, chmod '/workspace/packages/tests-unit/node_modules/@opennextjs/aws/dist/index.js' - Version information - Base image: node:20-slim - Build tooling: pnpm v10.32.1 (evidenced by “Done in 23.2s using pnpm v10.32.1”) - Context: OpenNext AWS package version 3.9.16 (@opennextjs/aws) being built - Notes - The failure occurs during TypeScript compilation of the OpenNext AWS package, indicating a type mismatch in express-dev.ts at line 75. This is a code-level error rather than a missing dependency.
Error: file not found: src/overrides/wrappers/express-dev.ts
No matches found.
- Sets up an Express dev wrapper for a Next.js-like environment. Exports default { wrapper, name: "expresss-dev", supportStreaming: true }.
- Imports:
- path from "node:path"
- express from "express"
- NextConfig from "config/index"
- type StreamCreator from "types/open-next.js"
- type WrapperHandler from "types/overrides.js"
- getMonorepoRelativePath from "utils/normalize-path"
- Wrapper logic (async (handler, converter)):
- Creates Express app, disables "x-powered-by".
- basePath = NextConfig.basePath ?? "".
- Serves static assets at basePath from path.join(getMonorepoRelativePath(), "assets").
- image handler:
- imageHandlerPath = path.join(getMonorepoRelativePath(), "image-optimization-function/index.mjs")
- imageHandler = await import(imageHandlerPath).then((m) => m.handler)
- Route: app.all(`${NextConfig.basePath ?? ""}/_next/image`, …)
- internalEvent = await converter.convertFrom(req)
- streamCreator.writeHeaders: res.writeHead(prelude.statusCode, prelude.headers)
- await imageHandler(internalEvent, { streamCreator })
- Route: app.all("*paths", …)
- If http and localhost: set process.env.__NEXT_PRIVATE_ORIGIN and req.headers["x-forwarded-proto"]
- internalEvent = await converter.convertFrom(req)
- AbortController setup; streamCreator with:
- writeHeaders: set "Set-Cookie", writeHead, flushHeaders
- onFinish: no-op
- abortSignal: abortController.signal
- res.on("close", () => abortController.abort())
- await handler(internalEvent, { streamCreator })
- Server startup:
- server = app.listen(Number.parseInt(process.env.PORT ?? "3000", 10), () => {
console.log(`Server running on port ${process.env.PORT ?? 3000}`);
})
- app.on("error", (err) => console.error("error", err))
- Return cleanup: () => { server.close(); }
- Key file references:
- assets directory: getMonorepoRelativePath() + "/assets"
- image optimization: getMonorepoRelativePath() + "/image-optimization-function/index.mjs"
- Port behavior:
- Uses process.env.PORT with default 3000. Logs: "Server running on port ${process.env.PORT ?? 3000}".import path from "node:path";
import express from "express";
import { NextConfig } from "config/index";
import type { StreamCreator } from "types/open-next.js";
import type { WrapperHandler } from "types/overrides.js";
import { getMonorepoRelativePath } from "utils/normalize-path";
const wrapper: WrapperHandler = async (handler, converter) => {
const app = express();
// We disable this cause we wanna use it ourself
// https://stackoverflow.com/a/13055495/16587222
app.disable("x-powered-by");
// To serve static assets
const basePath = NextConfig.basePath ?? "";
app.use(
basePath,
express.static(path.join(getMonorepoRelativePath(), "assets")),
);
const imageHandlerPath = path.join(
getMonorepoRelativePath(),
"image-optimization-function/index.mjs",
);
const imageHandler = await import(imageHandlerPath).then((m) => m.handler);
app.all(`${NextConfig.basePath ?? ""}/_next/image`, async (req, res) => {
const internalEvent = await converter.convertFrom(req);
const streamCreator: StreamCreator = {
writeHeaders: (prelude) => {
res.writeHead(prelude.statusCode, prelude.headers);
return res;
},
};
await imageHandler(internalEvent, { streamCreator });
});
app.all("*paths", async (req, res) => {
if (req.protocol === "http" && req.hostname === "localhost") {
// This is used internally by Next.js during redirects in server actions. We need to set it to the origin of the request.
process.env.__NEXT_PRIVATE_ORIGIN = `${req.protocol}://${req.hostname}`;
// This is to make `next-auth` and other libraries that rely on this header to work locally out of the box.
req.headers["x-forwarded-proto"] = req.protocol;
}
const internalEvent = await converter.convertFrom(req);
const abortController = new AbortController();
const streamCreator: StreamCreator = {
writeHeaders: (prelude) => {
res.setHeader("Set-Cookie", prelude.cookies);
res.writeHead(prelude.statusCode, prelude.headers);
res.flushHeaders();
return res;
},
onFinish: () => {},
abortSignal: abortController.signal,
};
res.on("close", () => {
abortController.abort();
});
await handler(internalEvent, { streamCreator });
});
const server = app.listen(
Number.parseInt(process.env.PORT ?? "3000", 10),
() => {
console.log(`Server running on port ${process.env.PORT ?? 3000}`);
},
);
app.on("error" as any, (err: any) => {
console.error("error", err);
});
return () => {
server.close();
};
};
export default {
wrapper,
name: "expresss-dev",
supportStreaming: true,
};
Concerns: Runtime image copies whole node_modules; could be optimized by pruning production dependencies in the builder to reduce image size., No HEALTHCHECK defined; consider adding to monitor runtime health., Assumes index.js is a CLI entrypoint; if it's a library, the CMD may not run as intended.
Smoke [PASS]: test -f packages/open-next/dist/index.js
Smoke [FAIL]: node -e 'try { require("./packages/open-next/dist/index.js"); console.log("open-next-loaded"); } catch(e) { console.error(e); process.exit(1); }
Output: sh: 1: Syntax error: Unterminated quoted string
Smoke [PASS]: node -vSmoke [PASS]: test -f packages/open-next/dist/index.js Smoke [PASS]: test -d node_modules Smoke [PASS]: node packages/open-next/dist/index.js --version 2>&1 | head -n 1 || true