How to Self-Host Next.js With Coolify: A Complete 2026 Walkthrough
Tutorial

How to Self-Host Next.js With Coolify: A Complete 2026 Walkthrough

Every Coolify tutorial stops at "push to deploy." This one covers the parts that actually break a real app: standalone output, a build that OOMs on a small box, env vars baked in at the wrong time, and a health check that would have caught all of it.

Published July 18, 202612 min readUpdated Sep 6, 2026

Written by · Full-Stack Agentic AI Software Engineer — AI Agents, Automation & Revenue Systems for GTM/RevOps teams

In brief

How do I actually get a Next.js app from a fresh VPS to a working custom domain on Coolify, and what breaks along the way?

Provision a small VPS (4 vCPU / 8 GB RAM is comfortable), install Coolify with its one-line script, point a domain at the server through Cloudflare, connect the repo, and set next.config output to standalone so the runtime image stays small. The parts every quick-start skips are what actually cause outages: the Next.js build process can get OOM-killed on anything under 2 GB of RAM, NEXT_PUBLIC_ variables have to exist before the build runs (not after), and Coolify's default health check will happily route traffic to a container that's up but broken unless you give it a real endpoint to poll.

  • A 4 vCPU / 8 GB VPS handles Coolify's own control plane plus a typical Next.js build with room to spare; anything under 2 GB risks the OOM killer mid-build
  • output: "standalone" in next.config.ts is what makes the runtime image small, but the standalone tracer doesn't copy .next/static or public/ for you
  • NEXT_PUBLIC_* variables are baked into the JS bundle at build time; server-only secrets are read fresh at container start — mixing these up is the single most common misconfiguration
  • Coolify's default TCP health check confirms the port is open, not that the app works; a real /api/health route is the difference between a failed deploy and a silent one

Evidence notes

Coolify installation requirements

Official docs specify a minimum of 2 vCPU / 2 GB RAM, 4 GB+ recommended for production, Ubuntu LTS, and no Docker-via-Snap.

Next.js standalone output

Next.js's own deployment docs describe output: "standalone" and the manual copy step for static assets and the public folder.

Contabo Cloud VPS 4 pricing and specs

4 vCPU, 8 GB RAM, 100 GB SSD, traffic advertised as unlimited, for €5.50/month including VAT for the first 24 months — the tier this walkthrough assumes.

Every Coolify tutorial I read before doing this myself ends at the same triumphant screenshot: green checkmark, "deployment successful," cut to credits. Mine doesn't, because that's roughly where the real problems start. The build that works on your laptop gets killed halfway through on a cheap VPS. The env var you set in Coolify's dashboard doesn't show up in the app, because you set it a build too late. The site goes down at 2am and Coolify's dashboard says everything is fine, because its health check only confirms the port answered, not that the page rendered. None of that is a Coolify problem specifically — it's what happens when a tutorial written for a demo app meets an app with a real database connection, a few hundred megabytes of node_modules, and actual traffic. This is the walkthrough that covers those parts, in the order they'll actually bite you.

What do you actually need before starting?

