# Dockerize a Fastify App With MySQL

Yes. An agent writes a multi-stage Dockerfile for your TypeScript Fastify app, adds it to Compose beside MySQL with a health check, boots the stack and opens a PR.

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

## What the agent does

The agent reads how the app is built and started before it writes the Dockerfile, because the answers are in several small files. In a Fastify app shaped like fastify/demo, `package.json` is an ES module project on `fastify` 5.10 with TypeScript 6.0, `@fastify/autoload`, `@fastify/env`, `knex` with `mysql2`, `postgrator` for migrations and `close-with-grace`; `build` runs `tsc` into `dist`, `start` runs `npm run build && node dist/server.js`, `dev` runs `tsx --env-file=.env ./src/server.ts`, and the `db:*` scripts run the files under `scripts/` with the same `--env-file`. `tsconfig.json` extends `fastify-tsconfig` with `rootDir: src` and `outDir: dist`. There is no Dockerfile; `docker-compose.yml` has one service, `db`, on `mysql:8.4` with a `mysqladmin ping` healthcheck and a named volume. CI in `.github/workflows/ci.yml` runs on Node 22 and 24 against a MySQL 8.4 service.

Three details change what a naive Dockerfile would do. `.gitignore` lists `package-lock.json`, `yarn.lock` and `pnpm-lock.yaml`, so there is no lock file to feed `npm ci`; the agent uses `npm install` and says in the pull request that a committed lock file is what makes the image reproducible, leaving that decision to the maintainers. `.npmrc` sets `ignore-scripts=true` and `min-release-age=7`, which the image inherits by copying the file before install. And `src/plugins/external/env.ts` validates the environment at startup with a schema that requires `MYSQL_HOST`, `MYSQL_PORT`, `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_DATABASE`, `COOKIE_SECRET`, `COOKIE_NAME` and `COOKIE_SECURED`, so a container started without them exits with a clear error, and `.env.example` already lists them with `RATE_LIMIT_MAX=4` for the tests.

The Dockerfile has a build stage and a runtime stage on the Node version CI tests, which the agent names in the pull request. The build stage copies `package.json` and `.npmrc`, installs, copies `src`, `@types` and `tsconfig.json`, and runs `npm run build`. The runtime stage installs with `--omit=dev`, copies `dist` and `migrations`, runs as the image's `node` user, exposes 3000, which `src/server.ts` listens on when `PORT` is unset, and starts with `node dist/server.js` rather than `npm start`, so the build does not rerun in the container and signals reach the process; `close-with-grace` in `server.ts` already handles them with `FASTIFY_CLOSE_GRACE_DELAY`. A `.dockerignore` excludes `node_modules`, `dist`, `.env` and `uploads/tasks`. In `docker-compose.yml` the agent adds an `app` service built from the Dockerfile, with `env_file: .env`, `MYSQL_HOST=db` overriding the file, `depends_on` on `db` with `condition: service_healthy`, and a one-shot `migrate` service built from the Dockerfile's build stage, where `tsx` is still installed, that runs `scripts/migrate.ts` once before `app` starts, since the migrations live in `scripts/` and are not run by the server.

For a container healthcheck it makes one small code change: `src/plugins/external/under-pressure.ts` already runs `SELECT 1` through knex every 5 seconds as its `healthCheck`, but exposes no route, so the agent adds `exposeStatusRoute` at `/health` to that plugin's config and uses it in the Compose healthcheck, which then reflects database connectivity rather than process liveness. It runs `docker compose up --build`, waits for `app` to turn healthy, requests `/health` and the home route, and runs `npm test`, which seeds the database and runs the Node test runner under `c8`, against the Compose MySQL. The pull request carries the Compose log and the health response.

## Start it with one API call

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

```json
{
  "agent": "claude",
  "input": "Dockerize this Fastify app. Read package.json, .npmrc, .gitignore, tsconfig.json, src/server.ts, src/app.ts, src/plugins/external/env.ts, src/plugins/external/under-pressure.ts, scripts/migrate.ts, .env.example, docker-compose.yml and .github/workflows/ci.yml first. Write a two-stage Dockerfile on a Node version from the CI matrix: build with tsc, then a runtime stage with production dependencies only, running dist/server.js as the node user on port 3000. Note that lock files are gitignored, use npm install, and say in the pull request what a committed lock file would change. Add a .dockerignore. Add app and one-shot migrate services to docker-compose.yml, depending on the db healthcheck, with env_file .env and MYSQL_HOST=db. Expose under-pressure's status route at /health and use it as the app healthcheck. Run docker compose up --build, request /health and the home route, run npm test against the Compose database, and open a pull request with the Compose log and responses.",
  "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 that adds `Dockerfile` and `.dockerignore`, extends `docker-compose.yml` with the `app` and `migrate` services, and makes one change in `src/plugins/external/under-pressure.ts` to expose the status route. No route, schema or repository file changes. The description names the Node image tag it chose from the CI matrix, quotes the Compose log through the point where `migrate` exits and `app` turns healthy, and includes the `/health` response and the home route's response.

A reviewer checks that the image copies `.npmrc` before `npm install`, so `ignore-scripts=true` and `min-release-age=7` apply in the build as they do on a laptop; that `.env` is excluded from the build context and `COOKIE_SECRET` arrives only through the environment; and that the `npm test` run in the transcript used the Compose database, since the test script seeds it first. The lock-file paragraph in the description is the decision the maintainers make, not the agent.

## Run it on every service without a Dockerfile

A script on your server lists the organization's repositories with `GET /o/{org}/github`, checks each for a `package.json` that depends on `fastify` and has no `Dockerfile`, and sends one `POST /o/{org}/sessions` per match with this prompt and that repository as the `resources` URL. A `request_id` per repository makes the script safe to re-run.

Set a `webhook_url` on the API key and Rush POSTs `run.completed` when each turn finishes, with the `session_url` to read the pull request from; a `run.failed` names the service whose stack the agent could not bring up.

## Why run it on Rush?

Containerizing a TypeScript service is a chain of small decisions: which Node, `npm ci` or `npm install` when there is no lock file, whether scripts run, how migrations happen, what the healthcheck means. The agent reads the files that answer each one, writes the decision into the pull request, and proves the result by bringing the stack up in its own sandbox and running the test suite against it. Your laptop stays out of it.

The pull request is reviewable on its own: the Compose log, the health response and the test run are in the transcript, so the reviewer reads rather than reproduces. Several services can be containerized at once, each in its own sandbox and branch.

## Frequently asked questions

### Why not npm ci?

Because `.gitignore` excludes every lock file, so there is nothing for `npm ci` to install from. The agent uses `npm install`, and the pull request says plainly that committing a lock file is what would make image builds reproducible. That is a repository policy, so it is left for the maintainers to decide.

### How do the migrations run?

Through a one-shot `migrate` service in the Compose file that runs `scripts/migrate.ts` once `db` is healthy, before `app` starts. The server does not run migrations itself; `postgrator` is driven from `scripts/`, so the agent keeps that separation rather than adding a startup hook.

### What does the health check actually test?

Database connectivity. `@fastify/under-pressure` is already configured with a `healthCheck` that runs `SELECT 1` through knex every 5 seconds; the agent only exposes its status route at `/health`. A container whose database connection is lost reports unhealthy, which a check on the home route would not.

### Will the tests pass inside the container?

The tests run from the sandbox against the Compose MySQL, the way CI runs them against its MySQL service, with `RATE_LIMIT_MAX=4` from `.env.example` because the rate-limit test depends on it. The transcript holds the `npm test` output. The image itself carries production dependencies only, so tests are not run inside it.
