Skip to content
~/sph.sh

Next.js Deployment Alternatives to Vercel: A Comprehensive Guide

A comprehensive guide to deploying Next.js applications beyond Vercel, with practical cost analysis, implementation details, and migration strategies for production environments

Ever found yourself staring at a Vercel invoice wondering how a side project suddenly costs more than your Netflix subscription? Or maybe you're evaluating deployment options for a Next.js application and wondering if there's life beyond Vercel's platform? Working with production migrations and deployment optimizations has taught me that the alternatives are viable and often superior for specific use cases.

Let me share what I've discovered about deploying Next.js applications without Vercel, including the gotchas nobody mentions in the documentation and the real costs you'll encounter in production.

The Context - Why Teams Are Looking Beyond Vercel

Working with Next.js applications has taught me that while Vercel offers an excellent developer experience, several factors drive teams to explore alternatives:

  1. Vendor Lock-in Concerns: Vercel's platform-specific APIs and deployment patterns create dependencies that make future migrations challenging. Teams find themselves tied to proprietary features that don't translate to other platforms.

  2. Single Point of Dependency: Relying on one vendor for critical infrastructure introduces risk. When Vercel experiences outages or changes their pricing model, teams have limited recourse.

  3. Cost at Scale: The platform charges 150perterabyteofbandwidthbeyondtheincludedquota(150 per terabyte of bandwidth beyond the included quota (0.15/GB in overage fees), and function invocations can accumulate rapidly. I've observed situations where marketing campaigns drive 10x normal traffic, revealing function invocation limits precisely when conversion potential is highest - a pattern that highlights the importance of understanding platform limits before scaling events.

Additional factors that influence the decision:

  • Need for specific regional compliance or data residency
  • Desire to leverage existing cloud infrastructure investments
  • Requirements for custom caching rules or deployment configurations
  • Budget constraints that don't align with Vercel's pricing tiers

Analysis Framework - Evaluating Deployment Options

Before diving into specific platforms, here's the framework I use to evaluate Next.js deployment options:

Managed Platform Alternatives

AWS Amplify - The Enterprise-Ready Choice

AWS Amplify has matured significantly for Next.js deployments. Here's what a production configuration looks like:

yaml
# amplify.ymlversion: 1frontend:  phases:    preBuild:      commands:        - npm ci --cache .npm --prefer-offline        # Fix for sharp/image optimization        - npm install --os=linux --cpu=x64 sharp    build:      commands:        - npm run build  artifacts:    baseDirectory: .next    files:      - '**/*'  cache:    paths:      - .npm/**/*      - node_modules/**/*
# Custom cache configurationcustomHeaders:  - pattern: '**/*'    headers:      - key: 'Cache-Control'        value: 'public, max-age=31536000, immutable'  - pattern: '**/*.html'    headers:      - key: 'Cache-Control'        value: 'public, max-age=0, must-revalidate'

Key Implementation Details:

  • Build minutes cost $0.01 each (typical builds: 2-4 minutes)
  • Bandwidth pricing: $0.15/GB after 15GB free tier
  • Supports large numbers of redirects, though console performance degrades with thousands of rules
  • Automatic branch deployments for pull requests

Real-World Gotcha: The redirect console performance issue isn't documented. This becomes apparent when migrating legacy applications with 2,000+ redirects. The solution? Implement redirects at the application level using middleware:

typescript
// middleware.tsimport { NextResponse } from 'next/server';import type { NextRequest } from 'next/server';
// Load redirects from a JSON file or databaseimport redirects from './redirects.json';
export function middleware(request: NextRequest) {  const pathname = request.nextUrl.pathname;
  // Check if pathname needs redirect  const redirect = redirects[pathname];  if (redirect) {    return NextResponse.redirect(      new URL(redirect.destination, request.url),      redirect.permanent ? 308 : 307    );  }
  return NextResponse.next();}
export const config = {  matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)',};

Cloudflare Pages - The Cost-Effective Powerhouse

Cloudflare Pages with the OpenNext adapter has become surprisingly capable. The implementation has two approaches:

Option 1: Edge-Only Runtime (Limited but Fast)

bash
npm install @cloudflare/next-on-pages

Option 2: Full Node.js Support (Recommended)

bash
npm install @opennextjs/cloudflare

Here's a production-ready configuration using OpenNext:

javascript
// next.config.jsconst { withOpenNextConfig } = require('@opennextjs/cloudflare/next-config');
const nextConfig = {  output: 'standalone',  images: {    remotePatterns: [      {        protocol: 'https',        hostname: '**.cloudinary.com',      },    ],  },  experimental: {    // Enable edge runtime for specific routes    runtime: 'edge',  },};
module.exports = withOpenNextConfig(nextConfig);
toml
# wrangler.tomlname = "nextjs-production"compatibility_date = "2024-01-01"compatibility_flags = ["nodejs_compat"]
[site]bucket = "./.vercel/output/static"
[env.production]vars = { ENVIRONMENT = "production" }
[[d1_databases]]binding = "DB"database_name = "production"database_id = "your-database-id"
[[kv_namespaces]]binding = "CACHE"id = "your-kv-namespace-id"

Cost Analysis:

  • Bandwidth: Unlimited (no marketing speak)
  • Requests: 100,000/day free, then $0.50 per million
  • Workers: 100,000 requests/day free
  • Total monthly cost for most applications: $0

Netlify - The Developer-Friendly Middle Ground

Netlify's Next.js support has improved dramatically. Here's a configuration that handles complex requirements:

toml
# netlify.toml[build]  command = "npm run build"  publish = ".next"
[[plugins]]  package = "@netlify/plugin-nextjs"
[build.environment]  NEXT_USE_NETLIFY_EDGE = "true"  NETLIFY_NEXT_PLUGIN_SKIP = "false"
# Function configuration for API routes[functions]  directory = "netlify/functions"  included_files = ["data/**"]
# Redirect rules with splat support[[redirects]]  from = "/old-blog/*"  to = "/posts/:splat"  status = 301  force = true
# Custom headers for security[[headers]]  for = "/*"  [headers.values]    X-Frame-Options = "DENY"    X-Content-Type-Options = "nosniff"    X-XSS-Protection = "1; mode=block"

Pro Tip: Netlify's form handling and split testing features work seamlessly with Next.js, something that requires additional setup on other platforms.

Self-Hosting Solutions - Maximum Control

SST on AWS - Serverless Done Right

SST (formerly Serverless Stack) provides the best serverless deployment experience for Next.js. Here's a complete production setup:

typescript
// sst.config.tsimport { SSTConfig } from "sst";import { NextjsSite, Bucket, Table } from "sst/constructs";
export default {  config(_input) {    return {      name: "nextjs-production",      region: "us-east-1",    };  },  stacks(app) {    app.stack(function Site({ stack }) {      // DynamoDB for session storage      const table = new Table(stack, "sessions", {        fields: {          sessionId: "string",        },        primaryIndex: { partitionKey: "sessionId" },      });
      // S3 for uploads      const bucket = new Bucket(stack, "uploads", {        cors: [          {            maxAge: "1 day",            allowedOrigins: ["*"],            allowedHeaders: ["*"],            allowedMethods: ["GET", "PUT", "POST", "DELETE", "HEAD"],          },        ],      });
      // Next.js site      const site = new NextjsSite(stack, "site", {        customDomain: {          domainName: "example.com",          hostedZone: "example.com",        },        environment: {          DATABASE_URL: process.env.DATABASE_URL,          SESSION_TABLE_NAME: table.tableName,          UPLOAD_BUCKET_NAME: bucket.bucketName,        },        bind: [table, bucket],        // Performance optimizations        memorySize: 1024,        timeout: "30 seconds",        // Regional configuration        regional: {          enableServerUrlIamAuth: true,        },      });
      stack.addOutputs({        SiteUrl: site.url,        CloudFrontUrl: site.cdk.distribution.distributionDomainName,      });    });  },} satisfies SSTConfig;

Cost Breakdown for 1M requests/month:

  • Lambda: ~$20 (including free tier)
  • CloudFront: ~$10 for bandwidth
  • S3: ~$1 for storage
  • Total: ~31/month(comparedto31/month (compared to 320 on Vercel for similar traffic)

Docker + VPS - The Budget Champion

For teams comfortable with server management, self-hosting on Hetzner or DigitalOcean provides unbeatable value. Here's a production-grade Docker setup:

dockerfile
# Dockerfile# DependenciesFROM node:20-alpine AS depsRUN apk add --no-cache libc6-compatWORKDIR /app
# Install dependencies based on lockfileCOPY package.json package-lock.json* ./RUN \  if [ -f package-lock.json ]; then npm ci --omit=dev; \  else echo "Lockfile not found." && exit 1; \  fi
# BuilderFROM node:20-alpine AS builderRUN apk add --no-cache libc6-compatWORKDIR /appCOPY --from=deps /app/node_modules ./node_modulesCOPY . .
# Build applicationENV NEXT_TELEMETRY_DISABLED 1RUN npm run build
# RunnerFROM node:20-alpine AS runnerWORKDIR /app
ENV NODE_ENV productionENV NEXT_TELEMETRY_DISABLED 1
# Create non-root userRUN addgroup --system --gid 1001 nodejsRUN adduser --system --uid 1001 nextjs
# Copy built applicationCOPY --from=builder /app/public ./publicCOPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjsEXPOSE 3000ENV PORT 3000
CMD ["node", "server.js"]
yaml
# docker-compose.ymlversion: '3.8'
services:  nextjs:    build: .    restart: unless-stopped    environment:      - NODE_ENV=production      - DATABASE_URL=${DATABASE_URL}    healthcheck:      test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]      interval: 30s      timeout: 10s      retries: 3    networks:      - app-network
  nginx:    image: nginx:alpine    restart: unless-stopped    ports:      - "80:80"      - "443:443"    volumes:      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro      - ./nginx/ssl:/etc/nginx/ssl:ro      - ./nginx/cache:/var/cache/nginx    depends_on:      - nextjs    networks:      - app-network
  # Optional: Redis for caching  redis:    image: redis:alpine    restart: unless-stopped    command: redis-server --appendonly yes    volumes:      - redis-data:/data    networks:      - app-network
networks:  app-network:    driver: bridge
volumes:  redis-data:

Nginx Configuration for Production:

nginx
# nginx.confupstream nextjs {    server nextjs:3000;}
server {    listen 80;    server_name example.com;    return 301 https://$server_name$request_uri;}
server {    listen 443 ssl http2;    server_name example.com;
    ssl_certificate /etc/nginx/ssl/cert.pem;    ssl_certificate_key /etc/nginx/ssl/key.pem;
    # Security headers    add_header X-Frame-Options "SAMEORIGIN" always;    add_header X-Content-Type-Options "nosniff" always;    add_header X-XSS-Protection "1; mode=block" always;
    # Cache static assets    location /_next/static {        proxy_pass http://nextjs;        proxy_cache_valid 365d;        add_header Cache-Control "public, immutable";    }
    # Cache images    location /_next/image {        proxy_pass http://nextjs;        proxy_cache_valid 365d;        add_header Cache-Control "public, max-age=31536000, immutable";    }
    # Everything else    location / {        proxy_pass http://nextjs;        proxy_http_version 1.1;        proxy_set_header Upgrade $http_upgrade;        proxy_set_header Connection 'upgrade';        proxy_set_header Host $host;        proxy_cache_bypass $http_upgrade;        proxy_set_header X-Real-IP $remote_addr;        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;        proxy_set_header X-Forwarded-Proto $scheme;    }}

Platform-as-a-Service - Coolify and Alternatives

Coolify has emerged as a powerful self-hosted alternative to Vercel. Installation on a fresh VPS:

bash
# Install Coolify on Ubuntu/Debiancurl -fsSL https://coolify.io/install.sh | bash
# Or using Docker directlydocker run --rm -it \  -v /var/run/docker.sock:/var/run/docker.sock \  -v coolify:/data \  -e COOLIFY_APP_ID=your-app-id \  -p 8000:80 \  -p 8443:443 \  -p 3000:3000 \  coolify/coolify:latest

Coolify Configuration for Next.js:

yaml
# coolify.yamlname: nextjs-apptype: nodeport: 3000build:  command: npm run build  install: npm cistart:  command: npm startenv:  NODE_ENV: production  DATABASE_URL: ${DATABASE_URL}health:  path: /api/health  interval: 30resources:  limits:    memory: 1Gi    cpu: 1000m  requests:    memory: 512Mi    cpu: 500m

Real-World Cost Comparison

Based on actual production deployments, here's what you can expect to pay monthly:

Note: These costs are based on applications serving approximately 500GB bandwidth per month with moderate compute requirements.

PlatformSmall App (10GB/month)Medium App (500GB/month)Large App (2TB/month)Notes
Vercel$20$80$320Predictable but expensive at scale
Netlify$0 (free tier)$20$95+Better predictability than Vercel
Cloudflare Pages$0$0$0Unlimited bandwidth
AWS Amplify~$5~$30~$70Pay-as-you-go model
Hetzner + CloudflareEUR3.79EUR3.79EUR3.79Fixed cost regardless of traffic
SST on AWS~$10~$20-40~$50-100Varies by usage patterns
DigitalOcean Apps$5$25$100Simple pricing structure

Migration Strategy - Week-by-Week Approach

Week 1 - Assessment and Planning

Start by analyzing your current Vercel usage:

javascript
// scripts/analyze-vercel-usage.jsconst { VercelClient } = require('@vercel/client');
async function analyzeUsage() {  const client = new VercelClient({ token: process.env.VERCEL_TOKEN });
  // Get bandwidth usage  const bandwidth = await client.get('/v1/usage/bandwidth');
  // Get function usage  const functions = await client.get('/v1/usage/functions');
  // Get build minutes  const builds = await client.get('/v1/usage/builds');
  console.log({    monthlyBandwidth: bandwidth.total,    functionInvocations: functions.total,    buildMinutes: builds.total,    estimatedCost: calculateCost(bandwidth, functions, builds)  });}
function calculateCost(bandwidth, functions, builds) {  // Implement Vercel pricing calculation  // This helps understand your baseline costs}

Week 2 - Proof of Concept

Deploy a minimal version to your chosen platform:

bash
# Example: Testing Cloudflare Pages deploymentnpx create-next-app@latest test-deploymentcd test-deployment
# Add OpenNext adapternpm install @opennextjs/cloudflare
# Configure and deploynpx wrangler pages deploy .next

Week 3 - Production Preparation

Implement monitoring and observability:

typescript
// lib/monitoring.tsimport { metrics } from '@opentelemetry/api-metrics';
const meter = metrics.getMeter('nextjs-app', '1.0.0');
// Create custom metricsconst requestDuration = meter.createHistogram('http_request_duration', {  description: 'Duration of HTTP requests in milliseconds',  unit: 'ms',});
const deploymentCost = meter.createGauge('deployment_cost', {  description: 'Estimated deployment cost in USD',  unit: 'USD',});
export function trackRequest(route: string, duration: number) {  requestDuration.record(duration, { route });}
export function updateCost(platform: string, cost: number) {  deploymentCost.record(cost, { platform });}

Week 4 - Migration and Validation

Execute the migration with a rollback strategy:

yaml
# .github/workflows/deploy-with-rollback.ymlname: Deploy with Rollback
on:  push:    branches: [main]
jobs:  deploy:    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v3
      - name: Backup current deployment        run: |          # Save current deployment info for rollback          echo ${{ github.sha }} > .last-known-good
      - name: Deploy to new platform        run: |          # Your deployment commands here          npm run deploy:production
      - name: Health check        id: health        run: |          # Verify deployment is healthy          curl -f https://your-app.com/api/health || exit 1
      - name: Rollback on failure        if: failure()        run: |          # Rollback to previous version          LAST_GOOD=$(cat .last-known-good)          npm run deploy:rollback $LAST_GOOD

Common Pitfalls and Solutions

The Sharp/Image Optimization Challenge

Almost every platform struggles with Next.js image optimization. Here's the universal solution:

javascript
// next.config.jsmodule.exports = {  images: {    loader: 'custom',    loaderFile: './lib/image-loader.js',  },};
// lib/image-loader.jsexport default function cloudinaryLoader({ src, width, quality }) {  const params = ['f_auto', 'c_limit', `w_${width}`, `q_${quality || 'auto'}`];  const paramsString = params.join(',');  return `https://res.cloudinary.com/your-cloud-name/image/upload/${paramsString}/${src}`;}

Environment Variable Management

Different platforms handle environment variables differently. Here's a unified approach:

typescript
// lib/config.tsinterface Config {  database: {    url: string;    poolSize: number;  };  redis: {    url: string;  };  platform: 'vercel' | 'amplify' | 'cloudflare' | 'self-hosted';}
function detectPlatform(): Config['platform'] {  if (process.env.VERCEL) return 'vercel';  if (process.env.AWS_REGION) return 'amplify';  if (process.env.CF_PAGES) return 'cloudflare';  return 'self-hosted';}
export const config: Config = {  database: {    url: process.env.DATABASE_URL!,    poolSize: detectPlatform() === 'self-hosted' ? 20 : 1,  },  redis: {    url: process.env.REDIS_URL || 'redis://localhost:6379',  },  platform: detectPlatform(),};

ISR Cache Behavior Differences

Incremental Static Regeneration behaves differently across platforms:

typescript
// pages/api/revalidate.tsimport { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(  req: NextApiRequest,  res: NextApiResponse) {  const { path, platform } = req.query;
  try {    switch (platform) {      case 'vercel':        await res.revalidate(path as string);        break;
      case 'cloudflare':        // Cloudflare KV-based revalidation        await fetch(`https://api.cloudflare.com/client/v4/zones/${process.env.CF_ZONE}/purge_cache`, {          method: 'POST',          headers: {            'Authorization': `Bearer ${process.env.CF_TOKEN}`,            'Content-Type': 'application/json',          },          body: JSON.stringify({            files: [`https://example.com${path}`],          }),        });        break;
      case 'aws':        // CloudFront invalidation        const cloudfront = new AWS.CloudFront();        await cloudfront.createInvalidation({          DistributionId: process.env.CF_DISTRIBUTION_ID,          InvalidationBatch: {            CallerReference: Date.now().toString(),            Paths: {              Quantity: 1,              Items: [path as string],            },          },        }).promise();        break;    }
    res.status(200).json({ revalidated: true });  } catch (err) {    res.status(500).json({ error: 'Failed to revalidate' });  }}

Performance Comparison - Real-World Metrics

After migrating multiple production applications, here are the actual performance metrics observed:

Note: These metrics are from applications serving 100-500 requests per second with mixed static and dynamic content.

Time to First Byte (TTFB):

  • Vercel: 45-60ms (global average)
  • Cloudflare Pages: 25-40ms (best-in-class)
  • AWS with CloudFront: 50-80ms
  • Self-hosted with Cloudflare: 60-100ms
  • Direct VPS (no CDN): 100-300ms

Cold Start Times:

  • Vercel Functions: 100-300ms
  • AWS Lambda (SST): 400-800ms
  • Cloudflare Workers: 0-500ms (marketed as "zero cold starts" but users report 100-500ms in practice)
  • Container-based: 0ms (always warm)

Build Times:

  • Vercel: 2-3 minutes
  • Netlify: 3-4 minutes
  • AWS Amplify: 3-5 minutes
  • GitHub Actions to VPS: 1-2 minutes

Recommendations Based on Use Case

For Startups and MVPs

Recommendation: Cloudflare Pages with OpenNext

  • Zero bandwidth costs removes a major scaling concern
  • Free tier handles most startup traffic
  • Global performance out of the box

For Enterprise Applications

Recommendation: SST on AWS

  • Full AWS service integration
  • Infrastructure as code for compliance
  • Predictable costs with reserved capacity

For High-Traffic Content Sites

Recommendation: Self-hosted with Cloudflare CDN

  • Fixed monthly costs regardless of traffic
  • Complete control over caching strategy
  • No vendor lock-in

For Agencies and Freelancers

Recommendation: Coolify on Hetzner

  • Host unlimited client projects on one VPS
  • Simple deployment interface for clients
  • Cost-effective at EUR3.79/month per server

What I'd Do Differently Today

Looking back at various migrations, here's what I've learned:

Start with OpenNext Compatibility: Design your application to work with OpenNext from day one. This provides maximum flexibility for platform switches without code changes.

Implement Platform-Agnostic Monitoring: Use OpenTelemetry or similar vendor-neutral observability tools rather than platform-specific solutions. This makes migrations much smoother.

Build Cost Tracking Early: Implement cost tracking from the beginning:

typescript
// lib/cost-tracker.tsclass DeploymentCostTracker {  private costs: Map<string, number> = new Map();
  track(service: string, amount: number) {    const current = this.costs.get(service) || 0;    this.costs.set(service, current + amount);  }
  async reportDaily() {    const total = Array.from(this.costs.values()).reduce((a, b) => a + b, 0);
    // Send to monitoring service    await fetch('/api/metrics', {      method: 'POST',      body: JSON.stringify({        date: new Date().toISOString(),        costs: Object.fromEntries(this.costs),        total,      }),    });
    // Reset for next day    this.costs.clear();  }}

Design for Multi-Platform Deployment: Maintain deployment configurations for multiple platforms. This provides negotiating power with vendors and quick disaster recovery options.

Key Takeaways

After migrating numerous Next.js applications away from Vercel, here's what consistently proves true:

  1. The alternatives are production-ready. Every major Next.js feature now works on alternative platforms thanks to OpenNext and improved platform support.

  2. Cost savings can be significant. Teams often reduce their monthly deployment costs by 70-90% while maintaining or improving performance.

  3. Platform lock-in is avoidable. With proper architecture decisions, switching platforms can be accomplished in days, not weeks.

  4. Self-hosting is more accessible than ever. Tools like Coolify and Dokploy have democratized what once required significant DevOps expertise.

  5. There's no universal best choice. Your specific requirements - traffic patterns, team expertise, budget constraints - should drive the decision.

The landscape of Next.js deployment has evolved dramatically. Vercel remains an excellent platform, but it's no longer the only viable option for production deployments. Whether you're optimizing costs, seeking more control, or aligning with existing infrastructure, there's likely a deployment strategy that better fits your needs.

Choose based on your actual requirements, not on what's popular. And remember - with OpenNext and modern deployment tools, you can always change your mind later.

Related Posts