# Dockerize a Nuxt App

Yes. An agent writes a multi-stage Dockerfile and compose file for your Nuxt app, builds the Nitro output, boots it in a sandbox and opens a PR.

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

## What the agent does

The agent starts by reading how the app is built and what it talks to at runtime, because a Nuxt image is small only if the runtime stage ships the Nitro output and nothing else. In an app like `nuxt/movies`, `package.json` pins the toolchain through `devEngines`: Node `^24.0.0` and pnpm `11.1.2`, both with `onFail: download`. The `build` script is `nuxt build`, `start` is `node .output/server/index.mjs`, and `pnpm-workspace.yaml` adds a second package, `proxy`, with `shamefullyHoist: true` and `allowBuilds` set to false for `sharp`, `esbuild` and `@parcel/watcher`. `nuxt.config.ts` loads `@vueuse/nuxt`, `@unocss/nuxt`, `@nuxt/image`, `@nuxtjs/i18n` with fourteen locale files under `internationalization/`, and `@nuxtjs/html-validator`. There is no `Dockerfile`, no compose file and no `.dockerignore`; `.gitignore` already excludes `node_modules`, `.output`, `.nuxt`, `.nitro` and `.env*`.

Then it works out the one runtime dependency that matters. The app does not call The Movie Database directly: `nuxt.config.ts` and `app/composables/tmdb.ts` both hardcode `apiBaseUrl` to a hosted proxy, with a commented `http://localhost:3001` beside it, and the image provider is `ipx` through that same proxy. The `proxy/` package is a Nitro app that reads `TMDB_API_KEY` from its runtime config, serves `/tmdb/**` through a cached handler and `/ipx/**` for images, and runs on port 3001 in development. So the container works with no secret at all against the public proxy, and the agent writes the Dockerfile that way by default, with the self-hosted proxy as a second service you opt into. It also notes in the pull request that the composable reads its own constant rather than `runtimeConfig.public.apiBaseUrl`, so pointing the app at your proxy is a one-line change in that file, not an environment variable.

The `Dockerfile` has a build stage on the Node 24 image that installs pnpm 11.1.2 with corepack, copies `package.json`, `pnpm-lock.yaml`, `pnpm-workspace.yaml` and the proxy's `package.json` first so `pnpm install --frozen-lockfile` caches as its own layer, then copies the rest and runs `pnpm build`. The runtime stage is the same Node image with only `.output/` copied in, a non-root user, `PORT=3000`, `HOST=0.0.0.0`, `EXPOSE 3000` and `node .output/server/index.mjs` as the command, which is exactly the repository's `start` script. A second stage builds the proxy with `nitropack build` for the compose file. A `.dockerignore` mirrors `.gitignore` and adds `playwright-report/` and `test-results/`.

Then it proves the image runs. In the sandbox the agent runs `docker compose up --build`, waits on port 3000, requests `/` and `/search` and checks the HTML renders server-side rather than returning an error page. It runs the repository's own checks against the result: `pnpm run test:unit` as `.github/workflows/tests-unit.yml` does, and the Playwright spec in `tests/e2e/` against the container by setting `BASE_URL`, which `playwright.config.ts` reads to point the Nuxt test utilities at a running host. It records `docker image ls` for the image size and fixes and rebuilds until the requests and the tests pass.

## Start it with one API call

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

