Deploy a Next.js app

Deploy a Next.js app on Lucity, with a database, environment variables that behave, and no Dockerfile.

Next.js needs no configuration to deploy here. Point Lucity at the repository and it recognizes the framework, runs your build script, and starts the server the way next start expects. What follows is the short list of things that are worth knowing anyway.

Start from a fresh app

If you want something to try this on:

npx create-next-app@latest my-app

Push it to GitHub, then follow the quickstart to connect the repository. Nothing in the generated project needs changing first.

What the builder does with it

The build runs npm run build and the service starts with npm run start, which is next start in a stock project. Two details follow from that:

  • Your start script is the contract. Change it and Lucity uses whatever you changed it to. Delete it and detection falls back to the main field, then index.js.
  • The port comes from us. Lucity sets PORT in the container and next start reads it, which is why the default project works untouched. If you define your own PORT variable, yours wins and the service must listen on it.

Telemetry is switched off during the build, so you will not see the Next.js notice in your build logs.

You do not need output: "standalone". That mode exists to shrink Docker images, and there is no Dockerfile here to shrink.

Static exports are a different deployment

Set output: "export" in next.config.ts and the shape of the deployment changes: the build produces static files and Lucity serves them from a static web server instead of running Node. Faster, cheaper, and no server runtime, but API routes, server actions and anything else that runs per request stop working. Choose deliberately.

Which variables are frozen

Anything prefixed NEXT_PUBLIC_ is inlined into the JavaScript bundle when the build runs. Everything else is read at runtime by the server.

That difference matters here because variables normally take effect without a rebuild: change one, the service rolls out in seconds with the same image. A NEXT_PUBLIC_ value is already baked into that image, so it will not change until the next build. Set public values before you build, and treat editing one as a code change.

Server-side variables have no such catch. A rotated database password reaches the running app on the next rollout.

Reading a database from server components

Add a PostgreSQL database and link DATABASE_URL to its fqdn-uri. From there it is ordinary Next.js: pg, Prisma, Drizzle, whichever client you like.

// app/page.tsx
import { Pool } from 'pg';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });

export default async function Page() {
  const { rows } = await pool.query('SELECT title FROM posts ORDER BY votes DESC');
  return <ul>{rows.map(row => <li key={row.title}>{row.title}</li>)}</ul>;
}

One thing to watch: pages that hit the database while being statically generated need that database reachable at build time. Either mark those routes dynamic, or fetch on the client.

For uploads, add a bucket and use the AWS SDK against the credentials Lucity injects. The local filesystem is not the place: it disappears with the container.

When the build runs out of memory

next build is the memory-hungry part of a Next.js deployment. A large app can exhaust what the build environment has, and the logs end abruptly with a killed process rather than an error.

Raising the service's memory does not help: that setting applies to the running container, while builds run in a shared build environment with its own fixed ceiling. Reduce what the build needs instead:

  • Cap the Node heap so V8 collects rather than grows: set NODE_OPTIONS to --max-old-space-size=3072 as a variable.
  • Turn off production source maps in next.config.ts if you have them on.
  • Split the work: precompute large data at request time or in a background job rather than generating thousands of pages at build time.

Next steps