Docs / Guides
Multi-service stacks
Describe several services in agentserve.yaml, wire them together with references, and control start order.
A stack can run up to 4 services: say, an API, a frontend and a worker. You describe them in
agentserve.yaml (or agentserve.yml or agentserve.json) at the root of the
directory you deploy. This guide covers how services find each other and the order they build and start in. For every
field, see the agentserve.yaml reference.
A two-service manifest#
This is the manifest of a todo app with a FastAPI backend and a React frontend:
name: todo-app
ttl: 1h
services:
api:
path: ./backend
runtime: python-3.12
start: uvicorn main:app --host 127.0.0.1 --port $PORT
health: /healthz
env:
CORS_ORIGINS: ${web.url}
web:
path: ./frontend
runtime: static
build: npm run build
output: dist
env:
VITE_API_URL: ${api.url}
depends_on: [api]
- Service names are lowercase letters and digits, start with a letter, and are at most 20 characters. The name is also the first part of the service's host name.
pathis relative to the uploaded directory and can't point outside it.runtimeispython(optionally with a version, such aspython-3.12),nodeorstatic. If you leave it out, it's detected from the files inpath.- Python and Node services must listen on
127.0.0.1:$PORT. Static services have nostart: AgentServe serves theiroutputdirectory itself, with a fallback toindex.htmlfor client-side routes.
References between services#
Values in env can reference other services in the same stack with
${<service>.<field>}. Four fields exist:
| reference | resolves to | example |
|---|---|---|
${api.url} | The service's public URL | https://api-todo-app-8c1f.agentserve.sh |
${api.host} | Its public host name | api-todo-app-8c1f.agentserve.sh |
${api.internal_url} | Its loopback address | http://127.0.0.1:54321 |
${api.port} | Its local port | 54321 |
Use url for anything a browser calls, such as a frontend's API base URL or a CORS origin. Use
internal_url for server-to-server calls that don't need to leave the machine. A reference to a service
that doesn't exist is rejected when you deploy. References can point both ways: in the example, api
references web and web references api. This works because every service's URL
is fixed before anything builds.
The local port is assigned by AgentServe, and it can change if the port is taken when a service restarts. Prefer
url where you can; the public URL never changes.
Build and start order#
depends_on lists services that must come first. AgentServe sorts the services by it and uses that
order for everything:
- Every service is built, one after another, in dependency order.
- Every service is started in the same order. Each process must answer its health check before the next one starts.
The health check calls health (for example /healthz) and expects a 2xx response. Without
health, any response below 500 on / counts. A service has 60 seconds to become healthy.
Static services count as ready once built.
A cycle in depends_on is rejected with the cycle spelled out, for example
depends_on has a cycle: api -> worker -> api. References don't create ordering; only
depends_on does.
Build-time and runtime env#
The same env block, with references resolved, is applied to the build commands and to the running
process. What differs is when a value takes effect:
- Static and bundled frontends read env at build time.
VITE_API_URL: ${api.url}is baked into the JavaScript bundle bynpm run build, so changing it means redeploying. - Python and Node processes read env when they start, so they get freshly resolved values on every start and restart.
Besides your own variables, every service gets:
| variable | value |
|---|---|
PORT, HOST | The port to listen on, and 127.0.0.1 |
DATA_DIR | The service's persistent directory; see Data and persistence |
AGENTSERVE | 1 |
AGENTSERVE_STACK, AGENTSERVE_SERVICE | The stack id and the service name |
AGENTSERVE_URL | The service's own public URL |
NODE_ENV | production unless you set it (npm install runs with development so devDependencies are installed) |
Only PATH, HOME, LANG, TMPDIR, USER and
SHELL are passed through from the server's own environment. Python services get a virtualenv on their
PATH.
Inside a service, AGENTSERVE_URL is the service's own URL. It is not the AgentServe server address
that the CLI reads from the same variable name.
Without a manifest#
If the upload has no manifest, AgentServe infers one. It first looks at the root of the upload:
package.json:node, orstaticif there is nostartscript and it depends onvite,astro,parcelorreact-scripts.requirements.txt,pyproject.toml,main.pyorapp.py:python.index.html:static.
A match at the root gives one service, named web if it's static and app otherwise. If
the root matches nothing, each top-level subdirectory is checked the same way and becomes its own service, named after
the directory. A backend/ and frontend/ layout becomes two services,
backend and frontend.
Inferred stacks have no env, no references and no depends_on, so the services can't find
each other. As soon as one service calls another, write a manifest. Start commands are inferred too: a Python service
with FastAPI( or Starlette( in main.py, app.py,
server.py or api.py runs under uvicorn, one with Flask( under
flask run, and a Node service runs npm start. Anything else needs an explicit
start.