WheelmachineDocs
Open app

Deploy an app

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
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:

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
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), 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
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.

ToolWhat it doesArguments
deploy_appDeploy 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_deploymentsThe deployments you see, newest first.project
get_deploymentOne deployment as it is now.deployment, wait_s
redeployFetch and build it again, or send a new folder.deployment, folder, exclude and dry_run (wm-mcp alone), wait_s
delete_deploymentDelete it and its machine.deployment
deployment_logsA run's log, or what the app writes.deployment, kind, run, max_output_bytes

From the libraries#

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
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.