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.

Ownkube team | | How-To | 6 min

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: instead of a build that expects the full project and node_modules present at runtime, it emits a self-contained .next/standalone folder with only the files the server needs, plus a minimal server.js entry point. That is what 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) or push directly with the okctl CLI:

okctl login
okctl apps create nextjs-app --dockerfile Dockerfile
okctl deploy

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

4. Set your environment variables

Split variables by when Next.js actually needs them:

# Server-only, read at runtime by API routes or Server Components
okctl env set DATABASE_URL="postgres://user:password@db-host:5432/nextjs_production" --app nextjs-app
okctl env set AUTH_SECRET="$(openssl rand -base64 32)" --app nextjs-app

# Client-visible, must also be passed as a build argument (see step 2)
okctl env set NEXT_PUBLIC_API_URL="https://nextjs-app.ownkube.app/api" --app nextjs-app

Environment variables set through Ownkube are encrypted at rest and only injected into the running container. 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 single-instance, private (in-region) Postgres box starting at $4 a month plus storage at $0.15 per GB, with backups included. Multi-instance HA and public access are on the roadmap, not shipped yet.

With Prisma, point DATABASE_URL at the connection string and run migrations as part of the build:

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

prisma migrate deploy applies pending migrations without prompting, which is what you want in a non-interactive build. 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:

okctl deploy --cpu-limit 500m --memory-limit 512Mi

Turn on horizontal autoscaling if traffic is uneven, so Ownkube adds server instances under load and scales back down when it is quiet:

okctl apps update nextjs-app --autoscale-min 1 --autoscale-max 4 --autoscale-cpu-target 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. That is not something a platform like Railway offers, since it only runs your app on its own infrastructure. Our Railway alternative comparison walks through where that gap tends to matter.

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 env set --app <name>, 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 Ownkube’s Starter (BYOC) clusters.

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. Deploy your first app.

More posts