BlogTutorialsDeploy Next.js 15 to Production: Complete Hosting Guide 2025

Deploy Next.js 15 to Production: Complete Hosting Guide 2025

Adrian Silaghi
Adrian Silaghi
December 25, 2025
14 min read
13 views
#nextjs #deployment #docker #postgresql #redis #production #vercel-alternative

Next.js 15 is the most powerful React framework for production applications. But deploying it beyond Vercel can seem daunting. This guide shows you exactly how to run Next.js 15 in production on your own infrastructure—with full control over your data and costs.

We'll cover two deployment approaches:

  1. VPS with Docker: Full control, predictable costs
  2. Serverless Containers: Auto-scaling, pay-per-use

By the end, you'll have a production-ready Next.js 15 deployment with PostgreSQL, Redis, and SSL—all for a fraction of Vercel's pricing.

Why Self-Host Next.js?

Before we dive in, let's address the elephant in the room: why not just use Vercel?

Vercel Limitations

  • Pricing surprises: Function invocations, bandwidth, and build minutes add up fast
  • Vendor lock-in: Edge middleware, Image optimization, and other features are Vercel-specific
  • Data residency: Limited control over where your data lives
  • Resource limits: Function timeouts, memory limits, and cold starts
  • Database costs: Vercel Postgres/KV are expensive compared to managed alternatives

Self-Hosting Benefits

  • Predictable costs: Fixed monthly pricing, no surprises
  • Full control: Choose your database, cache, storage
  • No limits: No function timeouts, no memory caps
  • Data sovereignty: Keep data in your chosen region
  • Cost savings: 50-80% cheaper for most workloads

Cost Comparison

Scenario Vercel Pro DanubeData
Base platform $20/month €8.99/month (VPS)
Database (PostgreSQL) $20-60/month €19.99/month
Cache (Redis/KV) $10-30/month €4.99/month
100GB bandwidth $15-55/month Included
Monthly Total $65-165 €33.97

Prerequisites

Before starting, make sure you have:

  • A Next.js 15 application ready to deploy
  • Basic familiarity with Docker and the command line
  • A DanubeData account (or any VPS provider)

Option 1: VPS Deployment with Docker

This is the most reliable approach for production. You get consistent performance, no cold starts, and complete control.

Step 1: Create Your VPS

Log into DanubeData and create a VPS:

  • Size: Small (2 vCPU, 4GB RAM) is perfect for most apps
  • Image: Ubuntu 24.04 LTS
  • Add your SSH key

Step 2: Prepare Your Next.js App

First, ensure your Next.js app is configured for standalone output:

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "standalone",
  // Enable if using Docker
  experimental: {
    // Recommended for Docker deployments
  },
};

export default nextConfig;

Step 3: Create Dockerfile

Create a production-optimized Dockerfile:

# Dockerfile
FROM node:20-alpine AS base

# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN 
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; 
  elif [ -f package-lock.json ]; then npm ci; 
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; 
  else echo "Lockfile not found." && exit 1; 
  fi

# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Environment variables for build
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL

RUN 
  if [ -f yarn.lock ]; then yarn run build; 
  elif [ -f package-lock.json ]; then npm run build; 
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; 
  else echo "Lockfile not found." && exit 1; 
  fi

# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

ENV NODE_ENV=production

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public

# Automatically leverage output traces to reduce image size
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

CMD ["node", "server.js"]

Step 4: Create Docker Compose Stack

Create a complete production stack with PostgreSQL and Redis:

# docker-compose.yml
services:
  app:
    build:
      context: .
      args:
        - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
    restart: always
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://postgres:${DB_PASSWORD}@db:5432/myapp
      - REDIS_URL=redis://cache:6379
      - NEXTAUTH_URL=${NEXTAUTH_URL}
      - NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    networks:
      - app-network

  db:
    image: postgres:16-alpine
    restart: always
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=myapp
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - app-network

  cache:
    image: redis:7-alpine
    restart: always
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes
    networks:
      - app-network

  nginx:
    image: nginx:alpine
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certbot/conf:/etc/letsencrypt:ro
      - ./certbot/www:/var/www/certbot:ro
    depends_on:
      - app
    networks:
      - app-network

  certbot:
    image: certbot/certbot
    volumes:
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot

volumes:
  postgres_data:
  redis_data:

networks:
  app-network:
    driver: bridge

Step 5: Configure Nginx

Create an Nginx configuration for SSL and reverse proxy:

# nginx.conf
events {
    worker_connections 1024;
}

