Skip to main content
Next.js is a React-based full-stack web framework created by Vercel. It extends React with powerful features for server-side rendering, routing, data fetching, API endpoints, and performance optimization, all in a single unified framework. It’s designed to help you build production-ready web applications with high performance, great developer experience, and strong scalability. Next.js can be deployed using either Application Hosting (SSR) or Static Site Hosting (SSG). The primary difference lies in how you configure next.config.mjs, which determines whether pages are rendered at runtime or generated ahead of time.

Application Hosting

Choose Application Hosting (Server-Side Rendering) when your application requires server-side logic or dynamic behavior that static files cannot provide. SSR is ideal when your site includes:
  • Personalized user experiences, such as dashboards and profiles.
  • Real-time or frequently changing data that must be fetched on every request.
  • Dynamic routes that cannot be known ahead of time, making static pre-rendering impractical.
  • Authentication-dependent content, where access and rendering vary based on the user’s session or permissions.

Configuration

To configure your Next.js application for use with Sevalla:
  • Enable the output: 'standalone' setting in your next.config.js file. This option generates an optimized, self-contained production build that includes only the minimal files needed to run your application.
    // next.config.js
    module.exports = {
      output: 'standalone',
    };
    
  • Use Next.js Image Optimization and configure the appropriate image loader to ensure efficient and high-quality asset delivery.
  • Use opt-in caching with the use cache directive for dynamic content.
  • Enable compression and set appropriate caching headers to improve performance.

Containerization

Dockerfile

The build for Dockerfiles is fully customizable. The following is an example Dockerfile for Next.js:
# Dockerfile for Next.js on Sevalla

# Stage 1: Dependencies
FROM node:lts-alpine AS deps
WORKDIR /app

COPY package*.json ./
RUN npm ci

# Stage 2: Builder
FROM node:lts-alpine AS builder
WORKDIR /app

COPY --from=deps /app/node_modules ./node_modules
COPY . .

RUN npm run build

# Stage 3: Runner
FROM node:lts-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

# Create non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Copy necessary files
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

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

Nixpacks

You can customize the Nixpacks build process by defining a nixpacks.toml file and using Nixpacks-specific environment variables. This allows you to fine-tune how dependencies are installed, how your application is built, and which runtime settings are applied. The following is an example nixpacks.toml configuration:
[phases.setup]
nixPkgs = ["nodejs", "yarn"]

[phases.install]
cmds = ["yarn install --frozen-lockfile"]

[phases.build]
cmds = ["yarn build"]

[start]
cmd = "yarn start"
Nixpacks will automatically detect your project’s lock file and select the appropriate package manager during deployment. Additionally, you can specify the Node.js version used during the build by setting the NIXPACKS_NODE_VERSION environment variable.

Buildpacks

If you’re using Buildpacks, you cannot modify the underlying build phases directly or control dependencies. You must rely on the runtime environment that Buildpacks detects. You can influence the build process by adjusting the build script in your package.json. Buildpacks will run whatever command you specify under the build script. For example, the standard command used to compile a Next.js application is:
"build": "next build"
You can also add additional logic before the build runs, for example:
"build": "echo \"Hi mom!\" && next build"

CDN

Sevalla provides a premium, Cloudflare-powered CDN for Application Hosting at no additional cost. To get the most out of Sevalla’s CDN when deploying your Next.js application, we recommend the following best practices:
  • Enable the CDN for all production applications to ensure faster global delivery and reduced latency.
  • Use the Next.js <Image> component to take advantage of built-in image optimization and responsive delivery.
  • Purge the CDN cache after deploying critical updates to guarantee that users receive the latest content.
  • Next.js automatically versions built assets in _next/static/, this ensures that static assets are uniquely hashed and safe to cache indefinitely.
  • Organize static files in the public/ directory, allowing the CDN to efficiently serve commonly accessed assets.

Edge caching

Edge caching stores your Sevalla site cache on Cloudflare’s 260+ global data centers, delivering responses from the location nearest to each visitor for faster performance. To maximize the benefits of Sevalla’s Edge Caching for your Next.js application, we recommend the following best practices:
  • Use the use cache directive for opt-in caching, avoiding implicit caching behaviors.
  • Select appropriate cacheLife profiles based on how frequently your content changes.
  • Use updateTag() in Server Actions to immediately update cached content while maintaining read-your-writes consistency.
  • Use revalidateTag() with the required cacheLife profile to refresh cache selectively.
  • Set appropriate Cache-Control headers for API routes to balance speed with accuracy.
  • Combine Edge Caching with the CDN for a comprehensive, high-performance caching strategy.
  • Monitor cache efficiency using Cloudflare headers (e.g., cf-cache-status) to track hit rates.
  • Cache individual functions for more granular control over which parts of your application are cached.
Next.js 16 changes caching from implicit to opt-in, with the use cache directive.

Cache-Control

