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 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 && npm run build", "start": "npm start", "port": 3000}'Say where the code comes from in source:
- A repository of your connections (
connectionandrepo), from the Code 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, withPUT /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 "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 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 shopA 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#
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)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.
Open in Claude