http {
    upstream nextjs {
        server app:3000;
    }

    # Redirect HTTP to HTTPS
    server {
        listen 80;
        server_name your-domain.com www.your-domain.com;

        location /.well-known/acme-challenge/ {
            root /var/www/certbot;
        }

        location / {
            return 301 https://$host$request_uri;
        }
    }

    # HTTPS server
    server {
        listen 443 ssl http2;
        server_name your-domain.com www.your-domain.com;

        ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;

        ssl_session_timeout 1d;
        ssl_session_cache shared:SSL:50m;
        ssl_session_tickets off;

        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
        ssl_prefer_server_ciphers off;

        # HSTS
        add_header Strict-Transport-Security "max-age=63072000" always;

        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_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;
            proxy_cache_bypass $http_upgrade;
        }

        # Cache static assets
        location /_next/static {
            proxy_pass http://nextjs;
            proxy_cache_valid 200 365d;
            add_header Cache-Control "public, max-age=31536000, immutable";
        }

        location /static {
            proxy_pass http://nextjs;
            proxy_cache_valid 200 365d;
            add_header Cache-Control "public, max-age=31536000, immutable";
        }
    }
}

Step 6: Create Environment File

# .env.production
DB_PASSWORD=your-secure-password-here
NEXT_PUBLIC_API_URL=https://your-domain.com
NEXTAUTH_URL=https://your-domain.com
NEXTAUTH_SECRET=generate-with-openssl-rand-base64-32

Step 7: Deploy to VPS

# SSH into your VPS
ssh root@your-vps-ip

# Install Docker and Docker Compose
curl -fsSL https://get.docker.com | sh
apt install docker-compose-plugin -y

# Clone your repo
git clone https://github.com/your-repo/your-app.git
cd your-app

# Copy your .env.production
nano .env.production

# Initial SSL certificate (run once)
docker compose run --rm certbot certonly 
  --webroot --webroot-path=/var/www/certbot 
  -d your-domain.com -d www.your-domain.com

# Build and start
docker compose up -d --build

# Check logs
docker compose logs -f app

Step 8: Set Up Auto-Renewal

# Add to crontab (crontab -e)
0 3 * * * cd /path/to/app && docker compose run --rm certbot renew && docker compose restart nginx

Option 2: DanubeData Managed Services

For an even simpler setup, use DanubeData's managed PostgreSQL and Redis instead of running them in Docker:

Benefits of Managed Services

  • Automatic backups and point-in-time recovery
  • SSL certificates included
  • Monitoring and alerting
  • Easy scaling without downtime
  • No database admin required

Simplified Docker Compose

# docker-compose.yml (with managed services)
services:
  app:
    build:
      context: .
      args:
        - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
    restart: always
    ports:
      - "3000:3000"
    environment:
      # Connect to DanubeData managed services
      - DATABASE_URL=${DANUBEDATA_DATABASE_URL}
      - REDIS_URL=${DANUBEDATA_REDIS_URL}
      - NEXTAUTH_URL=${NEXTAUTH_URL}
      - NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
    networks:
      - app-network

  nginx:
    image: nginx:alpine
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certbot/conf:/etc/letsencrypt:ro
      - ./certbot/www:/var/www/certbot:ro
    depends_on:
      - app
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

Then in your DanubeData dashboard:

  1. Create a PostgreSQL database (Small: €19.99/mo)
  2. Create a Redis cache (Micro: €4.99/mo)
  3. Copy the connection strings to your .env file
  4. Add your VPS IP to the firewall rules

Option 3: Serverless Containers (Auto-Scaling)

For applications with variable traffic, serverless containers offer automatic scaling and pay-per-use pricing.

Deploy with Git Integration

  1. In DanubeData dashboard, create a new Serverless Container
  2. Connect your GitHub/GitLab repository
  3. Select the branch to deploy
  4. Configure environment variables
  5. Deploy automatically on every push

Serverless Dockerfile Optimization

# Dockerfile.serverless
FROM node:20-alpine AS base

FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN npm ci

FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

# Serverless containers use PORT env variable
EXPOSE 8080
ENV PORT=8080
ENV HOSTNAME="0.0.0.0"

# Minimal memory footprint for fast cold starts
CMD ["node", "server.js"]

Database Setup with Prisma

Most Next.js apps use Prisma for database access. Here's how to set it up properly:

Install Prisma

npm install prisma @prisma/client
npx prisma init

Configure Schema

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  posts     Post[]
}

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

Create Prisma Client Singleton

// lib/prisma.ts
import { PrismaClient } from "@prisma/client";

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};

export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({
    log: process.env.NODE_ENV === "development" ? ["query"] : [],
  });

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

Run Migrations in Production

# In your deployment script or CI/CD
npx prisma migrate deploy

# Or in Docker
docker compose exec app npx prisma migrate deploy

Redis Caching Setup

Add Redis caching for optimal performance:

Install Redis Client

npm install ioredis

Create Redis Client

// lib/redis.ts
import Redis from "ioredis";

const getRedisUrl = () => {
  if (process.env.REDIS_URL) {
    return process.env.REDIS_URL;
  }
  throw new Error("REDIS_URL is not defined");
};

export const redis = new Redis(getRedisUrl());

Cache API Responses

