# Dockerize a Vue App

Yes. An agent writes a two-stage Dockerfile for your Vite-built Vue app with an nginx config for the history router, builds and runs it, and opens a pull request.

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

## What the agent does

The agent clones the app into its own cloud sandbox and reads how it is built before it writes a Dockerfile. In a Vue app like realworld-apps/vue-realworld-example-app, `package.json` has Vue `^3.5.0`, `vue-router` `^5.1.0` and `pinia` `^3.0.0` with Vite `^8.0.16`, the scripts are `build` (`vite build`), `serve` (`vite`), `preview`, `lint`, `format:check` and the Playwright `test` scripts, and the lockfile is `bun.lock`, which both `.github/workflows/playwright.yml` and `deploy.yml` honour with `oven-sh/setup-bun` and `bun install`. There is no Dockerfile, no compose file and no `.dockerignore`. `deploy.yml` builds `dist/` and attaches it to a GitHub release as a zip, which tells the agent the output is a static bundle rather than a Node server.

Two details decide the image. First, `src/main.js` imports `../realworld/assets/theme/styles.css`, and `.gitmodules` says `realworld` is a git submodule of the RealWorld spec repository; both workflows check out with `submodules: true`, so the build context must contain that directory or Vite fails on the first import. Second, `src/common/config.js` reads `import.meta.env.VITE_API_URL` with a fallback to `https://api.realworld.show/api`, and Vite bakes that value in at build time, so the API base is a build argument, not a runtime environment variable. `src/router/index.js` uses `createWebHistory`, so a deep link such as `/article/some-slug` or `/profile/someone` must be answered with `index.html` by the server. `index.html` loads Ionicons and Google Fonts from CDNs, which the container does not need to serve.

The Dockerfile it writes has two stages. The build stage starts from the official Bun image, copies `package.json` and `bun.lock` first so the install layer caches, runs `bun install --frozen-lockfile`, copies the rest of the context including `realworld/`, takes `VITE_API_URL` as an `ARG` and runs `bun run build`. The runtime stage starts from an nginx image, copies `dist/` into the web root and adds an `nginx.conf` with `try_files $uri $uri/ /index.html` for the router, long cache headers for the hashed files under `/assets/`, no caching for `index.html`, and gzip on. A `.dockerignore` keeps `node_modules`, `dist`, `playwright-report`, `test-results` and `.git` out of the context, and a `compose.yaml` passes the build argument and maps the port.

Before it opens anything, the agent runs `git submodule update --init`, builds the image in the sandbox, starts it with `docker compose up`, and checks three things with `curl` and the sandbox browser: `/` returns the app shell, `/article/example` and `/editor` return `index.html` with a 200 rather than nginx's 404, and a hashed file under `/assets/` comes back with the cache header. It opens the home page in the browser, which renders the global feed from the public API, takes a screenshot, and records what `docker images` prints for the image. It then runs `bun run lint` and `bun run format:check` so the new files do not break the existing `lint.yml` check.

## Start it with one API call

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

```json
{
  "agent": "claude",
  "input": "Dockerize this Vue app. Read package.json, bun.lock, vite.config.js, src/main.js, src/common/config.js, src/router/index.js, .gitmodules and the workflows under .github/workflows first. Write a two-stage Dockerfile: a build stage on the official Bun image that copies package.json and bun.lock before the rest, runs bun install --frozen-lockfile, takes VITE_API_URL as a build argument and runs bun run build with the realworld submodule present in the context; and a runtime stage on nginx that serves dist with an nginx.conf using try_files to index.html for the history-mode router, long cache headers for /assets, no cache for index.html and gzip enabled. Add a .dockerignore and a compose.yaml that passes the build argument. Run git submodule update --init, build the image, start it with docker compose, confirm with curl that / and a deep link like /article/example return the app shell with status 200 and that a hashed asset carries the cache header, open the home page in the browser and take a screenshot, and record the image size from docker images. Run bun run lint and bun run format:check. Open a pull request with the files, the commands to build and run, the screenshot, and a note that VITE_API_URL is fixed at build time.",
  "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 adding `Dockerfile`, `nginx.conf`, `.dockerignore` and `compose.yaml`, with no change to `package.json`, `vite.config.js` or the workflows. The description explains each choice from what the agent read: the Bun image because of `bun.lock` and the workflows, the submodule step because of the stylesheet import in `src/main.js`, the `try_files` rule because of `createWebHistory`, and the build argument because `VITE_API_URL` is read at build time. It includes the three `curl` results, the image size `docker images` printed in the sandbox, the screenshot of the home page, and the commands to build and run locally.

The session read names the repository and the pull request, and the screenshot is one of the session's artifacts with a download link that lasts an hour per read. The transcript at `GET /o/{org}/sessions/{id}/messages` keeps the submodule update, the `docker build` layers, the compose start and every `curl`, so a reviewer can see the deep link test without checking the branch out.

Before merging, a reviewer confirms the API base the team deploys against is the one passed as the build argument, decides whether CI should build the image on every pull request, which is a one-step addition to `playwright.yml` or a new workflow, and checks that the nginx config does not cache `index.html`, since a cached shell would keep pointing at old hashed assets after a deploy.

## Run it on every new Vue repository

A team that starts front-end repositories from a template sends this request once per new repository, from a script or the console, with `resources` pointing at it and a `request_id` per repository so a retry returns the run already started. Each run has its own sandbox, branch and pull request.

Set a webhook URL on the key and Rush POSTs `run.completed` with `data.session_url` when the turn ends, so a handler can read the pull request URL and post it where the team will see it. A review request to add a health endpoint or a different API URL is a follow-up on the same session, which wakes the parked sandbox with the image already built.

## Why run it on Rush?

A Dockerfile that was never built is a guess, and the two things that break this one, the missing submodule and a router deep link answered with a 404, only show up when the image runs. The agent builds and runs it in its own sandbox, checks the deep links with `curl`, and loads the page in a browser before the pull request exists, so a reviewer sees evidence rather than a promise.

The evidence stays with the result: the build layers, the compose start, each `curl` and the screenshot are on the session. Several front-end repositories can be containerised at once, each in its own sandbox, without any of the builds touching a developer's Docker daemon.

## Frequently asked questions

### Why is the API URL a build argument and not an environment variable?

Because `src/common/config.js` reads `import.meta.env.VITE_API_URL`, and Vite replaces that at build time, so the value is in the bundle by the time nginx serves it. The pull request says so and shows `--build-arg` in the build command. Making it runtime-configurable would need a small change to the app, which can be a follow-up.

### Does the image need the realworld submodule?

Yes, for the build stage. `src/main.js` imports `../realworld/assets/theme/styles.css`, so the submodule must be checked out in the build context, as both existing workflows do with `submodules: true`. The runtime stage copies only `dist/`, so the submodule is not in the final image.

### Will deep links work?

That is what the `try_files $uri $uri/ /index.html` rule in `nginx.conf` is for, since `src/router/index.js` uses `createWebHistory`. The agent requests `/article/example` and `/editor` on the running container and quotes the 200 responses in the pull request.

### Does it change the existing release workflow?

No. `deploy.yml` keeps zipping `dist/` onto a GitHub release, and `playwright.yml` keeps running the end-to-end tests against the dev server. Building the image in CI is a suggested next step in the description, not part of this change.
