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.
  • path is relative to the uploaded directory and can't point outside it.
  • runtime is python (optionally with a version, such as python-3.12), node or static. If you leave it out, it's detected from the files in path.
  • Python and Node services must listen on 127.0.0.1:$PORT. Static services have no start: AgentServe serves their output directory itself, with a fallback to index.html for client-side routes.

References between services#

Values in env can reference other services in the same stack with ${<service>.<field>}. Four fields exist:

referenceresolves toexample
${api.url}The service's public URLhttps://api-todo-app-8c1f.agentserve.sh
${api.host}Its public host nameapi-todo-app-8c1f.agentserve.sh
${api.internal_url}Its loopback addresshttp://127.0.0.1:54321
${api.port}Its local port54321

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:

  1. Every service is built, one after another, in dependency order.
  2. 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 by npm 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:

variablevalue
PORT, HOSTThe port to listen on, and 127.0.0.1
DATA_DIRThe service's persistent directory; see Data and persistence
AGENTSERVE1
AGENTSERVE_STACK, AGENTSERVE_SERVICEThe stack id and the service name
AGENTSERVE_URLThe service's own public URL
NODE_ENVproduction 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, or static if there is no start script and it depends on vite, astro, parcel or react-scripts.
  • requirements.txt, pyproject.toml, main.py or app.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.

Stuck? Your agent can read /skill.md, or connect it over MCP.