With Sevalla’s Cloudflare integration, Cache-Control headers are respected at the edge, giving you precise control over how content is cached and served globally. The following are some common directives you can use in your Next.js application:
  • public, s-maxage=3600 - Caches the response on the CDN for 1 hour, improving performance for frequently accessed content.
  • public, max-age=31536000, immutable - Ideal for versioned static assets (e.g., files in _next/static/), allowing them to be cached for up to 1 year with no revalidation.
  • private - Prevents CDN caching and ensures the response is only cached by the end user’s browser, for personalized or sensitive data.
Sevalla does not yet support the stale-while-revalidate Cache-Control directive. To prevent unexpected caching behavior, we recommend not using this directive in your API or asset caching settings.

Opt-in caching with use cache

// src/app/products/page.tsx
import { cacheLife } from "next/cache";

export default async function ProductsPage() {
  "use cache";
  cacheLife("hours"); // Built-in profile: seconds, minutes, hours, days, weeks, max

  const products = await fetchProducts();

  return (
    <div>
      {products.map((product) => (
        <div key={product.id}>{product.name}</div>
      ))}
    </div>
  );
}
Custom cache profiles
// next.config.mjs
export default {
  output: "standalone",
  cacheLife: {
    product: {
      stale: 3600, // 1 hour
      revalidate: 86400, // 1 day
      expire: 604800, // 1 week
    },
  },
};
// src/app/products/page.tsx
export default async function ProductsPage() {
  "use cache";
  cacheLife("product"); // Use custom profile

  const products = await fetchProducts();
  return <div>{/* ... */}</div>;
}

Caching individual functions

You can cache specific functions instead of entire components:
// src/lib/data.ts
import { cacheLife } from "next/cache";

export async function getPost(slug: string) {
  "use cache";
  cacheLife("hours");

  const post = await db.query("SELECT * FROM posts WHERE slug = $1", [slug]);
  return post;
}
// src/app/blog/[slug]/page.tsx
import { getPost } from "@/lib/data";

export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPost(slug); // Cached function
  return <article>{post.content}</article>;
}

API routes with edge caching

// src/app/api/products/route.ts
export async function GET() {
  const products = await fetchProducts();

  return Response.json(products, {
    headers: {
      "Cache-Control": "public, s-maxage=3600",
    },
  });
}

Cache invalidation APIs

Next.js 16 introduces new cache invalidation APIs: revalidateTag()- Now requires cacheLife profile:
// src/app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";

export async function POST(request: Request) {
  const { tag, secret } = await request.json();

  if (secret !== process.env.REVALIDATION_SECRET) {
    return Response.json({ message: "Invalid secret" }, { status: 401 });
  }

  try {
    // Next.js 16: Second parameter (cacheLife profile) is required
    revalidateTag(tag, "hours");
    return Response.json({ revalidated: true });
  } catch (err) {
    return Response.json({ message: "Error revalidating" }, { status: 500 });
  }
}
updateTag()- New server actions-only API:
// src/app/actions.ts
"use server";
import { updateTag } from "next/cache";

export async function updateProduct(productId: string, data: any) {
  // Update database
  await db.query("UPDATE products SET ... WHERE id = $1", [productId]);

  // Expire cache and immediately refresh data (read-your-writes)
  await updateTag(`product-${productId}`, "hours");

  return { success: true };
}
refresh()- New server actions-only API:
// src/app/actions.ts
"use server";
import { refresh } from "next/cache";

export async function refreshData() {
  // Refreshes only uncached data without touching cache layer
  refresh();
  return { success: true };
}
revalidatePath()- Still available:
// src/app/api/revalidate/route.ts
import { revalidatePath } from "next/cache";

export async function POST(request: Request) {
  const { path, secret } = await request.json();

  if (secret !== process.env.REVALIDATION_SECRET) {
    return Response.json({ message: "Invalid secret" }, { status: 401 });
  }

  try {
    revalidatePath(path);
    return Response.json({ revalidated: true });
  } catch (err) {
    return Response.json({ message: "Error revalidating" }, { status: 500 });
  }
}

Healthchecks and graceful shutdown

Ensure your application remains available during deployments by implementing health checks and graceful shutdown strategies:
  • Always implement health checks for production applications.
  • Keep checks lightweight; responses should complete in under 1 second.
  • Verify critical dependencies (e.g., databases, Redis) as part of the checks.
  • Return 200 for degraded states to allow deployments to continue smoothly.
  • Return 503 only for critical failures that require pod restarts.
  • Finish in-flight requests before terminating processes.
  • Set a shutdown timeout (recommended: 30 seconds).
  • Close connections cleanly (e.g., database pools, Redis) to prevent resource leaks.
  • Use a custom server if advanced shutdown control is needed.
  • Test deployments in staging with monitoring scripts to validate health checks and shutdown behavior.

Basic health check

// src/app/api/health/route.ts
export async function GET() {
  return Response.json(
    { status: "ok", timestamp: new Date().toISOString() },
    { status: 200 }
  );
}

Separate readiness and liveliness health checks

// src/app/api/health/ready/route.ts
// Readiness: Is the app ready to receive traffic?
import { pool } from "@/lib/db";

export async function GET() {
  try {
    // Check critical dependencies
    await pool.query("SELECT 1");

    return Response.json({ ready: true }, { status: 200 });
  } catch (error) {
    return Response.json(
      { ready: false, error: "Database unavailable" },
      { status: 503 }
    );
  }
}

