` tag, make sure the target repository exists in the project. See [Registries](../../project_admin/registries).
## How image build and deploy fit into MeshAgent
The image workflow sits between source files and a deployable MeshAgent service:
1. `meshagent build PATH --tag ...` streams a local directory into a room and builds an image there.
2. `meshagent deploy --tag ...` creates or updates a room service from an existing image.
3. `meshagent deploy PATH --tag ...` does both in one command.
This complements the normal service packaging flow. In practice, a room-connected agent can:
* prepare files in the room
* build an image from those files inside the room
* deploy a service that uses that image
That makes this workflow especially useful for cases where an agent is expected to build, test, and ship a containerized service for you from inside MeshAgent.
## Commands
### `meshagent build`
Use `build` when you want MeshAgent to build an image inside a room.
`meshagent build` streams a local directory into the room as the build context. If that directory contains a `Dockerfile`, MeshAgent uses it during the room-side build. The build runs in MeshAgent rather than requiring a local Docker daemon on the machine that started the command.
Example:
```bash theme={null}
meshagent build ./my-app \
--room myroom \
--tag registry.meshagent.com/myproject/my-app:dev
```
Key things to know:
* `PATH` is required and becomes the streamed build context
* `-t` or `--tag` is required
* `--room` is required unless `MESHAGENT_ROOM` is already set
* `-f` or `--file` selects a Dockerfile relative to `PATH`; otherwise MeshAgent uses `PATH/Dockerfile` or `PATH/Containerfile`
* build logs stream back through the CLI while the build runs
* this is a good fit for agent-driven build workflows inside a room because the build happens in MeshAgent, not on the local machine
For example, use a Dockerfile in a subdirectory with the same flags as Docker:
```bash theme={null}
meshagent build ./my-app \
--room myroom \
-t registry.meshagent.com/myproject/my-app:dev \
-f refresher/Dockerfile
```
### `meshagent deploy`
Use `deploy` when you already have an image and want to create or update a room service from it, or when you want to build and deploy from a local directory in one step.
Example deploying an existing image:
```bash theme={null}
meshagent deploy \
--room myroom \
--tag registry.meshagent.com/myproject/my-app:dev \
--env APP_ENV=dev
```
Example building and deploying from a local directory:
```bash theme={null}
meshagent deploy ./my-app \
--room myroom \
--tag registry.meshagent.com/myproject/my-app:dev \
--file refresher/Dockerfile \
--env APP_ENV=dev
```
When deploying, you can also:
* mount room storage with `--room-mount`
* mount project storage with `--project-mount`
* mount another image with `--image-mount`
* mount scratch storage with `--empty-dir-mount`
* inject a `MESHAGENT_TOKEN` with `--meshagent-token`
* run the deployed container as a service account with `--run-as`
* inject a service-account secret-backed environment variable with `--env-secret NAME=SECRET_ID`; this requires `--run-as`
* publish a route with `--domain` when the service exposes exactly one published port
When you pass `PATH`, `meshagent deploy` builds first and then deploys the resulting image tag. When you omit `PATH`, it deploys the image tag directly. In both cases the service name is derived from the image tag, and MeshAgent updates the existing room service when one already exists.
## How image build and deploy work with service packaging
`meshagent build` and `meshagent deploy` are complementary to service packaging.
Use this workflow when:
* your workflow is image-first
* you want to build inside a room
* you want a fast path from image tag to room service
* you want an agent in the room to participate in building and shipping the service
Use the service packaging docs when:
* you are writing or reviewing a `meshagent.yaml`
* you need a fuller service spec with more explicit deployment structure
* you are working with ServiceTemplates or broader service configuration
In other words, `meshagent build` and `meshagent deploy` are the room-native image workflow, while service manifests are the broader service-definition workflow.
## Related guides
* [Deploy a Web App](../deployment/deploy_web_app)
* [Registries](../../project_admin/registries)
* [Service YAML](../deployment/deploy_services)
* [MeshAgent Base Images](./meshagent_base_images)
* [Optimizing Containers](./optimizing_containers)
* [MeshAgent CLI Reference](../../reference/meshagent_cli_help)
# Optimizing Containers
Source: https://docs.meshagent.com/services/containers/optimizing_containers
MeshAgent runs room and project services as containers. You can also point to external services if you host them yourself. When you deploy a service that uses a container image, MeshAgent will pull, start, and keep that image in sync with the room or project it belongs to.
**How fast a service becomes available depends primarily on how quickly its container image can be pulled and started.** To make startup faster and more reliable, we publish [eStargz (“stargz”)](https://github.com/containerd/stargz-snapshotter?tab=readme-ov-file) optimized images that allow for **lazy pulling**, a technique that can reduce pull-to-start time by up to \~75% for common workloads. If you are deploying custom services with your own container image we highly encourage you to optimize them with stargz to ensure the services start as quickly as possible.
**Use MeshAgent base images when possible.** They are optimized and include eStargz variants:
* [Docker Hub](https://hub.docker.com/r/meshagent/)
* [Google Artifact Registry](https://console.cloud.google.com/artifacts/docker/meshagent-public/us-central1/images)
In MeshAgent cloud rooms, the room host already precaches `meshagent/cli:default`, `meshagent/python:default`, `meshagent/node:default`, `meshagent/python-sdk:default`, `meshagent/python-sdk-slim:default`, and `meshagent/node-sdk:default` at startup. For those stock images, the `:default` reference is usually the right first choice. The `-esgz` variants still matter most for custom images and for published images outside that default warm set.
## Why optimization matters (and how lazy pulling helps)
How fast an agent or service starts is directly related to how long it takes to pull and start its container image. Images that are large, slow to decompress, have unnecessary files or dependencies can take significantly longer to start.
Stargz improves startup time by enabling **lazy pulling**. This allows the container to start before the full image is downloaded. Only the files needed during initial setup are fetched and the rest of the image is streamed on demand if/when needed.
## How MeshAgent uses your images
* When a room starts, MeshAgent pulls and runs the images for every room service attached to that room. Pull time directly affects time-to-ready.
* If you redeploy a **room service**, MeshAgent detects the change and reconciles the running container automatically.
* If you redeploy a **project service**, restart the room for the change to take effect in that room.
* If you are deploying a CLI process agent or VoiceBot, use the `meshagent/cli` base image by default. If the container needs bundled Playwright browsers for in-container browser automation, use `meshagent/cli-playwright` instead. Both are already optimized and support the standard flags (tools, rules, room-rules, etc.), so you usually do not need to build a custom image.
* If you reference an **external service** you host yourself, MeshAgent skips pulling/running a container and just routes to your endpoint. Container optimizations only matter for services you package with a container image.
## What Stargz is and how we use it
We publish images in an **eStargz/stargz** format and run with the [stargz snapshotter](https://github.com/containerd/stargz-snapshotter?tab=readme-ov-file) (a containerd plugin). This makes image layers *seekable* (the runtime can jump directly to specific files instead of downloading the whole layer first):
* Instead of downloading and decompressing an entire layer before start, the runtime lazily fetches just the files that are actually touched at startup.
* Startup is faster (lower cold-start latency) and data transfer is smaller, especially for larger Python/Node/ML stacks.
* Lazy pulling requires the containerd stargz-snapshotter; if it is not present, the image pulls and runs like a normal OCI/Docker image (just without the lazy-pull speedup).
* MeshAgent does not need a special tag or flag: the runtime decides. If the host has the stargz snapshotter, your stargz-compressed image is lazily pulled; if not, it is pulled normally.
We publish **`-esgz` image variants** that are optimized for lazy pulling via the stargz snapshotter. On environments without the snapshotter, these `-esgz` images behave like normal images; they just don’t get the lazy-pull speedup.
What we do on our images:
* Build stargz variants alongside the normal tags.
* Provide prefetch lists (the small set of files the agent touches during boot: entrypoint, deps, config) so the snapshotter pulls only what is needed up front.
## When deploying custom services
1. Use MeshAgent base images when possible (they already include stargz builds with `-esgz` tag). If building your own image, we recommend optimizing it using stargz so your service can start quickly.
2. Keep images slim: install only what you need, clean caches, and avoid large unused assets.
3. You can either publish only a stargz-optimized image (recommended -- avoids storing two images in your registry) or publish both a standard and an optimized version. To create stargz images we recommend using [nerdctl here](https://github.com/containerd/nerdctl) and [ctr-remote here](https://github.com/containerd/stargz-snapshotter/blob/main/docs/ctr-remote.md).
**Sample Steps**
```bash theme={null}
# Option 1: Build → optimize → push only the eStargz image (recommended)
# 1. Build your app image locally (no push yet)
docker buildx build \
-t YOUR_REPO/YOUR_IMAGE:build-temp \
--load \
.
# 2. Convert the image to eStargz
nerdctl image convert --estargz --oci \
YOUR_REPO/YOUR_IMAGE:build-temp \
YOUR_REPO/YOUR_IMAGE:esgz
# 3. Push only the optimized image
nerdctl push YOUR_REPO/YOUR_IMAGE:esgz
# MeshAgent manifest:
# container:
# image: "YOUR_REPO/YOUR_IMAGE:esgz"
```
```bash theme={null}
# Option 2: Build & push a standard image + a stargz-optimized version
# 1. Build and push your standard image (any tag)
docker buildx build \
--tag YOUR_REPO/YOUR_IMAGE:latest \
--platform linux/amd64 \
--push
# 2a: Convert to stargz with nerdctl
nerdctl image pull YOUR_REPO/YOUR_IMAGE:latest
nerdctl image convert --estargz --oci YOUR_REPO/YOUR_IMAGE:latest YOUR_REPO/YOUR_IMAGE:esgz
nerdctl push YOUR_REPO/YOUR_IMAGE:esgz
# OR 2b: Convert to stargz with ctr-remote
# Then optimize to eStargz and push (needs access to your containerd socket, e.g., /run/containerd/containerd.sock)
ctr-remote image pull YOUR_REPO/YOUR_IMAGE:latest
ctr-remote image optimize --oci YOUR_REPO/YOUR_IMAGE:latest YOUR_REPO/YOUR_IMAGE:esgz
ctr-remote image push YOUR_REPO/YOUR_IMAGE:esgz
# Reference the tag you prefer in your service manifest:
# container:
# image: "YOUR_REPO/YOUR_IMAGE:esgz"
```
## Related Topics
* [Service YAML](../deployment/deploy_services): write and deploy service manifests with the MeshAgent CLI.
# Deploy a built in Agent
Source: https://docs.meshagent.com/services/deployment/deploy_builtin_agent
Deploy a process-backed agent from MeshAgent CLI flags, or generate a starting service manifest from those flags.
Deploy a process-backed agent with [`meshagent process`](../../reference/meshagent_cli_help#meshagent-process).
Use `meshagent process deploy` when the CLI flags describe the agent you want to run. Use `meshagent process spec` when you want MeshAgent to generate a starting service manifest that you can review, edit, and deploy with `meshagent service`.
## Deploy directly with `meshagent process deploy`
```bash bash theme={null}
meshagent process deploy \
--service-name my-chatbot \
--agent-name my-chatbot \
--channel chat \
--web-search \
--storage \
--room myroom
```
`meshagent process deploy` generates the starting service manifest and deploys it immediately.
Use this when:
* the built-in CLI flags describe the agent
* the manifest does not need changes before deployment
* you want to deploy the agent to a room or project from one command
Pass `--room myroom` for a room service. Omit `--room` to deploy the service project-wide.
## Generate a service manifest with `meshagent process spec`
`meshagent process spec` generates the starting service manifest for a process-backed service and writes it to stdout.
```bash bash theme={null}
meshagent process spec \
--service-name my-chatbot \
--agent-name my-chatbot \
--channel chat \
--web-search \
--storage > meshagent.yaml
# Deploy into one room
meshagent service create --file meshagent.yaml --room myroom
# Deploy across the whole project
meshagent service create --file meshagent.yaml --global
```
Use this when:
* you want to review the service manifest before deployment
* you need to customize fields that are not exposed as `meshagent process` flags
* you want to keep the service definition in source control
## Read next
* [Service YAML](./deploy_services): write, validate, deploy, update, and delete services from a manifest.
* [Process Agents Overview](../../agents/process/overview): understand how process agents run.
* [Deploy a Process Agent](./process_agent_service): deploy a process-backed agent with chat, email, and scheduled queue entry points.
# Service YAML
Source: https://docs.meshagent.com/services/deployment/deploy_services
Write, validate, deploy, update, and delete MeshAgent services from a service manifest.
Write a service manifest when you want to review, customize, validate, deploy, update, or delete the service configuration yourself.
Use [`meshagent service`](../../reference/meshagent_cli_help#meshagent-service) with service manifests. Use [Deploy a built in Agent](./deploy_builtin_agent) when you want to deploy a process-backed agent from CLI flags instead.
In MeshAgent, you can deploy fixed or templated services project-wide or to specific rooms, either as MeshAgent-managed `container` services or as `external` services that MeshAgent routes to; for more detail, see [Intro to Services](../intro).
The examples below show three common cases:
* a `Service` with a `container`
* a `ServiceTemplate` with a `container`
* a `Service` with an `external` runtime
### Example 1: `Service` with `container`
Use `kind: Service` when the configuration is fixed at deploy time.
```yaml Yaml theme={null}
kind: Service
version: v1
metadata:
name: my-chatbot
description: "A chatbot with web search and storage"
annotations:
meshagent.service.id: "my-chatbot"
agents:
- name: my-chatbot
description: "A helpful chatbot"
annotations:
meshagent.agent.type: "ChatBot"
container:
image: "meshagent/cli:default"
command: >-
meshagent process join
--agent-name my-chatbot
--channel chat
--web-search
--storage
--rule "You are a helpful assistant. Use web search to find current information and save important findings to storage."
--room-rules "agents/my-chatbot/rules.md"
environment:
- name: MESHAGENT_TOKEN
token:
identity: my-chatbot
storage:
room:
- path: /data
read_only: false
```
This example uses `kind: Service` with a `container`. MeshAgent runs the `meshagent/cli:default` image, and the container starts `meshagent process join ...` inside it.
Validate and deploy it:
```bash bash theme={null}
meshagent service validate --file meshagent.yaml
meshagent service create --file meshagent.yaml --room myroom
```
Use `--global` instead of `--room myroom` to deploy the same manifest as a project service.
### Example 2: `ServiceTemplate` with `container`
Use `kind: ServiceTemplate` when the manifest shape stays the same but some values should be supplied during deployment or install.
```yaml Yaml theme={null}
kind: ServiceTemplate
version: v1
metadata:
name: my-language-chatbot
description: "A chatbot with web search and storage that responds in your chosen language"
annotations:
meshagent.service.id: "my-language-chatbot"
variables:
- name: language
description: "The language the chatbot should respond in (e.g. English, Spanish, French)"
agents:
- name: my-language-chatbot
description: "A helpful chatbot"
annotations:
meshagent.agent.type: "ChatBot"
container:
image: "meshagent/cli:default"
command: >-
meshagent process join
--agent-name my-language-chatbot
--channel chat
--web-search
--storage
--rule "You are a helpful assistant. Always respond in {{ language }}. Use web search to find current information and save important findings to storage."
--room-rules "agents/my-language-chatbot/rules.md"
environment:
- name: MESHAGENT_TOKEN
token:
identity: my-language-chatbot
storage:
room:
- path: /data
read_only: false
```
This example adds a `language` variable and uses `{{ language }}` inside the `container.command`.
Validate and deploy it:
```bash bash theme={null}
meshagent service validate-template --file meshagent.yaml
meshagent service create-template \
--file meshagent.yaml \
--value language=Spanish \
--room myroom
```
Use `--global` instead of `--room myroom` to deploy the rendered service project-wide.
If you want to inspect the rendered YAML first, use `meshagent service render-template --file meshagent.yaml --value language=Spanish`.
### Example 3: `Service` with `external`
Use `external` when MeshAgent should route to a service you already host elsewhere instead of running a container for you.
```yaml Yaml theme={null}
kind: Service
version: v1
metadata:
name: mcp-deepwiki
description: "Expose the DeepWiki MCP server"
ports:
- num: 443
type: http
endpoints:
- path: /mcp
mcp:
label: "mcp-deepwiki"
description: "MCP DeepWiki tools"
allowed_tools:
- tool_names: ["search", "read_page"]
read_only: true
external:
url: "https://mcp.deepwiki.com"
```
This example is a fixed `Service` that routes to an MCP server hosted outside MeshAgent. MeshAgent does not run the MCP server. It uses the `external.url` and `ports.endpoints` config to register that server's tools in the room.
Validate and deploy it:
```bash bash theme={null}
meshagent service validate --file mcp-service.yaml
meshagent service create --file mcp-service.yaml --room myroom
```
If you need installer-provided values such as a URL, label, or OAuth settings, you can use `external` inside a `ServiceTemplate` too.
For a longer walkthrough, see [Connect to an External MCP Server](./external_mcp_service).
## Manage deployed services
Use `meshagent service` after deployment to inspect and manage what you deployed.
```bash bash theme={null}
# List project services
meshagent service list
# List room services
meshagent service list --room myroom
# View details for one service
meshagent service get SERVICE_ID
# Update a deployed Service
meshagent service update --file meshagent.yaml --room myroom --id SERVICE_ID
# Update a deployed ServiceTemplate
meshagent service update-template --file meshagent.yaml --value key=value --room myroom --id SERVICE_ID
# Stop a service and prevent it from starting
meshagent service disable SERVICE_ID --room myroom
# Allow a disabled service to start again
meshagent service enable SERVICE_ID --room myroom
# Delete a deployed service
meshagent service delete SERVICE_ID --room myroom
```
Omit `--room myroom` when you are updating, listing, or deleting a project service instead.
## Service configuration field reference
### Top-level fields
| Field | Required | Description |
| ----------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `version` | Yes | Schema version. Always `v1`. |
| `kind` | Yes | `Service` or `ServiceTemplate`. |
| `enabled` | No | `Service` only. Whether the service should run. Defaults to `true`; setting it to `false` stops a running managed service and prevents future starts. |
| `metadata` | Yes | Service identity and display information. |
| `agents` | No | Agent identities exposed by this service. |
| `files` | No | Files MeshAgent should seed into room storage if they do not already exist. |
| `ports` | No | Network ports and HTTP endpoints MeshAgent can route to. |
| `container` | \* | Container configuration. Mutually exclusive with `external`. |
| `external` | \* | External service URL. Mutually exclusive with `container`. |
| `variables` | No | User-provided inputs for templating. `ServiceTemplate` only. |
\* Either `container` or `external` is required, but not both.
### metadata
Identifies the service and provides information displayed in the UI.
| Field | Type | Required | Description |
| ------------- | ------ | -------- | ---------------------------------------------------- |
| `name` | string | Yes | Unique service name. |
| `description` | string | No | Description shown in UI. |
| `repo` | string | No | Source code repository URL. |
| `icon` | string | No | Icon or emoji for UI display. |
| `annotations` | object | No | Key-value metadata. See [Annotations](#annotations). |
### agents
Declares the participant identities this service provides. MeshAgent uses this to route requests, apply policies, and display agents in the UI.
| Field | Type | Required | Description |
| ------------- | ------ | -------- | -------------------------------------------------------------------------- |
| `name` | string | Yes | Unique agent identity within the service. |
| `description` | string | No | Display text describing the agent. |
| `annotations` | object | No | Key-value metadata. See [Annotations](#annotations). |
| `channels` | object | No | Channel-specific routing config for mail, messaging, queues, and toolkits. |
| `email` | object | No | Mailbox settings for this agent identity. |
| `heartbeat` | object | No | Recurring queue turn for this agent. |
#### agents.channels
Use `channels` when one deployed agent should participate in more than one entry point.
| Field | Type | Required | Description |
| ----------- | ---- | -------- | ---------------------------------------------- |
| `email` | list | No | Inbound email channels for the agent. |
| `messaging` | list | No | Messaging channels and optional named prompts. |
| `queue` | list | No | Queue channels the agent should consume. |
| `toolkit` | list | No | Toolkits the agent should register or expose. |
Queue channel entries support:
| Field | Type | Required | Description |
| ---------------- | ------ | -------- | --------------------------------------------------------------- |
| `queue` | string | Yes | Queue name the agent should consume. |
| `threading_mode` | string | No | Threading mode for queue-delivered work, such as `default-new`. |
| `message_schema` | object | No | JSON schema describing expected queue payload shape. |
Messaging channel entries support:
| Field | Type | Required | Description |
| ---------- | ------ | -------- | ------------------------------------------------------------- |
| `protocol` | string | No | Messaging protocol. Defaults to `meshagent.agent-message.v1`. |
| `prompts` | list | No | Named prompts available on that messaging channel. |
#### agents.email
Use `email` when the agent identity itself should have mailbox settings attached to it.
| Field | Type | Required | Description |
| --------- | ------- | -------- | --------------------------------------------------- |
| `address` | string | Yes | Email address for the agent. |
| `public` | boolean | No | Whether anyone can send to it. Defaults to `false`. |
#### agents.heartbeat
Use `heartbeat` when the agent should enqueue a recurring turn for itself without creating a separate scheduled-task resource by hand.
| Field | Type | Required | Description |
| ----------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `queue` | string | Yes | Queue that receives the recurring work item. |
| `thread_id` | string | No | Optional thread path or template. Time tokens such as `{year}`, `{month}`, `{day}`, `{hour}`, and `{minute}` are expanded at runtime. Token matching is case-insensitive, so `{YYYY}/{MM}/{DD}/{HH}/{mm}` also works. |
| `prompt` | list | Yes | Prompt content for each heartbeat turn. Uses typed content items such as `text` and `file`. |
| `minutes` | integer | Yes | Interval, in minutes, between heartbeats. |
Heartbeat `prompt` items use the same typed content model as agent turns:
| Field | Type | Required | Description |
| ------ | ------ | -------- | --------------------------------------------------------------- |
| `type` | string | Yes | `text` or `file`. |
| `text` | string | No | Text content when `type: text`. |
| `url` | string | No | File URL when `type: file`, including `room:///...` references. |
Example:
```yaml theme={null}
agents:
- name: assistant
annotations:
meshagent.agent.type: ChatBot
meshagent.chatbot.threading: "default-new"
meshagent.chatbot.thread-dir: agents/assistant/threads
email:
address: assistant@example.com
public: true
heartbeat:
queue: assistant-scheduled-tasks
thread_id: /agents/assistant/threads/heartbeats/{year}/{month}/{day}/{hour}.{minute}.thread
prompt:
- type: file
url: room:///agents/assistant/heartbeat.md
minutes: 60
files:
- path: /agents/assistant/heartbeat.md
text: |
# Assistant Heartbeat
Review recent room activity, unresolved requests, and any obvious follow-up work.
If there is clear useful work to do, do it directly.
If there is nothing useful to add, end the turn quietly without posting a filler message.
Previous threads can be found looking at the threads in the path format based on the time: /data/agents/assistant/threads/heartbeats/{year}/{month}/{day}/{hour}.{minute}.thread
Read the last few heartbeat threads to make sure you are up to date with what has been done so far
```
This mirrors the built-in assistant service template: each heartbeat run is queued to the assistant, uses the heartbeat prompt file from room storage, and writes the run into a timestamped thread under the assistant thread directory.
### files
Top-level `files` seed room storage before the service starts. MeshAgent creates each file only if it does not already exist, which makes this useful for editable starter content such as prompts, rules, or templates.
| Field | Type | Required | Description |
| ------ | ------ | -------- | ---------------------------- |
| `path` | string | Yes | Room-storage path to create. |
| `text` | string | Yes | Initial file contents. |
Use top-level `files` when the content should live in room storage and remain editable after deploy. Use `container.storage.files` when the file should be mounted inside the running container itself.
### container
Defines a container for MeshAgent to run. Fields marked **†** are only available in `Service`, not `ServiceTemplate`.
| Field | Type | Required | Description |
| ------------------ | --------------------- | -------- | ------------------------------------------------------------------------------------------------------------ |
| `image` | string | Yes | Container image (for example `meshagent/cli:default`). |
| `template` | `"agent"` or `"none"` | No | Runtime defaults to apply when the container starts. Defaults to `"agent"`. |
| `command` | string | No | Command to execute when the container starts. |
| `working_dir` | string | No | Absolute working directory used when starting the container command. |
| `environment` | list | No | Environment variables to set in the container. |
| `run_as` | object | No | Service account identity and runtime scopes. Required when using `SecretValue` environment variables. |
| `storage` | object | No | Storage volumes to mount into the container. |
| `on_demand` | boolean | No | When true, the container runs only when explicitly invoked. |
| `writable_root_fs` | boolean | No | Allow writes to the container's root filesystem for the life of that container instance. Default: read-only. |
| `private` | boolean | No | Advanced setting that keeps interactive container access private to the owning service. Default: true. |
#### container.template
`container.template` controls the MeshAgent runtime defaults added when the container starts:
* `"agent"` (default): mount room storage at `/data`, create a participant token for the container name with role `agent` and default agent API permissions, and inject MeshAgent/OpenAI/Anthropic runtime environment variables.
* `"none"`: do not add those defaults. Only values explicitly configured in the manifest or container start request are passed through.
The `"agent"` template sets defaults only when the environment variable is not already configured. Values in `container.environment` override template defaults.
The `"agent"` template may set:
| Variable | Description |
| ----------------------- | --------------------------------------------------------------------------------------------------- |
| `MESHAGENT_TOKEN` | Room participant token for the container name, with role `agent` and default agent API permissions. |
| `OPENAI_API_KEY` | Same participant token, for OpenAI-compatible clients using MeshAgent's proxy. |
| `ANTHROPIC_API_KEY` | Same participant token, for Anthropic-compatible clients using MeshAgent's proxy. |
| `SMTP_PASSWORD` | Same participant token, for SMTP authentication. |
| `SMTP_USERNAME` | Container name, for SMTP authentication. |
| `SMTP_HOSTNAME` | MeshAgent SMTP hostname, set from `MESHAGENT_MAIL_DOMAIN` when available. |
| `SMTP_PORT` | SMTP port. Defaults to `587`. |
| `OPENAI_BASE_URL` | OpenAI-compatible MeshAgent proxy URL injected into the runtime. |
| `ANTHROPIC_BASE_URL` | Anthropic-compatible MeshAgent proxy URL injected into the runtime. |
| `MESHAGENT_API_URL` | MeshAgent API URL reachable from the container. |
| `MESHAGENT_ROOM_URL` | MeshAgent room API base URL reachable from the container. |
| `MESHAGENT_ROOM` | Current room name. |
| `MESHAGENT_PROJECT_ID` | Current project ID, when available. |
| `MESHAGENT_SESSION_ID` | Current session ID, when available. |
| `OTEL_ENDPOINT` | OpenTelemetry collector endpoint reachable from the container. |
| `OTEL_PYTHON_LOG_LEVEL` | Python OpenTelemetry log level forwarded from the room runtime, when available. |
| `MESHAGENT_MAIL_DOMAIN` | MeshAgent mail domain forwarded from the room runtime, when available. |
#### container.run\_as
`container.run_as` selects the service account identity used by the container.
The default scope list grants access to the HTTP/MCP secret proxy without
granting broad project administration.
| Field | Type | Required | Description |
| -------- | --------------- | -------- | ------------------------------------------------------------------------------ |
| `email` | string | Yes | Service account email the container runs as. |
| `scopes` | list of strings | No | Runtime scopes for the service-account token. Defaults to `["secrets:proxy"]`. |
#### container.environment
Each entry sets an environment variable in the container. A variable can come from a literal `value`, a MeshAgent `token`, or a service-account `secret`.
| Field | Type | Required | Description |
| -------- | ------ | -------- | ------------------------------------------------------------------------------------------------ |
| `name` | string | Yes | Environment variable name. |
| `value` | string | No | Literal string value. |
| `token` | object | No | Request a participant token to be generated and injected as the value. |
| `secret` | object | No | Load a secret available to the service account in `container.run_as` and inject it as the value. |
Use:
* `value` for plain configuration
* `token` for MeshAgent API access
* `secret` for service-account credentials. `container.run_as` is required when any environment variable uses `secret`.
`token` fields:
| Field | Type | Required | Description |
| ---------- | ------ | -------- | ---------------------------------------------------------------------------- |
| `identity` | string | Yes | Participant identity the token is issued for. |
| `api` | object | No | API scope granted to the token. See [API Scopes](../../rest_api/api_scopes). |
| `role` | string | No | Participant role (for example `user`, `agent`, or `tool`). |
`secret` fields:
| Field | Type | Required | Description |
| ----- | ------ | -------- | ---------------------------------------------------------------- |
| `id` | string | Yes | Secret ID to load for the service account in `container.run_as`. |
This pattern is common for services: `token` gives the service access to MeshAgent, while `secret` gives it an external credential through its service account.
#### container.storage
Mounts storage volumes into the container.
| Field | Type | Description |
| ------------ | ---- | ----------------------------------------------------------------------- |
| `room` | list | Per-room storage. Read/write by default. |
| `project` | list | Project-wide shared storage. Read-only by default. |
| `images` | list | Content from another container image. Read-only by default. |
| `files` | list | Inline text content mounted as a file. Read-only by default. |
| `empty_dirs` | list | Writable temporary directories mounted into the container. |
| `configs` | list | MeshAgent runtime config mounts such as `spec.json` and `members.json`. |
Room and project mount fields:
| Field | Type | Required | Description |
| ----------- | ------- | -------- | --------------------------------------- |
| `path` | string | Yes | Mount path inside the container. |
| `subpath` | string | No | Subdirectory within the storage volume. |
| `read_only` | boolean | No | Whether the mount is read-only. |
Image mount fields:
| Field | Type | Required | Description |
| ----------- | ------- | -------- | -------------------------------- |
| `image` | string | Yes | Source container image. |
| `path` | string | Yes | Mount path inside the container. |
| `subpath` | string | No | Subdirectory within the image. |
| `read_only` | boolean | No | Whether the mount is read-only. |
File mount fields:
| Field | Type | Required | Description |
| ----------- | ------- | -------- | -------------------------------- |
| `path` | string | Yes | Mount path inside the container. |
| `text` | string | Yes | File contents. |
| `read_only` | boolean | No | Whether the mount is read-only. |
Empty directory mount fields:
| Field | Type | Required | Description |
| ----------- | ------- | -------- | --------------------------------------------- |
| `path` | string | Yes | Mount path inside the container. |
| `read_only` | boolean | No | Whether the temporary directory is read-only. |
Config mount fields:
| Field | Type | Required | Description |
| ------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------ |
| `path` | string | Yes | Directory inside the container where MeshAgent runtime config files should be mounted. Defaults to `/var/run/meshagent`. |
Config mounts are useful when a shell tool or helper process inside the container needs access to runtime files such as `spec.json` and `members.json`.
### external
Routes traffic to a service running outside MeshAgent. Requires `ports` to define how MeshAgent reaches the service.
| Field | Type | Required | Description |
| ----- | ------ | -------- | --------------------------------- |
| `url` | string | Yes | URL where the service is running. |
### ports
Defines network ports the service listens on and how MeshAgent routes HTTP traffic to them.
| Field | Type | Required | Description |
| ------------- | ------------ | -------- | ------------------------------------------------------ |
| `num` | `"*"` or int | Yes | Port number, or `"*"` for auto-assignment. |
| `type` | string | No | Protocol: `http` or `tcp`. |
| `liveness` | string | No | HTTP path for health checks. |
| `endpoints` | list | No | Endpoints served on this port. |
| `published` | boolean | No | Expose the port to the internet. |
| `public` | boolean | No | When false, requests must include a participant token. |
| `annotations` | object | No | Key-value metadata. |
For HTTP ports, set `liveness` to the path MeshAgent should poll to decide whether the service port is ready. For example, `liveness: /healthz` tells MeshAgent to request that path on the running service. A `2xx` response reports the port as `ready`; failed checks report `not ready`; ports without a liveness path report `no liveness` in `meshagent room service list`.
#### ports.endpoints
Each endpoint maps a URL path to either a MeshAgent-native service or an MCP server.
| Field | Type | Required | Description |
| ------------- | ------ | -------- | --------------------------------------------------------- |
| `path` | string | Yes | URL path for this endpoint. |
| `meshagent` | object | No | MeshAgent-native endpoint. Mutually exclusive with `mcp`. |
| `mcp` | object | No | MCP server endpoint. Mutually exclusive with `meshagent`. |
| `annotations` | object | No | Key-value metadata. |
##### meshagent endpoint
Connects the endpoint to a MeshAgent participant identity.
| Field | Type | Required | Description |
| ---------- | ------ | -------- | ----------------------------------------------------------------- |
| `identity` | string | Yes | Participant identity for this endpoint. |
| `api` | object | No | API scope overrides. See [API Scopes](../../rest_api/api_scopes). |
##### mcp endpoint
Registers an MCP server as a toolkit in the room.
| Field | Type | Required | Description |
| --------------------- | ------ | -------- | ------------------------------------------- |
| `label` | string | Yes | Toolkit display name. |
| `description` | string | No | Description of what the toolkit provides. |
| `allowed_tools` | list | No | Filters which tools are exposed. |
| `headers` | object | No | Custom HTTP headers to include in requests. |
| `require_approval` | string | No | `always` or `never`. |
| `oauth` | object | No | OAuth client configuration. |
| `openai_connector_id` | string | No | OpenAI connector ID. |
`allowed_tools` entries:
| Field | Type | Required | Description |
| ------------ | ------- | -------- | ----------------------------- |
| `tool_names` | list | Yes | Tool names to allow. |
| `read_only` | boolean | No | Treat the tools as read-only. |
`oauth` fields:
| Field | Type | Required | Description |
| ------------------------ | ------- | -------- | ------------------------------------------- |
| `client_id` | string | Yes | OAuth client ID. |
| `client_secret` | string | No | OAuth client secret. |
| `authorization_endpoint` | string | Yes | Authorization endpoint URL. |
| `token_endpoint` | string | Yes | Token endpoint URL. |
| `no_pkce` | boolean | No | Disable PKCE (Proof Key for Code Exchange). |
| `scopes` | list | No | OAuth scopes to request. |
### variables (ServiceTemplate only)
Defines user-provided inputs for a `ServiceTemplate`. Values are substituted into the YAML using `{{ variable_name }}` syntax.
Template values are rendered into the manifest before validation, so they can be used anywhere the resulting YAML remains valid.
| Field | Type | Required | Description |
| ------------- | ------- | -------- | ------------------------------------------------------------- |
| `name` | string | Yes | Variable identifier. Referenced as `{{ name }}` in templates. |
| `title` | string | No | Human-readable label shown in install UI. |
| `description` | string | No | Help text shown in UI and Powerboards. |
| `enum` | list | No | Restricts input to specific values. Displayed as a dropdown. |
| `optional` | boolean | No | Whether the variable can be left blank. |
| `obscure` | boolean | No | Hides the value in UI. Use for sensitive data. |
| `type` | string | No | Type hint (for example `email`). |
| `annotations` | object | No | Key-value metadata. See [Annotations](#annotations). |
### Annotations
Annotations are key-value strings attached to services, agents, or variables. MeshAgent and Powerboards use specific annotation keys to control behavior. You can also define custom annotations.
#### Service annotations
Set in `metadata.annotations`.
| Key | Description |
| -------------------------- | ---------------------------------------------------- |
| `meshagent.service.id` | Unique identifier for the service. |
| `meshagent.service.readme` | URL or inline content for the service documentation. |
#### Agent annotations
Set in `agents[].annotations`.
| Key | Description |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `meshagent.agent.type` | Internal agent type metadata used by service manifests and generated specs. |
| `meshagent.agent.widget` | UI widget to display for this agent. |
| `meshagent.agent.schedule` | JSON string defining a `ScheduledTaskSpec`. Use either `queue: { name, payload }` or `container`, not both. |
| `meshagent.agent.shell.command` | Shell command for `Shell`-type agents. |
| `meshagent.agent.dataset.schema` | Dataset schema metadata for the agent. |
#### Variable annotations
Set in `variables[].annotations`.
| Key | Description |
| ----------------------- | ------------------------------------------------------------------------- |
| `meshagent.secret.id` | Suggested secret identifier for installer-collected credential variables. |
| `meshagent.secret.name` | Display name for installer-collected credential variables. |
| `meshagent.secret.type` | Credential content type for installer-collected variables. |
#### Event annotations
Set in `agents[].annotations`. Subscribe an agent to room events. The value is the name of a queue that a queue-consuming agent can process.
| Key | Description |
| ----------------------------------------- | -------------------------------------------- |
| `meshagent.events.service.created` | Fires when a service is created in the room. |
| `meshagent.events.service.updated` | Fires when a service is updated. |
| `meshagent.events.room.user.grant.create` | Fires when a user is added to the room. |
| `meshagent.events.room.user.grant.delete` | Fires when a user is removed from the room. |
| `meshagent.events.room.user.grant.update` | Fires when a user's room grant is updated. |
## Read next
* [Build and Deploy Images](../containers/meshagent_image): build and deploy images with `meshagent build` and `meshagent deploy`
* [Secrets and Credentials](../../secrets/overview): manage user-owned and service-account-owned secrets
* [Observability](../../observability/overview): inspect logs, traces, metrics, and custom OpenTelemetry output
* [Webhook Handoffs](../../services/webhooks): handle `room.call` webhook handoffs in your own services
# Deploy a Web App
Source: https://docs.meshagent.com/services/deployment/deploy_web_app
Deploy a Node.js web app to a MeshAgent room with a private or public meshagent.app domain.
Use `meshagent deploy` to build a local web app, deploy it as a room service, and attach a stable domain such as `YOUR_SITE.meshagent.app`.
This guide starts from an empty directory. By the end, the app is running in a MeshAgent room, protected by MeshAgent sign-in, writing files to room storage, and calling the MeshAgent LLM router.
`meshagent deploy` reads the Dockerfile, builds the container image in MeshAgent, creates or updates the room service, and creates or updates the route when you pass `--domain`.
MeshAgent rooms run agents and services in containers. That means the same deployment pattern works for apps written in Node.js, Python, Go, .NET, Rust, or any other language that can run in a container. This guide uses Node.js because it is a popular choice for building web applications.
## Create a Node.js web server
Create a `package.json` file:
```json package.json theme={null}
{
"name": "meshagent-web-hello",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node server.js"
}
}
```
`package.json` tells Node.js that this project uses JavaScript modules and that `npm start` should run `server.js`.
Create the dependency lockfile:
```bash theme={null}
npm install
```
This app does not have external dependencies yet, but `npm install` creates `package-lock.json`. The lockfile records the exact dependency versions for the project. The Dockerfile below uses `npm ci`, which expects a lockfile and installs from it reproducibly.
Create `server.js`:
```js server.js theme={null}
import http from "node:http";
const port = Number(process.env.PORT || 8080);
const server = http.createServer((req, res) => {
if (req.url === "/healthz") {
res.writeHead(200, { "content-type": "text/plain" });
res.end("ok\n");
return;
}
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
res.end(`
MeshAgent Web App
Hello from MeshAgent
This app is running inside a MeshAgent room.
`);
});
server.listen(port, "0.0.0.0", () => {
console.log(`listening on ${port}`);
});
```
This server listens on the port from `PORT`, or `8080` when `PORT` is not set. The `/healthz` path is a simple health check. MeshAgent can call it to confirm the app is ready before sending browser traffic to the service.
Add a Dockerfile:
```dockerfile Dockerfile theme={null}
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY server.js ./
ENV NODE_ENV=production
ENV PORT=8080
EXPOSE 8080
CMD ["node", "server.js"]
```
A Dockerfile is the build recipe for the app. It tells Docker and MeshAgent which runtime to start from, which files to copy into the image, which install commands to run, and which command starts the app.
The Dockerfile above does the following:
* `FROM node:22-alpine` starts from a small Linux image with Node.js installed.
* `WORKDIR /app` sets the working directory inside the image.
* `COPY package*.json ./` copies `package.json` and `package-lock.json`.
* `RUN npm ci --omit=dev` installs production dependencies from the lockfile.
* `COPY server.js ./` copies the application code.
* `ENV PORT=8080` gives the app the HTTP port used by this guide.
* `EXPOSE 8080` marks the container port that serves HTTP traffic.
* `CMD ["node", "server.js"]` starts the server when the container runs.
A container image is a packaged version of the app and its runtime. A container is a running instance of that image. `meshagent deploy` builds the image and runs it as a service in the room.
## Deploy it
Deploy the current directory to a room and attach a `meshagent.app` domain:
```bash theme={null}
meshagent deploy . \
--room my-room \
--tag web-hello:v1 \
--domain YOUR_SITE.meshagent.app \
--liveness /healthz
```
The command uses:
* `.` as the source directory to build.
* `--room my-room` as the room where the service will run.
* `--tag web-hello:v1` as the image tag for this version of the app.
* `--domain YOUR_SITE.meshagent.app` as the browser URL for the service.
* `--liveness /healthz` as the readiness check path.
Replace `my-room` and `YOUR_SITE` with your room name and site name.
Use a new tag each time you deploy a meaningful update, such as `web-hello:v1`, `web-hello:v2`, and `web-hello:v3`. The tag is the deployable version of the app. If a later deploy has a problem, point the service back at an earlier tag while you fix the new version:
```bash theme={null}
meshagent deploy \
--room my-room \
--tag web-hello:v1 \
--domain YOUR_SITE.meshagent.app \
--liveness /healthz
```
That command redeploys the already-built `web-hello:v1` image. It does not include `.` because it is not building the current directory again.
Open:
```text theme={null}
https://YOUR_SITE.meshagent.app
```
A route connects the domain to the room service. By default, `meshagent deploy` creates a private route. A private route is protected by MeshAgent IAP. IAP stands for Identity-Aware Proxy: MeshAgent authenticates the browser user, checks that the user has access to the room, and then forwards the request to the web app.
This gives you a private web app without adding login code to the app. The app can focus on the UI and application behavior, while MeshAgent handles sign-in and room access at the route.
## Show the signed-in user
For private browser routes, MeshAgent provides the signed-in room identity to the app in the `X-MESHAGENT-USER` request header.
Update `server.js` to display that user:
```js server.js theme={null}
import http from "node:http";
const port = Number(process.env.PORT || 8080);
const server = http.createServer((req, res) => {
if (req.url === "/healthz") {
res.writeHead(200, { "content-type": "text/plain" });
res.end("ok\n");
return;
}
const user = req.headers["x-meshagent-user"] || "anonymous";
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
res.end(`
MeshAgent Web App
Hello from MeshAgent
Signed in as ${escapeHtml(String(user))}.
`);
});
server.listen(port, "0.0.0.0", () => {
console.log(`listening on ${port}`);
});
function escapeHtml(value) {
return value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
```
Deploy the updated app with a new tag:
```bash theme={null}
meshagent deploy . \
--room my-room \
--tag web-hello:v2 \
--domain YOUR_SITE.meshagent.app \
--liveness /healthz
```
Refresh the page. After sign-in, the app displays the authenticated room identity from the IAP header. The route still uses `YOUR_SITE.meshagent.app`; the service now runs the image tagged `web-hello:v2`.
## Write files to room storage
Containers have their own filesystem. When you run the app locally, writes go to your local disk. When you deploy the app into a room, writes go to the container filesystem unless you mount room storage into the container.
A mount connects a path inside the container to storage managed outside the container. The app still reads and writes normal files, but the files are stored in the room instead of only inside that one running container.
Update `server.js` to append each request to a log file:
```js server.js theme={null}
import { mkdir, readFile, appendFile } from "node:fs/promises";
import http from "node:http";
import path from "node:path";
const port = Number(process.env.PORT || 8080);
const dataDir = process.env.APP_DATA_DIR || path.join(process.cwd(), "data");
const visitLogPath = path.join(dataDir, "visits.log");
const server = http.createServer(async (req, res) => {
if (req.url === "/healthz") {
res.writeHead(200, { "content-type": "text/plain" });
res.end("ok\n");
return;
}
const user = req.headers["x-meshagent-user"] || "anonymous";
await recordVisit(String(user));
const visits = await readVisits();
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
res.end(`
MeshAgent Web App
Hello from MeshAgent
Signed in as ${escapeHtml(String(user))}.
Recent visits
${escapeHtml(visits)}
`);
});
server.listen(port, "0.0.0.0", () => {
console.log(`listening on ${port}`);
console.log(`writing visits to ${visitLogPath}`);
});
async function recordVisit(user) {
await mkdir(dataDir, { recursive: true });
await appendFile(visitLogPath, `${new Date().toISOString()} ${user}\n`);
}
async function readVisits() {
try {
return await readFile(visitLogPath, "utf8");
} catch (error) {
if (error.code === "ENOENT") {
return "";
}
throw error;
}
}
function escapeHtml(value) {
return value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
```
Test it locally:
```bash theme={null}
npm start
```
Open `http://localhost:8080`, then check the file on your machine:
```bash theme={null}
cat data/visits.log
```
The file is created locally because the app is running on your machine. The local request displays `anonymous` for the user because the browser is connecting directly to `localhost`, not through the private MeshAgent route.
Update the Dockerfile so the container writes app data to `/data`:
```dockerfile Dockerfile theme={null}
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY server.js ./
ENV NODE_ENV=production
ENV PORT=8080
ENV APP_DATA_DIR=/data
VOLUME /data
EXPOSE 8080
CMD ["node", "server.js"]
```
`APP_DATA_DIR=/data` tells the app to write `visits.log` under `/data` after it is deployed. `VOLUME /data` marks `/data` as the writable data directory for the container.
Deploy again with a room mount:
```bash theme={null}
meshagent deploy . \
--room my-room \
--tag web-hello:v3 \
--domain YOUR_SITE.meshagent.app \
--liveness /healthz \
--room-mount /web-hello:/data:rw
```
The room mount format is:
```text theme={null}
--room-mount ::
```
In this example, `/web-hello` is the path in room storage, `/data` is the path inside the container, and `rw` means the service can read and write the mounted files.
Refresh `https://YOUR_SITE.meshagent.app` a few times, then open [MeshAgent Studio](https://studio.meshagent.com), go to the room, and inspect room storage. The file is written at:
```text theme={null}
/web-hello/visits.log
```
Your local `data/visits.log` file is no longer updated by the deployed app. The app is running in the room, and `/data` now points at room storage.
## Call the LLM router
Add the OpenAI SDK:
```bash theme={null}
npm install openai
```
Update `server.js` to call the MeshAgent LLM router through the OpenAI SDK:
```js server.js theme={null}
import { mkdir, readFile, appendFile } from "node:fs/promises";
import http from "node:http";
import path from "node:path";
import OpenAI from "openai";
const port = Number(process.env.PORT || 8080);
const dataDir = process.env.APP_DATA_DIR || path.join(process.cwd(), "data");
const visitLogPath = path.join(dataDir, "visits.log");
const openai = new OpenAI({
baseURL: process.env.OPENAI_BASE_URL,
apiKey: process.env.OPENAI_API_KEY,
});
const server = http.createServer(async (req, res) => {
if (req.url === "/healthz") {
res.writeHead(200, { "content-type": "text/plain" });
res.end("ok\n");
return;
}
const user = req.headers["x-meshagent-user"] || "anonymous";
await recordVisit(String(user));
const visits = await readVisits();
const message = await generateMessage(String(user));
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
res.end(`
MeshAgent Web App
Hello from MeshAgent
Signed in as ${escapeHtml(String(user))}.
${escapeHtml(message)}
Recent visits
${escapeHtml(visits)}
`);
});
server.listen(port, "0.0.0.0", () => {
console.log(`listening on ${port}`);
console.log(`writing visits to ${visitLogPath}`);
});
async function recordVisit(user) {
await mkdir(dataDir, { recursive: true });
await appendFile(visitLogPath, `${new Date().toISOString()} ${user}\n`);
}
async function readVisits() {
try {
return await readFile(visitLogPath, "utf8");
} catch (error) {
if (error.code === "ENOENT") {
return "";
}
throw error;
}
}
async function generateMessage(user) {
const response = await openai.responses.create({
model: "gpt-5.4",
input: `Write one short welcome sentence for ${user}.`,
});
return response.output_text;
}
function escapeHtml(value) {
return value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
```
The OpenAI SDK normally sends requests to OpenAI. In this example, `OPENAI_BASE_URL` points the SDK at the MeshAgent LLM proxy instead. `OPENAI_API_KEY` is a MeshAgent credential for the room, not a provider key. That lets the app use the standard OpenAI SDK while MeshAgent handles project routing, usage tracking, and access control.
Test the app locally through the room:
```bash theme={null}
meshagent room connect --room=my-room --identity=web-hello -- npm start
```
Open:
```text theme={null}
http://localhost:8080
```
`meshagent room connect` runs the command on your machine and provides the same MeshAgent environment variables that the app receives in the room. That is why the local app can call the LLM router without hardcoding a proxy URL or token.
The local request still does not include `X-MESHAGENT-USER` because your browser is connecting directly to `localhost`; the request is not passing through the MeshAgent IAP route.
Deploy the LLM-enabled app with a new tag and the same room mount:
```bash theme={null}
meshagent deploy . \
--room my-room \
--tag web-hello:v4 \
--domain YOUR_SITE.meshagent.app \
--liveness /healthz \
--meshagent-token agentDefault \
--room-mount /web-hello:/data:rw
```
`--meshagent-token agentDefault` injects `MESHAGENT_TOKEN`, `OPENAI_API_KEY`, and `ANTHROPIC_API_KEY` for the deployed service. The token identity defaults to the service name derived from the image repository, so `web-hello:v4` uses the `web-hello` identity.
Refresh `https://YOUR_SITE.meshagent.app`. The page shows the signed-in user, recent visits from room storage, and a sentence generated through the MeshAgent LLM router.
## Deploy a public site
A site can also be public. Use `--public` when anyone on the internet should be able to load the site without a MeshAgent room sign-in:
```bash theme={null}
meshagent deploy . \
--room my-room \
--tag web-hello:v5 \
--domain YOUR_SITE.meshagent.app \
--liveness /healthz \
--meshagent-token agentDefault \
--room-mount /web-hello:/data:rw \
--public
```
Public sites are useful for demos, static marketing pages, public webhook targets, and other endpoints that are intentionally open. A public route still points at the same room service, but browser requests are not checked against room membership.
Do not depend on IAP headers in a public route because public requests are not authenticated by MeshAgent room access.
If the public site calls the LLM router, anyone who can reach the site can trigger those requests.
## Optimizing Cold Start Latency
Rooms have a lifecycle. When a room is active, MeshAgent starts the services needed by that room. When the room goes idle, those services can be stopped. The next request to a routed web app can start the room again before the app responds.
Cold start latency is the extra time before the first response while the service starts. For a web app, the important work is:
* preparing the container image
* starting the Node.js process
* loading application files and dependencies
* waiting for the app to listen on its HTTP port
The optimizations below reduce the amount of content that must be prepared and the amount of work Node has to do before the first request succeeds.
A small, cheap `/healthz` endpoint also helps. It gives MeshAgent a quick readiness check so traffic is sent to the app after the server is listening.
### Use a multistage build
A Docker image is built in layers. A normal Dockerfile can leave build tools, source files, package-manager cache, and development dependencies in the final image even though the running app does not need them.
A multistage Dockerfile separates the build environment from the runtime environment. The first stage installs dependencies and prepares the app. The final stage copies only the files needed to run the server:
```dockerfile Dockerfile theme={null}
FROM node:22-alpine AS build
WORKDIR /src
COPY package.json package-lock.json ./
RUN npm ci
COPY server.js ./
FROM node:22-alpine
WORKDIR /app
COPY --from=build /src/package.json /src/package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /src/server.js ./
ENV NODE_ENV=production
ENV PORT=8080
ENV APP_DATA_DIR=/data
VOLUME /data
EXPOSE 8080
CMD ["node", "server.js"]
```
This final image still uses Node, but it does not include the first stage's build cache or temporary files. Smaller runtime images are faster to prepare during room startup and have fewer files for the app to scan at runtime.
### Bundle with ncc
Node.js usually loads code by resolving imports from your app and from `node_modules`. That is flexible during development, but it can mean the runtime container has thousands of small files and Node has to resolve many paths during startup.
`ncc` is a Node.js bundler. It starts from an entry file, follows the imports used by that file, and writes the application plus its dependencies into a small output directory. For this app, the entry file is `server.js` and the output is `dist/index.js`.
Install `ncc` as a development dependency and build the bundle:
```bash theme={null}
npm install --save-dev @vercel/ncc
npx ncc build server.js --target es2022 --out dist
```
Update the Dockerfile to run the bundled output:
```dockerfile Dockerfile theme={null}
FROM node:22-alpine AS build
WORKDIR /src
COPY package.json package-lock.json ./
RUN npm ci
COPY server.js ./
RUN npx ncc build server.js --target es2022 --out dist
FROM node:22-alpine
WORKDIR /app
COPY --from=build /src/dist/index.js ./index.js
ENV NODE_ENV=production
ENV PORT=8080
ENV APP_DATA_DIR=/data
VOLUME /data
EXPOSE 8080
CMD ["node", "index.js"]
```
This helps cold starts because the runtime container starts Node with one application file instead of a source tree plus `node_modules`. The final image also does not need `ncc` itself, because bundling happened in the build stage.
### Use a scratch runtime image with `meshagent.runtime=node`
The previous Dockerfile still ships a full Node base image as part of your app image. For the smallest deployable artifact, use a final `scratch` stage.
`scratch` is Docker's empty base image. It contains only the files you copy into it. On its own, a `scratch` image cannot run Node, so add `LABEL meshagent.runtime=node` to tell `meshagent deploy` that this artifact should run on MeshAgent's Node runtime:
```dockerfile Dockerfile theme={null}
FROM node:22-alpine AS build
WORKDIR /src
COPY package.json package-lock.json ./
RUN npm ci
COPY server.js ./
RUN npx ncc build server.js --target es2022 --out dist
FROM scratch
LABEL meshagent.runtime=node
WORKDIR /app
COPY --from=build /src/dist/index.js ./index.js
ENV NODE_ENV=production
ENV PORT=8080
ENV APP_DATA_DIR=/data
VOLUME /data
EXPOSE 8080
CMD ["/app/index.js"]
```
The final image now contains only the bundled application file and metadata. The `meshagent.runtime=node` label tells `meshagent deploy` to run that application content on MeshAgent's prewarmed Node runtime image.
This helps because the app artifact stays small while the Node runtime comes from an optimized image MeshAgent can prepare ahead of time. Room services are started as part of the room lifecycle, so reducing the app image size and reusing the optimized runtime layer both reduce the amount of work needed before the first request can complete.
## Related docs
* [Routes](../../project_admin/routes): understand private and public routed domains.
* [Build and Deploy Images](../containers/meshagent_image): use MeshAgent image workflows from the CLI.
* [Optimizing Containers](../containers/optimizing_containers): optimize custom containers for faster startup.
# Intro to Services
Source: https://docs.meshagent.com/services/intro
Understand what deployed services are, when to use project or room services, and how Service, ServiceTemplate, container, and external fit together.
In MeshAgent, a deployed service is code MeshAgent saves and starts for you when a room session becomes active. That code can be an agent, a toolkit, an MCP server, an external integration, or other supporting application logic.
Use a deployed service when you want something to stay available in a room or across a project. If you only need to run something temporarily, use the [Containers API](../room_api/containers) instead.
## Project services, room services, and on-demand containers
| Option | Use it when |
| ----------------------- | ----------------------------------------------------------------------------- |
| **Project service** | the capability should be available in every room |
| **Room service** | the capability should be available in one room only or should vary per room |
| **On-demand container** | you want to run something temporarily without saving it as a deployed service |
Use a project service for shared functionality, a room service for per-room behavior, and the Containers API for temporary runs you do not want to save.
## Project services vs room services
Project services are shared across the whole project. Use them for common capabilities that should always be available, such as a shared toolkit or a project-wide agent.
Room services are saved to one room. Use them when the service is room-specific, user-specific, or something people should choose for their room.
The same service manifest can be deployed either way. The scope is set when you deploy it:
* `--global` creates a project service
* `--room myroom` creates a room service
## Service vs ServiceTemplate
A `Service` is ready to run as written.
A `ServiceTemplate` asks for values during deployment or installation, then renders a concrete `Service`.
Use:
* **`Service`** when the configuration is already known
* **`ServiceTemplate`** when users or installers should provide values such as prompts, API keys, names, addresses, or other settings
Both `Service` and `ServiceTemplate` can be used for project services or room services.
Service manifests can also describe the supporting runtime details around that service, such as seeded files, container mounts, agent email settings, and agent heartbeat behavior.
## Container vs external
Under either `Service` or `ServiceTemplate`, choose how MeshAgent reaches the service:
* **`container`**: MeshAgent pulls an image and runs it for you
* **`external`**: the service is already running somewhere else and MeshAgent routes traffic to it
Use `container` when you want MeshAgent to manage the runtime. Use `external` when you already host the service and just want MeshAgent to connect to it.
Both `Service` and `ServiceTemplate` can use either `container` or `external`.
## How deployed services behave
Deployed services are tied to room sessions.
* When a room becomes active, MeshAgent starts the services that should run there.
* When the room goes idle and the session ends, MeshAgent shuts those services down.
* Project services are managed at project scope. Changes usually take effect when the room starts a new session.
* Room services are managed per room. If the saved room service changes during an active session, MeshAgent can reconcile the running service to match.
## What counts as a service?
A service is anything you want MeshAgent to save and make available as part of the room or project runtime.
That can include:
* a process-based agent
* a toolkit or integration
* an MCP server
* a custom HTTP service
* supporting application logic that should run with the room
## Where to go next
* [Deploy a built in Agent](./deployment/deploy_builtin_agent): deploy a process-backed agent from CLI flags
* [Deploy a Web App](./deployment/deploy_web_app): deploy a Node.js app with a private or public `meshagent.app` domain
* [Service YAML](./deployment/deploy_services): write, validate, deploy, update, and delete services from a manifest
* [Containers API](../room_api/containers): run on-demand containers instead of deployed services
* [Build and Deploy Images](./containers/meshagent_image): build and ship container images for deployed services
# Webhook Handoffs
Source: https://docs.meshagent.com/services/webhooks
Handle `room.call` webhook handoffs in your own service with WebhookServer.
Use this page when MeshAgent needs to call your service or toolkit over the webhook transport.
## When to use `WebhookServer`
Use `WebhookServer` when your endpoint needs:
* built-in signature verification
* a default `GET /` health check
* a default `POST /webhook` handler
* support for `room.call` handoffs, including websocket upgrades
## What `room.call` sends
For `room.call`, MeshAgent sends:
* `room_name`
* `room_url`
* `token`
* optional `arguments`
That is the handoff MeshAgent uses when it calls your own service endpoint.
## Configure webhook verification
Set the webhook secret if you want the server to verify signed requests:
```bash theme={null}
export MESHAGENT_WEBHOOK_SECRET=your-secret
```
For local development, you can disable verification with `validate_webhook_secret=False`.
## Run `WebhookServer`
The built-in server listens on port `8080` by default and exposes:
* `GET /` for health checks
* `POST /webhook` for webhook delivery
* `GET /webhook` for websocket upgrades used by `room.call`
Use the SDK to create the server and handle `room.call`:
```python Python theme={null}
from meshagent.api.webhooks import (
WebhookServer,
CallEvent,
)
import asyncio
class CustomWebhookServer(WebhookServer):
async def on_call(self, event: CallEvent):
print(f"room call for {event.room_name}")
print(f"arguments: {event.arguments}")
async def main():
server = CustomWebhookServer()
await server.run()
asyncio.run(main())
```
## Related docs
* [Service YAML](../services/deployment/deploy_services)
* [MeshAgent CLI Commands](../reference/meshagent_cli_help)