Skip to content

How to deploy a Next.js app on Ownkube

Deploy a Next.js app on Ownkube using the standalone build output, environment variables, an optional managed Postgres, and automatic TLS on your hostname.

Abhishek Jaiswal

6 min Updated

On this page 9 sections

A Next.js app in a container needs one setting most tutorials skip: output: "standalone", which trims the build down to only the files next start actually needs instead of the full node_modules tree. Get that right and the rest of a Next.js deploy on Ownkube is a normal git push.

This walks through deploying a Next.js app (App Router or Pages Router, both work) on Ownkube Compute: the standalone build, environment variables split between build time and runtime, an optional managed Postgres for apps using an ORM, and automatic TLS.

TL;DR

  • Set output: "standalone" in next.config.js so the Docker image ships a minimal server instead of the whole node_modules folder.
  • Push the app with a Dockerfile, and Ownkube builds it, deploys it, and issues TLS automatically on your *.ownkube.app hostname.
  • NEXT_PUBLIC_* variables get baked into the build; everything else is a runtime environment variable set in the dashboard.
  • Add an Ownkube-managed Postgres database if you use Prisma or another ORM, and point the connection string at it.
  • Turn on horizontal autoscaling once traffic is uneven, since Next.js’s SSR path is CPU-bound per request in a way static pages are not.

1. Enable standalone output

In next.config.js, add the standalone output mode:

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
};
module.exports = nextConfig;

This changes what next build produces. It emits a self-contained .next/standalone folder with only the files the server needs, plus a minimal server.js entry point, so the runtime no longer needs the full project and node_modules. That keeps the final Docker image small.

2. Write a Dockerfile

A multi-stage build keeps dependencies out of the final image:

FROM node:20-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:20-slim AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# NEXT_PUBLIC_* values must be present at build time; they end up in the client bundle
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
RUN npm run build
FROM node:20-slim
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

The build stage takes NEXT_PUBLIC_API_URL as a build argument because Next.js inlines NEXT_PUBLIC_* variables into the client JavaScript at build time. A value set only at runtime in the dashboard will not reach client-side code; it has to be present when npm run build runs.

3. Push the repo and create the app

Connect the repository from the Ownkube dashboard (app.ownkube.io) to deploy on every push, or deploy the current directory with the okctl CLI:

Terminal window
okctl login
okctl up --name nextjs-app --port 3000

Ownkube builds the image, allocates a hostname on *.ownkube.app, and issues TLS automatically, so there is no reverse proxy or certificate to set up by hand.

4. Set your environment variables

Split variables by when Next.js actually needs them:

Terminal window
# Server-only, read at runtime by API routes or Server Components
okctl vars set nextjs-app AUTH_SECRET="$(openssl rand -base64 32)" --secret
# Client-visible, baked into the bundle at build time, so set it as a build argument (see step 2)
okctl deploy build-args nextjs-app --arg NEXT_PUBLIC_API_URL="https://nextjs-app.ownkube.app/api"

Environment variables set through Ownkube are encrypted at rest and only injected into the running container, which is why a NEXT_PUBLIC_ value goes in as a build argument instead. Anything without the NEXT_PUBLIC_ prefix stays server-side and never ships to the browser.

5. Add a managed Postgres database, if you use an ORM

A Next.js app using Prisma, Drizzle, or another ORM for a database-backed API route needs a real Postgres instance. Create a database box from the dashboard: New > Database > Postgres, walked through in full in add a managed Postgres database to your app.

It provisions a private (in-region) Postgres box starting at $4 a month plus storage at $0.25 per GB, with backups and point-in-time recovery available on Personal and Team. The database runs as a single instance by default; you can turn on opt-in public access and, on Team, high availability (a primary plus up to two standby instances) per database when you need them.

Link the database to the app, and Ownkube writes its connection string into the app as a secret DATABASE_URL variable and redeploys it:

Terminal window
okctl deploy link nextjs-app <database-id>