Basic graceful shutdown

// src/server.ts (if using custom server)
import { createServer } from "http";
import { parse } from "url";
import next from "next";

const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();

const PORT = parseInt(process.env.PORT || "3000", 10);

app.prepare().then(() => {
  const server = createServer((req, res) => {
    const parsedUrl = parse(req.url!, true);
    handle(req, res, parsedUrl);
  });

  server.listen(PORT, () => {
    console.log(`> Ready on http://localhost:${PORT}`);
  });

  // Graceful shutdown
  const gracefulShutdown = (signal: string) => {
    console.log(`Received ${signal}, starting graceful shutdown...`);

    server.close(() => {
      console.log("HTTP server closed");
      process.exit(0);
    });

    // Force shutdown after 30 seconds
    setTimeout(() => {
      console.error("Forced shutdown after timeout");
      process.exit(1);
    }, 30000);
  };

  process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
  process.on("SIGINT", () => gracefulShutdown("SIGINT"));
});

Multi-tenancy

Sevalla fully supports multi-tenancy. You can build multi-tenant Next.js applications using wildcard domains, allowing you to serve multiple tenants from separate subdomains efficiently and securely. Use the following best practices for multi-tenancy on Sevalla with your Next.js application:
  • Use wildcard domains to serve subdomain-based tenants (e.g., tenant1.app.com).
  • Add custom domains individually for tenants who have their own domains.
  • Cache tenant data to reduce database queries and improve performance.
  • Validate tenant existence before rendering pages to prevent errors or unauthorized access.
  • Isolate tenant data in the database using separate schemas or a tenant_id column.
  • Leverage free SSL certificates, which are automatically provided for both wildcard and custom domains.

Next.js 16 multi-tenant implementation

Extract tenant from subdomain:
// src/proxy.ts (renamed from middleware.ts in Next.js 16)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export async function proxy(request: NextRequest) {
  const hostname = request.headers.get("host") || "";
  const subdomain = hostname.split(".")[0];

  // Skip for main domain
  if (subdomain === "www" || subdomain === "yourdomain") {
    return NextResponse.next();
  }

  // Add tenant to headers
  const response = NextResponse.next();
  response.headers.set("x-tenant-id", subdomain);

  return response;
}
Next.js 16 renamed middleware.ts to proxy.ts and the exported function to proxy().
Use tenant in pages:
// src/app/page.tsx
import { headers } from "next/headers";

export default async function HomePage() {
  const headersList = await headers();
  const tenantId = headersList.get("x-tenant-id");

  const tenant = await getTenantData(tenantId);

  return (
    <div>
      <h1>Welcome to {tenant.name}</h1>
    </div>
  );
}
Tenant-specific data:
// src/lib/tenant.ts
import { pool } from "./db";

export async function getTenantData(tenantId: string | null) {
  if (!tenantId) return null;

  const result = await pool.query(
    "SELECT * FROM tenants WHERE subdomain = $1",
    [tenantId]
  );

  return result.rows[0] || null;
}

Custom domains per tenant

Allow tenants to use their own domains (e.g., customdomain.com → tenant data). Map custom domains to tenants:
// src/proxy.ts
export async function proxy(request: NextRequest) {
  const hostname = request.headers.get("host") || "";
  const tenant = await getTenantByDomain(hostname);

  const response = NextResponse.next();
  response.headers.set("x-tenant-id", tenant?.id || hostname.split(".")[0]);
  return response;
}

Static Site Hosting

Static Site Generation (SSG) pre-renders all pages as static HTML files at build time, delivering fast and reliable performance. Use SSG when your site includes:
  • Content that changes infrequently, such as blog posts, documentation, or product catalogs.
  • Pages that can be pre-rendered for all users without personalization.
  • Requirements for maximum performance and minimal hosting costs.
  • A global audience, as static pages benefit from fast CDN distribution.

Configuration

To deploy a fully static Next.js site on Sevalla, make sure to include the following configuration in your next.config.mjs file:
  • output: 'export' generates fully static HTML, CSS, and JS files for deployment to any static hosting. This generates all built assets to the out folder by default, unless you override this setting in the next.config.mjs file.
  • images.unoptimized: true disables Next.js image optimization, which is required for pure static exports.
  • trailingSlash: true ensures all routes include a trailing slash, which helps maintain a consistent URL structure and meet static hosting requirements.
// next.config.mjs
export default {
  // Enable static export mode to generate pure HTML/CSS/JS files
  output: 'export',

  // Add a trailing slash to all URLs (e.g., /about/ instead of /about)
  trailingSlash: true,

  images: {
    // Required for static export; disables automatic Next.js image optimization
    unoptimized: true,
  },
};

Redirects

To apply custom redirect rules, add a _redirects file containing your redirects to your repository’s root directory. Sevalla will automatically parse the file’s contents and apply the custom redirect rules.

Pretty URLs

In Sevalla, you can enable Pretty URLs to standardize your site’s URLs and improve SEO. This feature automatically enforces a trailing slash on the path of static site requests using a 301 redirect.