Seriously, test your production build locally first. I learned this the hard way after taking down staging for two hours because I was lazy and skipped this step. The team lead was... let's just say he wasn't thrilled about explaining to the CEO why our demo was broken.
When your build breaks in production: Vercel bills you for the failed attempts (yeah, they charge for failures too), your teammates start giving you the stink eye, and instead of shipping features you're debugging webpack nonsense at 11pm on a Friday.
The "It Builds Locally" Reality Check
Run this shit exactly as written:
## Delete node_modules and package-lock.json first (trust me on this)
rm -rf node_modules package-lock.json
npm install
## Build like production actually will
NODE_ENV=production npm run build
npm start
Hit localhost:3000
and click through your app like a pissed-off user would. Open the browser console - see any red errors? Check your terminal - any warnings about chunking or dynamic imports?
Critical: If you see Warning: Failed to load resource
, your images or API routes are fucked. Fix this shit now or you'll be debugging it at 2am when it breaks in production and your boss is blowing up your phone asking why the site is down.
Here's what I actually check before deploying (learned from production incidents):
- Dynamic imports actually work - test the pages that use lazy loading
- API routes don't timeout - hit your slowest endpoint a few times
- Images load from the right paths - Next.js Image optimization breaks differently in prod
- Database connections close properly - check your connection pool limits
- Environment variables exist - your app will fail silently if they're missing
Environment Variables: Where Dreams Go to Die
Environment variables cause 90% of deployment failures. I'm not exaggerating. Vercel's interface for managing them is confusing as hell, and their scoping system will bite you in the ass when you least expect it.
The nuclear option (do this first or suffer):
## Create .env.example with FAKE values
DATABASE_URL=\"postgresql://user:pass@localhost:5432/myapp_dev\"
NEXTAUTH_SECRET=\"your-secret-here\"
STRIPE_SECRET_KEY=\"sk_test_...\"
NEXT_PUBLIC_APP_URL=\"http://localhost:3000\"
## Then create .env.local with REAL values
## NEVER FUCKING COMMIT .env.local TO GIT (yes, I've done this)
Production environment gotchas I've learned the hard way:
Vercel has 3 environment scopes: Development, Preview, Production. If your Preview works but Production doesn't, you probably set variables in the wrong scope.
NEXT_PUBLIC_
variables are bundled into your client code. Don't put secrets in them.Database URLs break differently in serverless - connection pooling that works locally will crash in production. Use PlanetScale or Supabase with connection pooling enabled.
Vercel strips trailing slashes from URLs - if your API expects them, add redirects in
next.config.js
.
Dev mode clusterfuck: Sometimes Next.js just decides environment variables shouldn't reload in dev mode. You'll change a var, wonder why nothing works, then waste 20 minutes pulling your hair out before remembering to restart the damn dev server. Happens across multiple versions - ask me how I know.
Next.js 15 Config: The Stuff That Actually Matters
The Next.js docs show you pristine config files. Here's what actually works in production after I've debugged the failures:
// next.config.mjs - This actually works
/** @type {import('next').NextConfig} */
const nextConfig = {
// This breaks in Docker if you use it wrong
output: 'standalone',
// Images will cause issues if not configured properly
images: {
formats: ['image/webp', 'image/avif'],
dangerouslyAllowSVG: false, // Keep this false for security
domains: ['cdn.yourdomain.com'], // Add your image domains here
deviceSizes: [640, 750, 828, 1080, 1200], // Don't generate unused sizes
},
// Vercel enables this automatically, but good to be explicit
compress: true,
// This actually helps with bundle size (sometimes)
experimental: {
optimizePackageImports: ['@mui/material', 'lodash', 'date-fns'],
serverComponentsExternalPackages: ['mongoose'], // If you use Mongoose
},
// Add this or your builds will randomly fail
typescript: {
ignoreBuildErrors: false, // Set to true if you're desperate
},
// ESLint can break builds in CI/CD
eslint: {
ignoreDuringBuilds: false, // Set to true if you hate yourself
},
};
export default nextConfig;
Issues that will break your builds:
- Missing image domains - Next.js Image optimization needs explicit domain allowlists
- Mongoose/MongoDB connections - Add to
serverComponentsExternalPackages
or it'll crash - TypeScript errors - Fix them locally, don't ignore them in production
- ESLint config - Your local rules might be different from Vercel's environment
Monitoring: Because Your Users Will Find the Bugs First
// app/layout.tsx - Add this or you'll deploy blind
import { SpeedInsights } from '@vercel/speed-insights/next';
import { Analytics } from '@vercel/analytics/next';
export default function RootLayout({ children }) {
return (
<html lang=\"en\">
<body>
{children}
<SpeedInsights />
<Analytics />
</body>
</html>
);
}
Reality check: Vercel's analytics are decent but not free after your first 100k pageviews. They'll bill you. I recommend Plausible or PostHog for honest analytics without the Google privacy nightmare.
Error monitoring that actually works: Add Sentry or you'll be debugging production errors from angry user emails:
npm install @sentry/nextjs
npx @sentry/wizard -i nextjs
Database Connection Issues
Serverless + Database = Complexity. Traditional connection pooling doesn't work because each Vercel function is a separate container. Here's what actually works:
If you use Prisma (most common):
{
\"scripts\": {
\"db:migrate\": \"prisma migrate deploy\",
\"db:generate\": \"prisma generate\",
\"build\": \"prisma generate && prisma migrate deploy && next build\",
\"postinstall\": \"prisma generate\"
}
}
Connection pooling for PostgreSQL: Use PgBouncer or switch to Neon, PlanetScale, or Supabase. Regular Postgres will exhaust connections and crash your functions.
MongoDB users: Use connection pooling or your app will randomly fail with MongoNetworkError
. I spent an entire weekend debugging this because the error only happened under load - worked fine with one user, died with five.
Third-Party Services: The Expensive Surprise
Payment processing gotchas:
- Stripe webhook URLs need to be updated to your production domain
- Don't forget to switch from test keys to live keys (I've done this more than once)
- Webhook endpoints need to be HTTPS in production
Email services that actually work in serverless:
- Resend - built for this
- SendGrid - reliable but expensive
- Postmark - good deliverability
- Don't use SMTP - it times out in serverless functions
File upload pain: Vercel functions have a 6MB request limit. Use Upstash or upload directly to S3/Cloudinary from the client.
Security Basics
Skip Content Security Policy unless you actually understand it. Most tutorials get it wrong and break your app. Here's what actually matters:
Security headers that won't break your app:
// next.config.mjs
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
// Don't add CSP here unless you know what you're doing
],
},
];
},
};
Actually important security stuff:
- Don't commit secrets to git - use
.env.local
and add it to.gitignore
- Validate API inputs - use Zod or your endpoints will be exploited
- Rate limit your API routes - use Upstash Redis for simple rate limiting
- Use HTTPS everywhere - Vercel handles this but check your external API calls
Pre-Flight Checklist (Do This or Suffer)
Manual testing that catches 90% of issues:
- Run the production build locally -
npm run build && npm start
- Check all your forms - do they actually submit?
- Test authentication flows - login/logout/signup
- Click every navigation link - are there any 404s?
- Open browser dev tools - any console errors?
- Test on mobile - responsive design breaks in weird ways
- Check your API routes with curl - do they return what you expect?
## Test your API endpoints locally first (replace <your-app-name> with your actual domain)
curl -X POST https://<your-app-name>.vercel.app/api/auth/signin \
-H \"Content-Type: application/json\" \
-d '{\"email\":\"test@test.com\",\"password\":\"test\"}'
Bundle analysis (because Vercel will charge you):
## Check your bundle sizes
npm run build -- --analyze
open .next/analyze/client.html
## If any chunk is over 200kb, your users will hate you
Issues that often surface after deployment:
- Images that work locally but fail in production (wrong domains)
- API routes that timeout because you didn't test them with real data
- Authentication redirects that break because of environment URLs
- Database connections that worked with 1 user, fail with 10
I've learned to test everything thoroughly before deploying because debugging production issues at midnight is not fun. Trust the checklist.