# Dockerize a Next.js App

Yes. An agent writes a standalone multi-stage Dockerfile and a compose.yaml with Postgres for your Next.js app, boots it in a sandbox and opens a PR.

[Run this on Rush](https://cloud.getrush.ai/run/dockerize-app/nextjs/), in a Project that has your repository.

## What the agent does

The agent clones the repository into its own sandbox and reads how the app builds and what it needs at build time and at run time, which for Next.js are not the same list. In an app like nextjs/saas-starter, `package.json` has `next` at `15.6.0-canary.59`, React `19.1.0`, `drizzle-orm` with the `postgres` driver, and `stripe`; the lockfile is `pnpm-lock.yaml` with no `packageManager` field, so the image enables pnpm through Corepack rather than assuming npm. `next.config.ts` turns on `experimental.ppr` and `clientSegmentCache` and sets no `output`, and `.env.example` lists `POSTGRES_URL`, `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `BASE_URL` and `AUTH_SECRET`. There is no Dockerfile.

Two things in the code decide how the build stage is written. `lib/db/drizzle.ts` throws `POSTGRES_URL environment variable is not set` at import time, and `app/(dashboard)/pricing/page.tsx` sets `export const revalidate = 3600` and calls `getStripePrices()` and `getStripeProducts()`, so `next build` prerenders the pricing page and talks to Stripe while it does. The agent runs the build once in the sandbox to see exactly which of those fail without values, then picks a fix and writes it down in the PR: pass the variables as build arguments for a build that prerenders, or make the page render on request so the image builds with no secrets in it. It does not quietly bake a key into a layer.

Then it writes the `Dockerfile`: a dependencies stage that runs `pnpm install --frozen-lockfile`, a build stage that runs `pnpm build` with `output: 'standalone'` added to `next.config.ts`, and a runtime stage on a slim Node image that copies only `.next/standalone`, `.next/static` and `public`, runs as a non-root user and starts `node server.js` on port 3000. Migrations are not part of the server, so the image also carries `drizzle.config.ts` and `lib/db/migrations/` (one migration today, `0000_soft_the_anarchist.sql`) and `drizzle-kit`, which this app lists under `dependencies`, so a one-off `migrate` service can run `pnpm db:migrate` against the database before the app starts.

`compose.yaml` gets a `postgres` service with a health check and a named volume, the `migrate` service that waits for it, and the `app` service that waits for the migration, with `POSTGRES_URL` pointing at the `postgres` host and every other value read from the environment. In the sandbox the agent runs `docker compose up --build`, watches the migration apply, loads `/` and `/sign-in` in the sandbox's browser, and checks that `/dashboard` redirects to `/sign-in` without a session, as `middleware.ts` does. It records the image size from `docker image ls` and takes a screenshot of each page.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Containerize this Next.js app. Read package.json, the lockfile, next.config, .env.example, the database client and migration config, and list which environment variables the build needs and which only the running server needs. Run the build once to confirm. Write a production multi-stage Dockerfile using output standalone, the project's own package manager from its lockfile, a slim Node runtime stage with a non-root user, and no secret baked into any layer; if a page needs a secret at build time, choose between a build argument and rendering it on request, and explain the choice. Add a .dockerignore and a compose.yaml with Postgres (health check and volume), a one-off service that runs the database migrations, and the app, all configured from environment variables. Build and start it in the sandbox, run the migrations, load the home page and the sign-in page in the browser, and fix anything until they render. Open a pull request with the image size, the screenshots and the commands to run it locally.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a pull request on its own branch with a new `Dockerfile`, `compose.yaml` and `.dockerignore`, the one-line `output: 'standalone'` change in `next.config.ts`, and any change the build-time variables needed, such as a page moved to render on request. The description lists each environment variable with whether the build or the server reads it, the final image size from `docker image ls`, and screenshots of `/` and `/sign-in` served by the container.

The proof is in the session's transcript, which keeps every command with its output: the first `pnpm build` that showed what was missing, each `docker compose up --build`, the migration log, and the browser loads, including any that failed before a fix. If your repository runs checks on pull requests they run here as well; saas-starter has no workflows, so for an app like it the transcript is the record that the stack booted.

Before merging, a reviewer checks that no `.env` or Stripe key is copied into the image, that the standalone server starts with only what the runtime stage copies, that migrations run once and not on every app start, and that the Node version in the runtime image matches what production uses.

## Run it on every new Next.js service

When a team starts a new service from the same template, one `POST /o/{org}/sessions` per repository, with that repository in `resources` and this prompt as `input`, gives each one its container setup with the same structure. An `Idempotency-Key` per repository makes a retried request return the run it already started.

A webhook on the API key posts `run.completed` or `run.failed` to your endpoint when the turn finishes, with the `session_url` to read the result and the pull request, so a script that creates repositories can also wait for their Docker setup without polling.

## Why run it on Rush?

A Next.js Dockerfile is easy to write and easy to get wrong in ways that only show up when it runs: a build that needs a database URL it does not have, a standalone server missing its static files, a migration that never ran. Finding those means building, booting Postgres, migrating and loading pages several times. In a Rush sandbox the agent has its own Docker, its own Postgres container and a browser, so none of that runs against your laptop's port 5432 or fills its build cache.

The result is a pull request with the evidence beside it: build logs, the migration output, the image size and screenshots of pages the container served, all kept in the transcript with the commands that produced them. When a reviewer wants a smaller base image or a different Node version, a follow-up message to the same session wakes the parked sandbox where it stopped and rebuilds.

## Frequently asked questions

### Does the build need my Stripe and database secrets?

It depends on which pages prerender. In saas-starter the pricing page calls Stripe during `next build`, and the database client throws on import without `POSTGRES_URL`. The agent runs the build to find out, then either passes values as build arguments or makes those pages render on request, and says which in the pull request.

### How do I give the agent a Stripe test key?

Save it as a Project secret with `POST /o/{org}/secrets` and start the session in that Project with `POST /o/{org}/p/{project}/sessions`; the agent reads it as an environment variable. Without one, the agent boots the app with placeholder values and reports which pages need a real key to render.

### Will it use npm if my project uses pnpm?

No. It reads the lockfile and uses the package manager that wrote it, so a `pnpm-lock.yaml` means `pnpm install --frozen-lockfile` in the image. With a `packageManager` field in `package.json`, it uses that exact version.

### Where do migrations run?

In a separate one-off service in `compose.yaml` that runs before the app starts, using the project's own migration command (`pnpm db:migrate` with drizzle-kit here). Running them in the app's start command would race when you scale to more than one container.
