June 25, 2025

Trials and Tribulations of Self‑Hosting Next.js

Welcome to Community Posts
Click below to read the full article.
Arrow
Summary of What to Expect
Table of Contents
Co-authored by Austin Akers


next start” Isn’t Enough

We love Next.js. It’s an incredible framework for building modern web apps with React. But the moment you try to self‑host it—especially on bare metal or in clustered environments—you quickly learn that serverless hosts are doing a lot of heavy lifting.

We faced this firsthand at Harper, a platform that fuses a database, networking stack, and application runtime into one high‑performance Node.js engine. So, we asked: What does it really take to self‑host Next.js inside Harper? Spoiler: it’s way more than running next start.

This is the story of how we reimagined the Next.js lifecycle—Develop, Build, Deploy, and Run— within our platform.

Develop: Rebuilding next dev Internally

The Next.js dev server is magic. Hot Module Reloading (HMR), Fast Refresh, error overlays—everything just works. To replicate that inside Harper, we built:

  • A WebSocket-aware upgrade handler to inject hot-reload support into the Harper middleware.
  • A CLI wrapper, harperdb-nextjs dev, which boots Harper and then spins up a Next.js dev server with all the right wiring.
  • Seamless Hot Module Reloading via shared WebSocket channels between client and server.
Sequence diagram showing how HMR updates modules in the browser via WebSocket after file changes are detected by the dev server.

We dove deep into how WebSocket upgrades work in Node and exposed just enough of Harper’s networking stack to give Next.js what it needed.

Build: One Thread to Rule Them All

Harper is multi-threaded. So if we weren’t careful, every thread would build the app independently. Our fix:

  • Thread-locking to ensure only one thread triggers a build.
  • Shared artifact storage so other threads can reuse the output.
  • A harperdb-nextjs build command to kick off clean, single-threaded builds.
Flowchart detailing how threads manage file locks, handle stale states, and coordinate build execution to prevent concurrent access.

This let us avoid the explosion of duplicate builds while preserving concurrency where it matters.

Deploy: Rolling Your Own (Literally)

Production Harper instances often run as clustered nodes, replicated across regions. Replacing all of them at once? A disaster waiting to happen.

Instead, we built rolling deployment support, one node at a time:

  • Nodes take turns building and restarting.
  • Traffic shifts around them.
  • Zero downtime. Full data consistency.
Illustrates a rolling deployment strategy where updates are gradually applied across server groups while maintaining service availability and performance monitoring.

And the best part? This isn't just for Next.js—it's the same deploy system Harper uses for all components.

Run: Programmatic Next.js + Middleware Wizardry

We don’t use next start. Instead, we load Next.js programmatically:

const app = next({ dir: componentPath, dev: false });
await app.prepare();

Then we inject the request and WebSocket handlers directly into Harper’s middleware. This unlocks some wild capabilities:

  • Dynamic version loading: We require.resolve() the right Next.js version from the app’s package.json.
  • Multi-zone hosting: Several Next.js apps, each at different paths, running in one Harper process.
Shows how Harper routes client requests to zone-specific handlers and components using HTTP middleware across blog, store, and root apps.

This allows enterprise users to collapse complex micro-frontend setups into a single runtime—no hops, no hard navigation.

The Tricky Bits: Working Directory Problem

Next.js handles dynamic paths well. But some React tools assume process.cwd() reflects their app’s root. When you’re running multiple apps in one process, that assumption breaks.

We didn’t find a silver bullet here. Instead, we emphasize:

  • Explicit paths over relative ones.
  • Avoiding tools that depend on working directory voodoo.

It’s manageable—but worth flagging if you try this approach.

What’s Next? (Yes, Pun Intended)

We’re not done. Here’s what we’re working on now:

  • Smarter HTTP middleware to remove unnecessary redirects in multi-zone setups.
  • Next.js page cache ↔ Harper database integration to persist pre-rendered HTML and speed up cold starts.
  • Framework parity: bringing the same experience to Vue, Nuxt, Svelte, Astro, and more.

Conclusion

Self-hosting Next.js is far more involved than running a single command—but with the right approach, it can be both powerful and seamless. By rethinking each stage of the app lifecycle—develop, build, deploy, and run—we integrated Next.js deeply into Harper’s platform, unlocking support for features like multi-zone apps, rolling deployments, and dynamic versioning.

The result is a flexible, production-grade setup that gives teams full control without sacrificing the developer experience Next.js is known for.