This is the follow-up to moving a static Astro site off Vercel. That post covers the why. This one covers doing the same with Next.js, where the setup is less straightforward.
Next.js is the dominant React framework and it runs best on Vercel, which makes sense given Vercel built it. Features like the Edge Runtime, Incremental Static Regeneration, and Image Optimisation are designed to run on Vercel’s infrastructure. When you self-host, you get most of it, but not all of it, and some parts require non-obvious configuration.
OpenNext is the project that documents and fills those gaps. It started as a collection of workarounds and has become the practical standard for running Next.js outside Vercel.
Why self-hosting Next.js is not straightforward
A plain next build produces a .next directory. Running next start serves it. That works for basic cases, but it leaves out a few things that production apps depend on.
ISR (Incremental Static Regeneration) needs somewhere to store and revalidate cached pages. Image optimisation needs a sharp-based runtime to resize on the fly. Middleware runs at the edge on Vercel’s infrastructure; self-hosted, it runs in Node process or not at all depending on how you configure it.
None of these are impossible to solve. They just require deliberate setup rather than zero-config deployment.
What OpenNext does
OpenNext takes the Next.js build output and converts it into deployable packages for environments other than Vercel. The original focus was AWS Lambda. You get a separate Lambda function for the server, a CloudFront distribution, an S3 bucket for assets, and a cache layer. The OpenNext AWS adapter wires it up.
As of Next.js 15, the Vercel team published a Deployment Adapters RFC. The goal is a first-class adapter interface so platforms like Netlify, Cloudflare, and self-hosted Docker can support the full feature set without reverse-engineering it. OpenNext influenced that direction.
Option 1: AWS with OpenNext
The AWS path is the most complete. SST wraps OpenNext into a higher-level config:
// sst.config.ts
export default $config({
app(input) {
return {
name: "my-app",
removal: input?.stage === "production" ? "retain" : "remove",
home: "aws",
};
},
async run() {
new sst.aws.Nextjs("MyApp");
},
});
That deploys your Next.js app to Lambda, sets up CloudFront, and configures ISR using S3 and a revalidation queue. You pay per request on Lambda. For most apps that’s cheaper than Vercel at scale. For very high traffic apps, the cost profile becomes worth modelling before you commit.
The advantage over a VPS: you don’t manage servers. The disadvantage: AWS billing is usage-based too. Lambda and CloudFront are cheap until a pathological traffic pattern hits them. You still need alerting and spend limits.
Option 2: Docker on a VPS with Dokploy
The simpler path is Docker on a VPS. You lose ISR (unless you add a cache service yourself) but for apps that don’t need it, the setup is clean.
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
This uses Next.js’s output: 'standalone' mode, which produces a minimal Node server in .next/standalone. You need that option enabled in next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
};
module.exports = nextConfig;
Deploy this with Dokploy the same way as a static site: connect the repo, point at the Dockerfile, set your domain, and Traefik handles SSL. The app runs as a persistent Node process behind nginx or Traefik.
For ISR, you can add a Redis container and configure Next.js’s cache handler. That’s extra work but keeps you on a VPS rather than paying for managed cache infrastructure.
Which one to use
The AWS path makes sense if you need ISR, if you’re already invested in AWS, or if you want serverless scale without managing a box. SST plus OpenNext gets you there with minimal config (my experiences with SST vs CDK).
The VPS path makes sense if the app doesn’t need ISR, if you want predictable fixed costs, or if you already have a server running other things. At €5-20/month, a Hetzner box with Dokploy handles a surprising amount of traffic.
Both paths leave you off Vercel’s per-request pricing, which matters for apps where traffic spikes are plausible.
Caveats
Image optimisation with next/image requires a running Node process or a separate image service. It doesn’t work in the Docker static export. If you’re using <Image /> heavily, you need the standalone mode, not a static export.
Middleware runs as Node middleware in the standalone server. It doesn’t have the Edge Runtime constraints. That’s usually fine. If you wrote middleware specifically for Edge compatibility (sub-10ms response times, no Node APIs), you may need to adjust it.
The OpenNext docs cover the detailed edge cases. They’re worth reading before you commit to a deployment strategy.