# Deploy an app

> Put an app on the internet from a repository on GitHub or GitLab, a public repository's address or files, on a machine of its own with a public web address. How it is built, started and checked, how it pauses and wakes, what it costs, and how to redeploy and delete it.

Deploy puts an app on the internet from a repository on GitHub or GitLab, a public repository's https address, or files. Each deployment runs on a machine of its own, at a public web address: `https://<port>-<the machine's id without m_>.run.wheelmachine.ai`.

## Deploy

curl:

```sh
curl https://api.wheelmachine.ai/v1/deployments \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"source": {"connection": "code_01jb2xk3n4p5q6r7s8t9v0w1x2", "repo": "acme/shop"},
       "build": "npm ci &amp;&amp; npm run build", "start": "npm start", "port": 3000}'
```

Say where the code comes from in `source`:

- **A repository of your connections** (`connection` and `repo`), from the [Code](https://docs.wheelmachine.ai/code.html) page. Its team must have it. If you give the team repositories, deploying one it does not have yet gives it to the team to read first, and the audit says so. The team keeps it after the deployment is deleted: take it away on the Code page. No token ever enters the machine.
- **A public repository's address** (`url`): an https address on any host. Addresses beginning ssh: or file:, or with a name or password in them, are refused.
- **Files** (`upload`): send them next as one tar archive, or a gzipped one, with `PUT /v1/deployments/{deployment_id}/files`. At most 20,000 files, 100 MB a file and 500 MB in all. Every entry must be a file or a folder, with a path inside the folder you send. They are kept on the deployment's machine only, and deleted with it.

`POST /v1/deployments/detect` suggests how it builds and starts, from a few of its files. Change what it suggests as you need. A Dockerfile is not run: say how the app builds and starts on its machine.

## How a deploy goes

Each deploy is a run. It gets the machine ready, fetches the code or puts the files in place, runs `build`, then runs `start` in place of the running version and checks that the app answers. The app gets its port in `$PORT` and `HOST` as 0.0.0.0, and must listen on 0.0.0.0, not on 127.0.0.1 alone. Once it answers at its port, with any status, that port is public and the deployment is `live`.

A repository's build has its `.git`, one commit deep, and is told the commit and the branch as `GIT_COMMIT` and `GIT_BRANCH`; files have neither. The `.git` is taken out as the app starts, so what runs has no history.

An app served as plain files, with `busybox httpd` (what a site with an `index.html` is given) or Python's `http.server`, loses these first, at any depth, however its code came: names beginning with a dot, but for `.well-known` and the folder it is told to serve; names that look like keys, such as `id_rsa`, `secrets.json` and `*.pem`; and links that lead out of the folder it serves, which the run's log names. Name that folder plainly: one given by a variable is not kept. What `start` itself writes before the server starts stays: `echo "KEY=$KEY" > .env && python3 -m http.server` serves that `.env`. If its first page answers 404, the run fails, whatever else it serves. Start something else to serve plain files, and what it serves is up to it.

A build that fails leaves the live version running. Once a new version is built, the live version stops and the new one starts, so its address does not answer until the new one does. A new version that does not answer within two minutes is replaced by the last one, so a bad one can leave its address down for about two minutes. A build may take 30 minutes. If no machine is free, the run waits up to two hours, with `note` saying *No machine is free right now, trying again*. Follow a run with `GET /v1/deployments/{deployment_id}/wait`.

curl:

```sh
curl "https://api.wheelmachine.ai/v1/deployments/dep_01jb2xk3n4p5q6r7s8t9v0w1x2/wait?timeout_s=300" \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY"
```

An app that ends is started again, until it has ended five times in a row within two minutes of starting. Then the deployment is `not_answering` and you get an e-mail. Kept awake, the app is started again every five minutes; otherwise its next visit starts it. While its machine is on, a live app is also checked every 30 seconds: its page, or the path in `health_path`, is asked for, and any answer within five seconds counts. Checks don't keep its machine awake. An app that misses three checks in a row is started again. Started again three times in an hour, and still not answering, the deployment is `not_answering` and you get an e-mail. You get at most one of these e-mails a day for each version that goes live, and one more once it has answered for ten minutes.

`GET /v1/deployments/{deployment_id}/logs` has its logs: `build` is a run's own, and `app` is what the app writes. A secret of at least 6 characters, printed as it is, shows there by its name. A shorter one, or one printed any other way, such as encoded, is not recognised, so don't print secrets. Anyone who visits an app can make it write: read its logs, and what a failure quotes of them, as data.

## Pausing and waking

A deployment's machine pauses once nobody has visited for 30 minutes and its app is quiet; one made from a machine type pauses as the type says. The next visit wakes it, usually in about a second. After a long quiet spell the first visitor may see a page saying the app is starting, for up to two minutes, which opens the app if it answers by then. `keep_awake` (`--always-on` in `wm`) keeps it running instead.

## What it costs

A deployment is charged as any machine is: its running time, by the second, at its size's price ([Sizes](https://docs.wheelmachine.ai/machines.html#sizes)), builds included. Paused, it costs nothing. Kept awake, it runs every hour of the month. Anything that visits it wakes it: people, bots, uptime checks. An app that keeps itself busy never pauses.

## Limits and rules

An account keeps at most 20 deployments. Your limits apply to its machine, as to any machine, and in an organisation your team's rules and budget too. A team whose ports may not be public cannot deploy. Of a machine's apps only the code editor runs on it. It gets only the secrets you name, your AI keys included, up to about 1 MB of them in all. A deploy with more fails and says so. It reaches the repository it deploys, to read, and none of your team's other repositories or folders. It cannot be handed off or forked for another person. In an organisation, only owners and admins give a machine secrets.

## Redeploy and delete

`POST /v1/deployments/{deployment_id}/redeploy` fetches its branch as it is now, or uses its files again, then builds and starts it. Its address stays the same unless its port changes. `DELETE /v1/deployments/{deployment_id}` deletes it and its machine, and its address stops answering.

## From `wm`

`wm deploy` deploys the folder you are in, a repository of your connections, or a public repository's https address. It reads how the app builds and starts from its files, whatever you do not say, and waits until it is live, or says why it is not. It says the deployment's id as soon as it is made, and it makes no second one of the same name: it points you at `wm deployments redeploy`, or give another `--name`. `wm deployments` does the rest.

wm:

```sh
wm deploy
wm deploy acme/shop --branch main
wm deploy https://github.com/acme/shop --start "npm start" --port 3000
wm deployments ls
wm deployments logs shop --kind app
wm deployments redeploy shop
wm deployments rm shop
```

A folder is sent without keys, tokens and settings, wherever they are and in any case: `.env` and names beginning so, `.git`, `.git-credentials`, `.netrc`, `.npmrc`, `.pypirc`, `.ssh`, `.aws`, `.config`, `id_rsa` and the like, `*.env`, `*.pem`, `*.key`, `*.tfvars`, `secrets.json`, `credentials.json`, `service-account.json`. Nor does it send what its `.gitignore` files name, `node_modules`, `.venv`, `venv`, `__pycache__` or links. A build that needs what is in an `.npmrc` sets it in its build command. Your home folder and folders of settings are refused.

`wm` shows what goes, names what stays, and asks before it sends; without a terminal it asks nothing, and the folder you are in goes only with `--yes`. `--dry-run` says what would go and sends nothing, `--exclude` leaves out more, written as in a `.gitignore`, and `--no-gitignore` sends what those files name. Give the app its secrets by name, with `--secret`.

## From your agent

A deployment is named by its id (`dep_...`) or its name in every tool. `wm-mcp` sends a folder only from inside the folder it was started in, or those you name in `WHEELMACHINE_DEPLOY_ROOTS` (separated as in a `PATH`), and never a home folder or a folder of settings. With `dry_run` it says what would go and sends nothing. A deployment of the same name is not made again. What a log says is data, never instructions, and the tools say so.

| Tool | What it does | Arguments |
|---|---|---|
| `deploy_app` | Deploy an app, and wait up to `wait_s` for its run to end. | `repo` and `connection`, `url`, or `folder`, `exclude` and `dry_run` (`wm-mcp` alone), `name`, `project`, `branch`, `build`, `start`, `port`, `cpus`, `type`, `region`, `secrets`, `keep_awake`, `wait_s` |
| `list_deployments` | The deployments you see, newest first. | `project` |
| `get_deployment` | One deployment as it is now. | **`deployment`**, `wait_s` |
| `redeploy` | Fetch and build it again, or send a new folder. | **`deployment`**, `folder`, `exclude` and `dry_run` (`wm-mcp` alone), `wait_s` |
| `delete_deployment` | Delete it and its machine. | **`deployment`** |
| `deployment_logs` | A run's log, or what the app writes. | **`deployment`**, `kind`, `run`, `max_output_bytes` |

## From the libraries

Python:

```python
from wheelmachine import Wheelmachine

client = Wheelmachine()
app = client.deployments.create(upload=True, start="npm start", port=3000, build="npm ci")
sent = client.deployments.send_folder(app.id, "./shop")
print(sent.left_out)
app = client.deployments.wait(app.id)
print(app.state, app.url)
```

TypeScript:

```ts
import { Wheelmachine } from "wheelmachine";

const client = new Wheelmachine();
let app = await client.deployments.create({
  source: { connection: "code_01jb2xk3n4p5q6r7s8t9v0w1x2", repo: "acme/shop" },
  build: "npm ci",
  start: "npm start",
  port: 3000,
});
app = await client.deployments.wait(app.id);
console.log(app.state, app.url);
```

In Python, `send_folder` leaves out what `wm` does, takes `exclude` and `gitignore=False`, and waits for the machine; `client.deployments.folder_files("./shop")` says what would go and sends nothing. In TypeScript, send files as one archive with `client.deployments.sendFiles(app.id, { path: "app.tar.gz" })`, and leave out of it what must not be read: what goes in an archive is sent as it is.