```json
{
  "agent": "claude",
  "input": "Containerize this Nuxt app. Read package.json, pnpm-workspace.yaml, pnpm-lock.yaml, nuxt.config.ts, app/composables/tmdb.ts, proxy/package.json, proxy/nitro.config.ts, proxy/.env.example, .env.example, .gitignore, playwright.config.ts and .github/workflows/tests-unit.yml first, and list the Node and pnpm versions, the build and start commands, the workspace packages, the port and every external service the app calls at runtime. Write a multi-stage Dockerfile: a build stage that caches pnpm install in its own layer and runs nuxt build, and a runtime stage with only the .output directory, a non-root user, HOST and PORT set and EXPOSE for the port. Add a .dockerignore and a compose.yaml with the app service and an optional proxy service built from the proxy package that takes TMDB_API_KEY from the environment. Build it in the sandbox, start it, request the home page and the search page, run the unit tests and the Playwright spec against the container with BASE_URL, fix anything until they pass, and open a pull request with the image size 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 `Dockerfile`, `compose.yaml`, `.dockerignore` and a README section with the build and run commands. The description lists each choice and the file it came from: Node 24 and pnpm 11.1.2 from `devEngines`, the `.output/` runtime from the `start` script, port 3000 from `.env.example`, the public proxy default from `nuxt.config.ts`, and the proxy service built from the `proxy/` workspace package with `TMDB_API_KEY` from its `.env.example`.

The transcript on the session keeps every command with its output: each `docker compose build`, the container logs from Nitro starting, the requests to `/` and `/search` with their status codes, the unit test run and the Playwright run against the container, and any build that failed before the fix. The repository's own `Tests unit` workflow runs on the pull request too, so `vitest`, `eslint` and `nuxt typecheck` report on a change that touches no Vue file.

Before merging, a reviewer checks that the runtime image holds no `node_modules` from the build stage beyond what Nitro bundled into `.output/`, that the process runs as the non-root user, that no `.env` file is copied into the image, and that the proxy service is the only place `TMDB_API_KEY` appears. If the team wants the app to use the self-hosted proxy by default, one follow-up message to the same session changes the constant in `app/composables/tmdb.ts` and `nuxt.config.ts` and rebuilds.

## Run it on every new Nuxt project without a Dockerfile

A team containerizing several Nuxt apps starts one run per repository: a `POST /o/{org}/sessions` with that repository in `resources` and this prompt. Send an `Idempotency-Key` per repository so a retried request returns the run it already started.

Each run has its own sandbox with its own Docker daemon, so several builds proceed at once without sharing a pnpm store or port 3000. Up to the key's `max_concurrent` runs proceed in parallel, and a webhook on the key receives `run.completed` or `run.failed` for each turn with the `session_url` that holds the pull request.

## Why run it on Rush?

The time in this job goes to the loop: install, build, start, discover that the server bound to localhost inside the container, or that a native dependency pnpm was told not to build is missing at runtime, change one line, build again. On a laptop that loop competes with the dev server already on port 3000 and leaves images behind. In a Rush sandbox the agent has its own daemon and its own ports, and the sandbox parks when the turn ends.

The pull request carries its own proof. The requests against the container, the Playwright run with `BASE_URL` pointed at it and the image size sit in the transcript beside the commands that produced them, so a reviewer can see the app rendered from a clean image rather than from a machine that already had Node, pnpm and a warm `.nuxt` cache.

## Frequently asked questions

### Does the container need a TMDB API key?

Not by default. The app reads movie data through the proxy URL hardcoded in `nuxt.config.ts` and `app/composables/tmdb.ts`, which is a public hosted instance of the `proxy/` package. The key is only needed by the proxy service in the compose file, which you start when you want the app to go through your own proxy.

### Why is the runtime stage not an Alpine image with pnpm installed?

Because `nuxt build` produces a self-contained Nitro server in `.output/` that runs with plain `node`, so the runtime stage needs no package manager and no `node_modules` directory of its own. Copying only `.output/` is what keeps the image small and is what the repository's own `start` script already assumes.

### Does it respect the pnpm version the repository pins?

Yes. `package.json` pins pnpm 11.1.2 through `devEngines`, and the build stage activates exactly that version with corepack before running `pnpm install --frozen-lockfile`, so the lockfile resolves the same way it does in the `Tests unit` workflow.

### Can it run the end-to-end tests against the container?

Yes. `playwright.config.ts` passes `BASE_URL` to the Nuxt test utilities as the host to test against when the variable is set, so the agent runs the spec in `tests/e2e/` with `BASE_URL` pointed at the container instead of a dev server. The run and its result are in the transcript.
