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 keeps the example small.
Create a Node.js web server
Create apackage.json file:
package.json
package.json tells Node.js that this project uses JavaScript modules and that npm start should run server.js.
Create the dependency lockfile:
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:
server.js
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
FROM node:22-alpinestarts from a small Linux image with Node.js installed.WORKDIR /appsets the working directory inside the image.COPY package*.json ./copiespackage.jsonandpackage-lock.json.RUN npm ci --omit=devinstalls production dependencies from the lockfile.COPY server.js ./copies the application code.ENV PORT=8080gives the app the HTTP port used by this guide.EXPOSE 8080marks the container port that serves HTTP traffic.CMD ["node", "server.js"]starts the server when the container runs.
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 ameshagent.app domain:
.as the source directory to build.--room my-roomas the room where the service will run.--tag web-hello:v1as the image tag for this version of the app.--domain YOUR_SITE.meshagent.appas the browser URL for the service.--liveness /healthzas the readiness check path.
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:
web-hello:v1 image. It does not include . because it is not building the current directory again.
Open:
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 theX-MESHAGENT-USER request header.
Update server.js to display that user:
server.js
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. Updateserver.js to append each request to a log file:
server.js
http://localhost:8080, then check the file on your machine:
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
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:
/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, go to the room, and inspect room storage. The file is written at:
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:server.js to call the MeshAgent LLM router through the OpenAI SDK:
server.js
OPENAI_BASE_URL points the SDK at the room LLM router 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:
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:
--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:
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
/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
Bundle with ncc
Node.js usually loads code by resolving imports from your app and fromnode_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:
Dockerfile
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
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: understand private and public routed domains.
- Build and Deploy Images: use MeshAgent image workflows from the CLI.
- Optimizing Containers: optimize custom containers for faster startup.