Deploy

agenthost.yaml

Four settings a project commits in its own tree to control its build. runtime, install, build and start, what each runtime does with them, and what happens when one is wrong.

The runtime conventions are fixed on purpose: one entrypoint, one dependency file, no start command and no port in a tool call. That fits source an agent just wrote. It is too rigid for a repository that already exists, which is usually the reason for connecting one.

agenthost.yaml is the step past our conventions for when they don't quite fit. Commit it at the root of your project and it replaces individual build steps while agenthost keeps the base image, the port wiring and the layering:

runtime: node:22
install: npm ci --omit=dev
build: |
  npm run build
  node scripts/postbuild.js
start: node dist/server.js

Every key is optional. An empty file is a project that says nothing, and the conventions apply unchanged. agenthost.yml is accepted too, and read the same way.

It applies to every source, not just GitHub.

Inline files, a streamed upload and a cloned repository all arrive at the build as the same tree, so a committed build file is honoured whichever way the source got there.

The four settings

SettingWhat it does
runtimeThe runtime to build on, as "<language>[:<version>]". Same syntax as the tool argument: node:22, python:3.13, php:8.4, static.
installReplaces the runtime's dependency-install command.
buildReplaces the runtime's build command.
startReplaces the command the container runs.

Every value is a shell command, run from the project root. npm run build, ./scripts/build.sh and a ten-line block are all the same kind of thing. A committed script is reached by path like any other command, and the executable bit travels with the file, so ./scripts/build.sh works without a chmod step of yours.

Use a YAML block for several lines:

build: |
  npm run build
  node scripts/postbuild.js

A multi-line command runs with set -e, so the first failing line fails the build. Without that the exit status would be the last line's, and a failed install followed by a successful build would ship stale output as a green deploy.

What each runtime does with it

Runtimeinstall replacesbuild replacesstart replaces
nodenpm ci || npm installnpm run build --if-presentnpm start
pythonpip install --no-cache-dir -r requirements.txtnothing runs by defaultgunicorn app:app -b 0.0.0.0:8000
phpcomposer install when composer.json is presentnothing runs by defaultnot accepted
staticnot acceptednot acceptednot accepted

A php app is served by Apache from its document root and a static site is files served by nginx. Neither has a start command of ours to replace, and a static site has no build to apply steps to, so setting those fails the deploy rather than quietly ignoring what you committed. That is a fixed limit of those two runtimes, not something to work around — agenthost ignores any Dockerfile in your tree and always builds from its own recipe.

The port is not one of the settings. It stays fixed per runtime and your process must listen on it: 3000 for node, 8000 for python, 80 for php and static. Node and Python builds also get PORT in the environment, so node dist/server.js reading process.env.PORT lands on the right one.

What runtime decides

runtime in the file is taken ahead of anything inferred from your file names, and it is re-read on every deploy. Commit a change to it and the next push builds on the new one, with no tool call to re-run.

An explicit runtime argument on create_app or deploy_app still wins over the file. That order is deliberate: the file is what the project says about itself, and the argument is someone overriding it for this deploy.

See Choosing a version for what a version pins and which ones are supported.

A custom install costs the dependency cache

By default the build copies your manifest first, installs from it, and only then copies the rest of the tree. An unchanged dependency set reuses the install layer, so most deploys skip it.

A custom install may read files the manifest doesn't name (a lock file, a second requirements file, a workspace layout), so the whole tree is copied before it runs. That means it runs on every deploy. It is usually a fair trade for a build that works at all, but it is worth knowing why a repository with an install line builds slower than one without.

When something is wrong

The file is strict, and every failure names the file and stops the deploy. A build that quietly ignored a setting someone committed and believed would be worse than a failed one.

What you wroteWhat happens
Invalid YAMLThe deploy fails with the parser's own message, including the line and column.
A key that isn't one of the fourRejected by name, with the supported settings listed. A misspelling is not silently dropped.
A value that isn't a command, or is emptyRejected, with the block syntax shown for multi-line commands.
A document that isn't a mapping (a list, a bare string)Rejected, with an example of the shape it expects.
install, build or start on static, or start on phpRejected, naming the setting that runtime doesn't accept.

A committed Dockerfile is ignored.

agenthost always builds from its own generated recipe. A Dockerfile in your tree is stripped before the build, so it never takes over — install, build and start from your agenthost.yaml are what shape the build. runtime still counts, because it decides which port traffic is routed to.

When four settings aren't enough

install, build and start are the whole of what you can customize. Anything beyond them — system packages, a second process, a language we don't have a runtime for, a multi-stage build of your own — isn't supported. agenthost always builds from its own recipe, and a Dockerfile committed to your tree is ignored rather than used. If your project genuinely needs one of those, agenthost isn't the right home for it.

Where to go next