// app/api/posts/route.ts
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { redis } from "@/lib/redis";

export async function GET() {
  const cacheKey = "posts:all";

  // Check cache first
  const cached = await redis.get(cacheKey);
  if (cached) {
    return NextResponse.json(JSON.parse(cached));
  }

  // Query database
  const posts = await prisma.post.findMany({
    where: { published: true },
    include: { author: { select: { name: true } } },
  });

  // Cache for 5 minutes
  await redis.setex(cacheKey, 300, JSON.stringify(posts));

  return NextResponse.json(posts);
}

Production Optimization

1. Enable Compression

// next.config.ts
const nextConfig: NextConfig = {
  output: "standalone",
  compress: true,
};

2. Optimize Images

// next.config.ts
const nextConfig: NextConfig = {
  output: "standalone",
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "your-cdn.com",
      },
    ],
    // Use sharp for better performance
    unoptimized: false,
  },
};

3. Configure Caching Headers

// next.config.ts
const nextConfig: NextConfig = {
  output: "standalone",
  async headers() {
    return [
      {
        source: "/:all*(svg|jpg|png|webp|avif)",
        headers: [
          {
            key: "Cache-Control",
            value: "public, max-age=31536000, immutable",
          },
        ],
      },
    ];
  },
};

4. Health Check Endpoint

// app/api/health/route.ts
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { redis } from "@/lib/redis";

export async function GET() {
  try {
    // Check database
    await prisma.$queryRaw`SELECT 1`;

    // Check Redis
    await redis.ping();

    return NextResponse.json({
      status: "healthy",
      timestamp: new Date().toISOString(),
    });
  } catch (error) {
    return NextResponse.json(
      { status: "unhealthy", error: String(error) },
      { status: 503 }
    );
  }
}

CI/CD with GitHub Actions

Automate deployments with GitHub Actions:

# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Deploy to VPS
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.VPS_SSH_KEY }}
          script: |
            cd /var/www/myapp
            git pull origin main
            docker compose build --no-cache
            docker compose up -d
            docker compose exec -T app npx prisma migrate deploy
            docker image prune -f

Monitoring and Logging

View Logs

# Real-time logs
docker compose logs -f app

# Last 100 lines
docker compose logs --tail=100 app

# Specific time range
docker compose logs --since="2025-01-01T00:00:00" app

Monitor Resources

# Container stats
docker stats

# Disk usage
docker system df

Complete Architecture Diagram

┌─────────────────────────────────────────────────────────────┐
│                         Internet                             │
└─────────────────────────────┬───────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    Nginx (SSL/Proxy)                        │
│                    Port 80 → 443                            │
└─────────────────────────────┬───────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    Next.js 15 App                           │
│                    Port 3000                                │
│  ┌─────────────────┐  ┌─────────────────┐                  │
│  │  SSR Pages      │  │  API Routes     │                  │
│  └────────┬────────┘  └────────┬────────┘                  │
└───────────┼────────────────────┼────────────────────────────┘
            │                    │
            ▼                    ▼
┌───────────────────┐  ┌───────────────────┐
│   PostgreSQL      │  │     Redis         │
│   (DanubeData)    │  │   (DanubeData)    │
│   €19.99/mo       │  │   €4.99/mo        │
└───────────────────┘  └───────────────────┘

Cost Summary

Component DanubeData Service Monthly Cost
Application Server VPS Small €8.99
Database PostgreSQL Small €19.99
Cache Redis Micro €4.99
Total €33.97/month

Compare this to Vercel Pro + database + caching at $80-150+/month.

Troubleshooting

Container Won't Start

# Check build logs
docker compose logs app

# Rebuild with no cache
docker compose build --no-cache app

# Shell into container
docker compose run --rm app sh

Database Connection Failed

# Check if database is running
docker compose ps db

# Test connection
docker compose exec db psql -U postgres -d myapp -c "SELECT 1"

# Check DATABASE_URL format
# postgresql://user:password@host:5432/database

SSL Certificate Issues

# Check certificate status
docker compose run --rm certbot certificates

# Force renewal
docker compose run --rm certbot renew --force-renewal

Next Steps

You now have a production-ready Next.js 15 deployment. Consider adding:

  • Error tracking: Sentry or similar
  • Analytics: Plausible, Umami, or Posthog
  • CDN: Cloudflare for global edge caching
  • Backups: Automated database backups (included with DanubeData)

Get Started Today

Ready to deploy your Next.js 15 app to production?

👉 Create your free DanubeData account

Deploy your first VPS, database, and cache in under 5 minutes. No credit card required for testing.

Full-stack Next.js hosting starting at just €33.97/month—a fraction of Vercel's pricing with complete control over your infrastructure.

Need help deploying? Contact our team for personalized assistance.

Share this article

Ready to Get Started?

Deploy your infrastructure in minutes with DanubeData's managed services.