Deploying Next.js to Coolify: The Production Setup Guide

#deploy next.js to coolify
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Founder & Lead Developer

Expert in software development and legacy code optimization

More teams are moving off Vercel and onto their own servers, and Coolify has become one of the most common landing spots. If you want to deploy Next.js to Coolify and have it behave like a production platform rather than a hobby setup, there are about a dozen decisions that matter: how the app gets built, which environment variables exist at build time versus runtime, how deploys avoid downtime, and what breaks when features that Vercel handled for you suddenly become your problem.

This guide walks through the full setup for a Next.js 15 application on Coolify, based on what we configure for client projects. It assumes you already have a Coolify instance running on a VPS. If you are still deciding between platforms, our comparison of Coolify, Dokploy, and Kamal covers that decision in detail.

Why deploy Next.js to Coolify

The short version: cost and control. A Hetzner or OVH server at 20 to 40 euros a month can serve the same traffic that costs several hundred dollars on a serverless platform once you pass the free tier, and your data stays on infrastructure you choose. For European teams with GDPR obligations, running on an EU server you control simplifies the conversation with your data protection officer considerably.

The trade is that you take over responsibilities the managed platform used to hide. Most of this guide is about exactly those responsibilities.

Build setup: Nixpacks or a Dockerfile

Coolify offers Nixpacks as the default buildpack. It detects a Next.js app and produces a working image with zero configuration, which is fine for a first deploy. For production we recommend switching to a Dockerfile anyway, for one reason: the standalone output mode.

In next.config.ts, set:

const nextConfig = {
  output: 'standalone',
};

Standalone mode makes next build emit a server.js plus only the node_modules files the server actually imports. The resulting image drops from 1 GB or more to roughly 150 to 250 MB. Smaller images mean faster deploys, faster rollbacks, and less disk pressure on your VPS, which matters more than you would expect once several apps share one server.

A minimal production Dockerfile looks like this:

FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-alpine
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"]

Point Coolify at the Dockerfile in the application's build settings and set the port to 3000. If your server has 4 GB of RAM or less, also cap the build memory: NODE_OPTIONS=--max-old-space-size=2048 as a build-time variable prevents next build from taking the whole machine down while a deploy runs next to live traffic.

Environment variables: build time versus runtime

This is the single most common source of broken first deploys, so it deserves a precise explanation.

Next.js splits environment variables into two groups. Anything prefixed NEXT_PUBLIC_ is inlined into the JavaScript bundle during next build. Everything else is read from the process environment at runtime. Coolify, in turn, lets you mark each variable as available at build time, at runtime, or both.

The consequences are concrete. A NEXT_PUBLIC_API_URL that is only set as a runtime variable will be undefined in the browser, because the build that baked the bundle never saw it. A DATABASE_URL marked build-time only will crash the container on boot. The rule to remember: NEXT_PUBLIC_ variables must be enabled for build time, secrets should be runtime only so they never end up in an image layer, and if you change a NEXT_PUBLIC_ value you must trigger a rebuild, not a restart, for it to take effect.

Coolify stores secrets encrypted and injects them into the container, so you do not need an external secrets manager for a single-server setup. You do need discipline about which checkbox each variable gets.

Domains and TLS

Coolify fronts your containers with Traefik and handles certificates through Let's Encrypt. In the application settings, set the FQDN to https://yourdomain.com, point an A record at the server's IP, and the certificate gets issued and renewed automatically on first request.

Two practical notes. First, set the www redirect at the Traefik level in Coolify rather than in Next.js middleware; handling it in the proxy keeps redirect logic out of your application code. Second, if you use a CDN like Cloudflare in front, run it in DNS-only mode until the first certificate is issued, otherwise the ACME challenge fails and you will chase a confusing error.

Health checks and zero-downtime deploys

Out of the box, a Coolify redeploy stops the old container and starts the new one, which means a visible gap of a few seconds. To get rolling deploys, the new container must prove it is ready before the old one goes away, and that requires a health check endpoint.

Add a route at app/api/health/route.ts:

export async function GET() {
  return Response.json({ status: 'ok' });
}

Then enable the health check in Coolify with path /api/health, port 3000, and a start period of 20 to 30 seconds so the check does not kill a container that is still booting. With the check green, Coolify starts the replacement, waits for it to pass, switches traffic in Traefik, and only then stops the old container. Deploys become invisible to users.

Resist the urge to make the health endpoint check the database. If your database has a hiccup, you want the app up and serving cached pages with error states, not Traefik pulling every container out of rotation at the same moment.

Postgres: Coolify-managed or external

Coolify can provision a PostgreSQL container on the same server in a couple of clicks, with scheduled backups to S3-compatible storage. For staging environments and early-stage products this is entirely reasonable, and the latency between app and database is effectively zero since they share a host.

The honest limitation is operational. A database container on the same VPS shares its fate: a full disk, a kernel panic, or a botched server migration takes both down. Once real customers depend on the data, we usually move clients to a managed Postgres such as Neon, Supabase, or a Hetzner managed database, and keep Coolify for the application tier. Set DATABASE_URL as a runtime variable, enable connection pooling on the provider side, and the application does not care where the database lives.

Whichever option you pick, test a restore before you need one. A backup you have never restored is a hope, not a backup.

Automatic deploys from GitHub

Connect the repository through a GitHub App under Sources in Coolify rather than a plain deploy key. The App integration gives you push-triggered deploys, commit status updates back on GitHub, and preview deployments for pull requests if you enable them per application.

The flow after setup: push to your production branch, Coolify builds the image on the server, health checks gate the switch, and the deploy shows up as a status on the commit. For teams coming from Vercel this recovers most of the workflow they are used to, minus the per-seat pricing.

The gotchas nobody mentions until production

These three issues account for most of the surprised messages we get from teams a few weeks after they migrate.

ISR cache persistence. Incremental Static Regeneration writes its cache to .next/cache inside the container filesystem. Every deploy replaces the container, so the ISR cache starts empty and every revalidated page gets rebuilt on first hit. With a handful of pages nobody notices; with ten thousand product pages the first minutes after a deploy hammer your data sources. Mount a Coolify persistent volume at .next/cache, or for multi-container setups implement a custom cache handler backed by Redis. Our post on Next.js 15 caching and revalidation patterns goes deeper on how the cache layers interact.

Server-Sent Events and sticky sessions. If you scale to more than one container replica and use SSE for streaming responses, the stream and the follow-up requests can land on different replicas. Any in-memory state associated with the stream breaks. Either enable sticky sessions in the Traefik configuration or, better, keep stream state in Redis so any replica can serve any request.

Image optimization memory. next/image optimization runs through Sharp inside your container, and Sharp allocates aggressively. On a 2 GB VPS, a crawler requesting a few dozen uncached image sizes at once can OOM the container, which then drops real traffic while it restarts. Set a memory limit on the container so the kernel kills Sharp spikes early, restrict images.remotePatterns and the allowed device sizes in next.config.ts, and if images are central to your product, move optimization to a dedicated service or a CDN-level optimizer.

When the setup is the easy part

Getting a Next.js app onto Coolify takes an afternoon. Running it well for a year involves the less glamorous work: monitoring, backup restore drills, capacity decisions, and knowing which platform limitations to design around before they page you at night. That operational judgment is where most self-hosting migrations actually succeed or fail.

If you are planning a move off Vercel, or your self-hosted setup already exists and feels shakier than it should, we help teams with exactly this as part of our custom software development and tech stack strategy work. Write to hello@wolf-tech.io or find us at wolf-tech.io, and we can look at your setup together.