You have a local Next.js 15 application running perfectly. It utilizes Server Actions, dynamic routes, and the latest React Server Components. You push to AWS Amplify Gen 2, expecting a Vercel-like experience.
Instead, the build fails.
You might see generic "Access Denied" errors during the deployment phase, a 503 error on the deployed URL, or the build process might simply run out of memory and crash silently.
Deploying Server-Side Rendering (SSR) applications outside of the Vercel ecosystem requires understanding how the build artifacts are containerized. Here is the technical root cause analysis and the definitive configuration to deploy Next.js 15 to AWS Amplify Gen 2 successfully.
The Root Cause: Infrastructure Impedance Mismatch
To fix the build, you must understand what is breaking. When you deploy to Vercel, the platform automatically detects Next.js primitives and maps them to its proprietary Edge Network.
AWS Amplify Gen 2 takes a different approach. It attempts to deploy your application as a combination of Amazon CloudFront (CDN), S3 (Static Assets), and AWS Lambda (Compute for SSR/API).
The Three Failure Points
- The Output Format: By default, Next.js produces a build intended for a Node.js runtime managed by the developer. Amplify expects a "Standalone" build that bundles necessary dependencies (like
node_modules) into a deployable artifact for Lambda. - Memory Exhaustion: Next.js 15 builds are cache-heavy. The default Docker container used by Amplify CI/CD often hits the Node.js heap limit during the webpack compilation or static generation phase.
- IAM & Role Drifts: The "Access Denied" error often occurs not because you lack permission to push code, but because the Amplify Build Role lacks permission to update the specific CloudFormation stacks generated by the Gen 2 CDK adapter.
Solution Part 1: Configuring Next.js for Lambda
The most critical step is telling Next.js to produce a standalone folder. This creates a minimal Node.js server containing only the files necessary for production.
Open your next.config.mjs and explicitly set the output mode. We also need to optimize the image handling, as the default Next.js Image component relies on sharp, which behaves differently in Lambda environments.
/** @type {import('next').NextConfig} */
const nextConfig = {
// 1. Create a standalone folder for Docker/Lambda compatibility
output: "standalone",
// 2. Disable x-powered-by header for security
poweredByHeader: false,
// 3. Ensure React Strict Mode is on for robust error catching
reactStrictMode: true,
// 4. Logging for deeper visibility in CloudWatch
logging: {
fetches: {
fullUrl: true,
},
},
// 5. Optimize external packages if you use heavy UI libs
experimental: {
optimizePackageImports: ['lucide-react', '@radix-ui/react-icons'],
},
};
export default nextConfig;
Note on Image Optimization: When using output: "standalone", you must manually install sharp so the image optimization logic is bundled correctly.
npm install sharp
Solution Part 2: The Correct Amplify Build Specification
The default amplify.yml auto-generated by the console is frequently incorrect for Next.js 15 SSR apps. It often defaults to looking for a static out directory or fails to cache dependencies properly.
You need to map the .next folder specifically. We will also include memory settings directly in the build commands to prevent heap crashes.
Create or update amplify.yml at the root of your project:
version: 1
backend:
phases:
build:
commands:
- npm ci --cache .npm --prefer-offline
- npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID
frontend:
phases:
preBuild:
commands:
- npm ci --cache .npm --prefer-offline
build:
commands:
# Increase Node memory limit to 4GB to prevent OOM errors
- export NODE_OPTIONS="--max-old-space-size=4096"
- npm run build
artifacts:
baseDirectory: .next
files:
- '**/*'
cache:
paths:
- .next/cache/**/*
- .npm/**/*
- node_modules/**/*
Why This Config Works
baseDirectory: .next: We are pointing Amplify to the build output, not a static export folder.NODE_OPTIONS: Next.js 14/15 compilation is heavy. The default container usually has 4GB or 7GB of RAM, but the default Node process caps at roughly 1.5GB. We explicitly expand this to 4GB.npx ampx pipeline-deploy: This command ensures the Gen 2 backend resources (defined inamplify/backend.tsorresource.ts) are deployed before the frontend build finalizes.
Solution Part 3: Handling "Access Denied" & Rewrite Rules
If your build succeeds but the deployed site throws an "Access Denied" XML error on page load, the issue is routing.
CloudFront needs to know which requests to send to S3 (static assets) and which to send to Lambda (SSR).
In Amplify Gen 2, this is usually handled automatically, but if you have a rewrites section in next.config.mjs, it can conflict with Amplify's routing.
Validating the Rewrite Rule
Navigate to the Amplify Console -> App Settings -> Rewrites and redirects. Ensure you have the catch-all rule for SSR:
- Source address:
/<*> - Target address:
/index.html(for Gen 1) or the Compute endpoint. - Type: 200 (Rewrite)
Critical Gen 2 Note: In Gen 2, if you are using output: 'standalone', Amplify's adapter usually handles this. However, if you see 403 errors on assets (like _next/static), you need to ensure your build artifacts in amplify.yml included the static folder recursively (**/*).
Deep Dive: Server Actions and 500 Errors
A common issue in Next.js 15 on Amplify is Server Actions failing with a 500 error.
Server Actions use HTTP POST requests to the same URL as the page. If your CloudFront distribution is configured to cache POST requests or strip headers, Server Actions will fail.
Amplify Gen 2 defaults should handle this, but if you have customized the CDK customHttp output, ensure you are allowing the specific headers Next.js uses for server actions:
rscnext-router-state-treenext-action
If you are defining infrastructure in amplify/backend.ts, you do not typically need to touch this. However, if you are seeing CSRF issues, verify your Middleware.
Middleware configuration
If you use middleware.ts, ensure you are not accidentally blocking internal Next.js requests.
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
// Always allow internal Next.js requests to pass through
if (request.nextUrl.pathname.startsWith('/_next')) {
return NextResponse.next();
}
// Your custom logic here
return NextResponse.next();
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
Summary of the Fix
To successfully deploy Next.js 15 SSR on Amplify Gen 2:
- Update
next.config.mjs: Setoutput: 'standalone'. - Install Sharp: Run
npm install sharpfor image optimization. - Configure
amplify.yml: PointbaseDirectoryto.next, setNODE_OPTIONSmemory limits, and cachenode_modules. - Verify Roles: If build fails immediately, disconnect and reconnect the branch in the Amplify console to reset the IAM service role permissions.
By treating the Amplify environment as a containerized Lambda deployment rather than a simple static host, you eliminate the fragility of the build pipeline and unlock the full performance of Next.js 15.