With Prisma, generate the client in the build stage:

# In the build stage, after copying the schema
RUN npx prisma generate

Run prisma migrate deploy as a one-off job instead of in the build. Variables reach the running container rather than the build, and the database is private to its region, so the build cannot reach it.

The slim standalone image leaves out the Prisma CLI and your prisma folder, so give the job its own Dockerfile that installs dependencies and copies the schema, set its command to npx prisma migrate deploy, and link the same database to it. Then trigger it when a release includes new migrations:

Terminal window
okctl deploy link nextjs-migrate <database-id>
okctl deploy job-runs trigger nextjs-migrate

A purely static or content-fetching Next.js app can skip a database entirely.

6. Scale and watch it run

A fresh Next.js app usually starts fine on a small metered ceiling, for example a 500m CPU limit and 512 MiB of memory, set on the sliders under Settings → Resources.

Turn on horizontal autoscaling if traffic is uneven, so Ownkube adds server instances under load and scales back down when it is quiet. On Ownkube Compute you turn it on through your coding agent (the update_deployment MCP tool) or the API, with a minimum and maximum replica count and a CPU target such as 70%.

Server-side rendering and API routes are CPU work per request in a way a purely static export is not, so an SSR-heavy app benefits more from autoscaling headroom than a mostly-static one.

Metered billing charges CPU and memory on actual per-minute usage, so a quiet Next.js app between visits barely moves the wallet. Logs and metrics for the app live in the dashboard, so a slow render or a route that keeps erroring shows up in one place.

Moving to your own AWS later

If the app needs to run inside your own AWS account later, for compliance reasons or because a client wants their name on the infrastructure, the same Dockerfile and Next.js app move over with no rewrite. Ownkube provisions the instance, wires up the same deploy flow, and brings the logs, metrics, and health checks with it.

Railway only runs your app on its own infrastructure. Our Railway alternatives guide compares Railway with the other options.

FAQ

Do I need output: "standalone", or does the default build work?

The default build works, but it expects the full project and node_modules present at runtime, which makes for a much larger Docker image and a slower deploy. Standalone output is the setting almost every Next.js Docker guide converges on, and it is the one used in the Dockerfile above.

Does Ownkube support the App Router and Server Actions?

Yes, both the App Router and the older Pages Router run the same way, since next start in the standalone build serves whatever the app builds. Server Actions run as part of the same Node process and read server-side environment variables normally.

How do I handle a value that is different between preview and production?

Set the environment variable per app in the dashboard or with okctl vars set <app> NAME=VALUE, and use a separate Ownkube app for each environment you want isolated, each with its own hostname, environment variables, and (if needed) database.

What happens if my wallet balance runs out mid-month?

The app pauses and any attached database keeps its data. Adding credit or the next monthly plan load resumes it. Unused wallet credit rolls over and never expires.

Can I use a custom domain?

An automatic *.ownkube.app hostname with TLS is provisioned for every app the moment it deploys, so you always have a working URL with no configuration. Custom domain support on Ownkube Compute is on the roadmap; it is available today on Starter clusters in your own AWS account.

For the always-current version of this walkthrough, see the Deploy Next.js guide in the docs.

Where Ownkube fits

Next.js wants a slim server image, a clean split between build-time and runtime configuration, and a database only when the app actually uses one, and Ownkube gives you all three without provisioning a cloud account first. Set standalone output, push the Dockerfile, set your environment variables, and the hostname and TLS are already waiting. See Ownkube pricing for the per-minute wallet model, from $5 a month. Deploy my first app.

Abhishek Jaiswal · Founder and Engineer

Abhishek Jaiswal is the founder and engineer behind Ownkube, a cloud for personal software. He builds the platform and writes here about cloud cost, migrations, and running production infrastructure in your own AWS account without a DevOps team.

Share

More posts

All posts

Ship my first app in the next five minutes.

Push a repo and get a live URL, with no cloud account or config.

Top up from $5, billed per minute with no subscription.