Not much, which is the appeal. You need a VPS you can SSH into as root, a domain you control the DNS for, and a Next.js app in a Git repo that Coolify can reach — public repo or a deploy key for a private one. That's it. No Kubernetes cluster, no separate CI runner, no managed database unless you want one. If you are scoping how to self-host next.js with coolify for a business rather than a codebase, XenGrowth covers that angle.

  • A VPS with at least 2 vCPU / 2 GB RAM (Coolify's stated minimum); 4 vCPU / 8 GB is where builds stop being a source of anxiety — a Contabo Cloud VPS 4 at €5.50/month is that tier

  • Ubuntu 22.04 or 24.04 LTS, freshly provisioned, nothing else installed on it yet

  • A domain or subdomain, with its DNS either at your registrar or moved to Cloudflare

  • A GitHub (or GitLab) repo containing the Next.js app, ideally already building cleanly with npm run build locally

  • Root or sudo SSH access and about 45 minutes

One thing worth deciding before you touch DNS: whether your origin server's IP is public. It doesn't have to be. Running the domain's traffic through Cloudflare's free tier in front of the VPS gets you a CDN, a masked origin IP, and free SSL at the edge before Traefik ever has to negotiate a handshake — none of which this walkthrough strictly requires, but all of which take about ten extra minutes and are hard to retrofit later once real traffic depends on the DNS record staying put. The XenGrowth resource library approaches this from the the operations side of this side.

The walkthrough, start to finish

  1. Provision the box and don't leave it as root. SSH in, create a non-root user with sudo, copy your public key over, and disable password auth before you do anything else — this five-minute step is the one people skip and regret; the full version of it, including fail2ban and the firewall rules, is its own post.

  2. Install Coolify with the official one-liner: curl -fsSL https://cdn.coollabs.io/coolify/install.sh | sudo bash. It installs Docker if it's missing, pulls Coolify's own containers, and takes three or four minutes on the tier above. Don't run this on a box that already has Docker installed via Snap — Coolify explicitly doesn't support that, and the install script will half-succeed in a confusing way.

  3. Open http://<your-server-ip>:8000 once the script finishes. First load asks you to set an admin email and password and, separately, the FQDN Coolify's own dashboard should live at — this can be a subdomain like coolify.yourdomain.com, pointed at the server later.

  4. Point DNS at the box before you touch the app. Create an A record for the domain (or subdomain) you want the app on, targeting the VPS's IP. If you're routing through Cloudflare, leave the proxy (orange cloud) off for this record until the certificate issues, then turn it back on.

  5. In Coolify, create a Project, then add a Resource inside it and connect the GitHub repo and branch. Coolify auto-detects a Next.js app and offers Nixpacks as the build pack by default; for anything beyond a toy project, switch to Dockerfile so you control exactly what gets built and copied.

  6. Set output: "standalone" in next.config.ts before you build anything. This makes next build emit a self-contained .next/standalone folder with a single server.js and only the node_modules the app actually needs — the difference between a 900MB image and a 150MB one. The part that trips people up: the standalone tracer does not copy .next/static or the public/ folder for you. Your Dockerfile has to copy both in manually, or the deployed app serves a page with no CSS and no images.

  7. Set the environment variables in Coolify's resource settings, and get the timing right. Anything prefixed NEXT_PUBLIC_ gets inlined into the client JS bundle while next build runs, so it has to be present before that step — mark it as a build variable in Coolify. Server-only values like a database URL or an API key for a Route Handler are read from process.env when the container starts, not when it's built, so they belong as runtime-only variables and should never be marked build-time, or they'll sit in the image's build cache in plaintext.

  8. Set the custom domain in the resource's domain field, matching the DNS record from step four. Coolify's Traefik layer requests a Let's Encrypt certificate automatically the moment it can resolve that domain and reach the box on port 80 — if this hangs, it's almost always DNS that hasn't propagated yet or Cloudflare's proxy interfering with the ACME challenge.

  9. Add a real health check before the first production deploy. Coolify's default is a TCP check against the container's port, which confirms the process is listening, nothing more. Build an /api/health route that does something meaningful — even just a database ping — returning 200 on success, and point Coolify's health check path at it with a short interval. This is what turns "the container crash-loops silently" into "the deploy is marked unhealthy and doesn't get traffic."

  10. Deploy, and watch the build log, not just the final status. A clean build finishes in well under two minutes on the recommended tier; if it's grinding past four or five minutes with memory climbing, that's the next section's problem, not a fluke.

  11. Once it's green, hit the domain over HTTPS. If the cert isn't there yet, give it another minute and refresh — Traefik retries the ACME challenge on its own schedule, and forcing a redeploy doesn't speed that up.

Where the build actually breaks on a small box

Nobody mentions this until it happens to them: next build is memory-hungry. The compiler and minifier hold a lot in memory at once, and on a VPS with 1-2 GB of RAM, that process gets picked off by the kernel's OOM killer partway through — usually during the "Collecting page data" or minification step, with no error that points at memory as the cause. It just stops, or the container restarts, and the logs look like nothing happened. This is exactly the kind of undersizing question the vCPU or RAM sizing guide covers in more depth — for a build step specifically, RAM is what fails first, not CPU.

VPS RAM

What happens to next build

What to do about it

1-2 GB

Gets OOM-killed partway through the compile or minify step on anything past a toy app; the deploy just fails with no clear reason in the logs

Add a 2GB swap file before your first deploy, or build the image elsewhere (CI) and have Coolify pull the finished image instead of building on-box

4 GB

Usually finishes, but slows down and swaps if Coolify's own control plane and another app's build are running at the same time

Fine for a single app; don't stack several build-heavy projects on the same box without staggering deploys

8 GB (Contabo Cloud VPS 4, €5.50/month)

Finishes in well under two minutes for a typical app, with headroom left for Coolify itself and a couple of other containers

The tier this walkthrough assumes, and the one to budget for if you're only running one box

Should Coolify even be the thing building your image?

Everything above assumes Coolify runs next build itself, on the box, every time you push. That's the fastest way to get started and it's what this walkthrough uses, but it's not the only option, and it stops being the best one once a build routinely pushes a small VPS toward its RAM ceiling or takes long enough to delay a deploy you needed five minutes ago. The alternative is to build the Docker image somewhere with more headroom — GitHub Actions' own runners, for instance — push the finished image to a registry, and have Coolify just pull and run it instead of compiling anything locally. That moves the memory-hungry part off the box entirely; the VPS only ever has to run the app, not build it. The tradeoff is a second moving part to maintain: a CI pipeline that has to build, tag, and push correctly before Coolify ever sees a new image. Whether that's worth it depends on how the rest of your deploy pipeline is shaped — the full git-push-to-production workflow walks through exactly where that decision sits and what it looks like end to end once you've made it. There is a longer treatment of AI agents and marketing automation in XenGrowth on governed AI marketing workflows.

Env vars: build time vs runtime, the part every quick-start glosses over

This is the single most common support question in Coolify's own Discord, and it's not really about Coolify — it's about how Next.js handles environment variables. A NEXT_PUBLIC_ value gets read once, during next build, and gets literally written into the JavaScript that ships to the browser. Change it after the build and redeploy without rebuilding, and the old value is still there, baked in, because the container never re-ran the build step. Everything else — anything a Server Component, Route Handler, or proxy.ts reads directly from process.env — is read live when the container actually handles a request, which means you can rotate a database password or an API key and restart the container without touching the build at all. (Next 16 renamed middleware.ts to proxy.ts, and it's worth knowing the new name if you're grepping an older codebase for it.)

Variable type

When Coolify needs it

Example

NEXT_PUBLIC_* (client-exposed)

At build time — mark it a build variable, or the old value ships to browsers after a redeploy

NEXT_PUBLIC_API_URL

Server-only secrets used in Server Components / Route Handlers

At container start (runtime) — never mark these as build variables, or they end up cached in the image layer

DATABASE_URL, RESEND_API_KEY

Build-only flags that only affect what gets statically generated

Build time only, and safe to mark as such since nothing sensitive is in them

NEXT_BUILD_MODE=production

Health checks that actually catch a broken deploy

A container can be up, answering on its port, and still be completely broken — a missing env var throwing on every request, a database connection that never resolves, a 500 page rendered with a 200 status because someone's error boundary is too generous. Coolify's default TCP check won't catch any of that; it just confirms something is listening. Give it a real target instead: a route that actually exercises the thing that tends to break, returns a plain 200 with a small JSON body on success, and a non-200 the moment something's wrong. Point Coolify's health check path at that route with a short interval — 10 to 15 seconds is reasonable — and a deploy that's technically running but functionally dead gets marked unhealthy instead of quietly serving errors to real visitors. If AI search, GEO and discovery is the part you are stuck on, XenGrowth on building one SEO and GEO content system is the better reference.

  • Build fails with no obvious error: check RAM first via docker stats during the build, not after — this is almost always the OOM killer, not a code problem

  • Site loads with no styling: the Dockerfile isn't copying .next/static and public/ alongside the standalone server.js

  • An env var "isn't working": check whether it's a NEXT_PUBLIC_ value set after the last build, or a server secret accidentally marked build-only

  • Certificate never issues: DNS hasn't propagated, or Cloudflare's proxy is on before the ACME challenge completes — turn the orange cloud off until the cert is live

  • Container restarts in a loop with no logs: usually a missing required env var throwing on startup before the app can log anything useful

  • Deploy looks green but the site is broken: the default TCP health check passed; this is exactly why the /api/health route above exists

So is this actually production-ready?

Yes, for the same reason it works for the setup behind the Contabo, Coolify and Cloudflare stack that replaced a Vercel bill entirely: none of the steps above are exotic, and the box doing the work costs less per month than a single Vercel Pro seat. The failure modes are just different ones than the platform you're used to hides from you. Vercel's build step doesn't OOM on you because you're not paying for the machine it runs on; here, you are, so the machine's limits become part of the job. Once the health check is real and the env vars are in the right bucket, this setup runs for months without you thinking about it — which is the actual bar, not "it deployed once." If you're still deciding whether Coolify is the right tool for this over its closest competitor, the Coolify vs Dokploy comparison is the next thing to read, and if the whole idea of moving off a managed platform is still theoretical for you, my rule for deciding what to self-host and what happens when your VPS runs out of RAM are worth reading before you commit a weekend to this.

Further reading from XenGrowth

Where this work meets go-to-market

For the marketing and revenue operations view of how to self-host next.js with coolify, see the team at XenGrowth.

Past the green checkmark

Five questions on the parts of a Coolify deploy that a tutorial's happy path skips. Answers and reasoning at the end.

1 / 5
You set output: "standalone" and deploy, and the site loads with no CSS and broken images. Why?

Apply this article

How to turn insights into execution

A practical sequence for teams turning concepts into production outcomes.

CoolifyNext.jsSelf-HostingVPSDockerDevOpstutorials

Audit your current state

Map the bottlenecks and constraints connected to the article’s core problem.

Choose one bounded change

Test the most useful recommendation on one workflow before widening the scope.

Measure what changed

Keep the parts that improve the work, document what failed, and make the next decision from evidence.

Next step

Need help applying this in your stack?

I can translate these patterns into a concrete implementation plan for your team.

Discuss implementationBack to blog

Replies usually within 24 hours.

Next Steps

Continue reading

Docker Compose Is More Powerful Than You Think

Most people learn Compose from a five-line docker-compose.yml in a README and stop there. Profiles, real healthchecks, resource limits, and layered override files cover most of what people reach for Kubernetes to get — you're probably one flag away from using the tool you already have.

Navigate

The Docker Concepts You Need Before Coolify Hides Them From You

Coolify's whole pitch is that you shouldn't have to think about Docker, and most days that's true. The days it isn't are the days a container is 'running' but unhealthy, a redeploy quietly ate a volume, or an env var vanished — and the dashboard doesn't explain any of it.

Navigate

How to Back Up and Restore a Coolify Server Properly

Coolify's built-in backup covers less than most people assume, and a filesystem copy of a live Postgres data directory is not a valid database backup no matter how confident it looks in a file listing. Here's what actually has to be backed up, and a restore you've actually tried before you need it.

Navigate

Where console.log Actually Goes When You Self-Host Next.js

On Vercel, a console.log just shows up in a dashboard somewhere. Self-hosted, it goes through a chain most people never trace end to end — and the two most common questions I get are why a log doesn't show up at all, and why the disk filled with logs nobody remembers writing.

Navigate

10 Mistakes That Break a Self-Hosted SaaS

None of these ten show up as a single dramatic outage. They show up as a disk that quietly fills, a rollback that turns out to be impossible, a backup nobody ever restored. Here's the mechanism behind each one, and the fix.

Navigate

My Free Monitoring Stack for Self-Hosted Apps

Vercel gives you monitoring whether you ask for it or not. A VPS gives you a blank terminal and the assumption you'll figure it out. Here's what to actually watch on a self-hosted box, with tools that cost nothing, and why watching from the box itself is the one setup that will lie to you.

Navigate

How I Restore an Entire Server From Backup (And How Long It Takes)

The order matters more than people expect, and the answer to "how long will this take" isn't a number I can hand you — it's dominated by your database size, your download bandwidth, and one DNS setting most people only think to change after they needed to.

Navigate

How I Back Up PostgreSQL to S3-Compatible Storage on a Schedule

pg_dump, pg_basebackup and WAL archiving aren't three ways to do the same thing — they answer three different questions about how much data you're willing to lose. Here's which one to run, from a container that has no business having shell access to your host, pushed to storage that makes restoring free instead of expensive.

Navigate

How to Host Unlimited Domains on One VPS With Free SSL

"Unlimited domains, one server" sounds like a sales pitch, but it's a genuinely accurate description of what SNI-based virtual hosting and Let's Encrypt automation do together. The real ceiling isn't domain count. It's RAM.

Navigate

Wildcard Domains With Coolify and Cloudflare

A wildcard certificate can't be proven the way a normal one is — there's no single page to fetch for a domain that doesn't exist yet. That's why it needs a DNS record instead of an HTTP request, and why Cloudflare's API has to be involved at all.

Navigate