instavm.yaml is the deploy manifest for an InstaVM project. Drop it at the root of your app and run:
instavm deploy .Use it when you want the deploy path committed with the code: reproducible across machines, reviewable in PRs, and not inferred from whatever the local directory looks like.
For a worked example that uses these fields to deploy a real research agent, see Deploy an Exa deep research agent on InstaVM with vault-backed API keys.
Minimal service manifest
Smallest valid manifest for a web service:
schema_version: 2
kind: service
slug: hello-fastapi
title: Hello FastAPI
version: "0.1.0"
summary: Simple FastAPI app with a health endpoint.
category: web
runtime: python-fastapi
deploy:
kind: upload_and_run
source:
include:
- app.py
- requirements.txt
- instavm.yaml
exclude: []
setup_command: python -m pip install --no-cache-dir -r requirements.txt
vm:
memory_mb: 1024
vcpu_count: 1
app:
port: 8000
healthcheck_path: /health
readiness_timeout_seconds: 90
share_public_default: true
run:
workdir: .
start_command: python -m uvicorn app:app --host 0.0.0.0 --port 8000
secrets: []
post_deploy_notes: []Validate the resolved plan before deploying:
instavm deploy . --plan
instavm deploy . --plan --jsonDeploy:
instavm deploy .Use a non-default manifest filename:
instavm deploy -f instavm.web.yamlMetadata fields
Every manifest opens with a metadata header. These fields identify the workload and don't affect runtime behavior on their own.
schema_version: 2
kind: service
slug: hello-fastapi
title: Hello FastAPI
version: "0.1.0"
summary: Simple FastAPI app with a health endpoint.
category: web
runtime: python-fastapi| Field | Meaning |
|---|---|
schema_version |
Manifest schema number required by the parser. |
kind |
Workload type: service, cron, or job. Use service for web deploys. |
slug |
Stable app id. Use lowercase letters, numbers, and dashes. |
title |
Display name. |
version |
App or cookbook version string. |
summary |
One-line description shown in deploy and cookbook output. |
category |
Group label such as web, agents, automation, or dev-environment. |
runtime |
Runtime label such as python-fastapi, node, or code-server. |
deploy
The deploy block selects the deployment strategy.
deploy:
kind: upload_and_runupload_and_run is the strategy for source-based deploys: upload files from source.include, run source.setup_command, start run.start_command, poll app.healthcheck_path, and return a share URL.
For prebuilt cookbooks, point at a snapshot instead:
deploy:
kind: published_snapshot
snapshot_name: cookbook/hello-fastapi:0.1.0Snapshot deploys use artifact and optionally build instead of source.
source
The source block lists what to upload and how to install dependencies.
source:
include:
- app.py
- requirements.txt
- instavm.yaml
exclude:
- .venv
- node_modules
- dist
setup_command: python -m pip install --no-cache-dir -r requirements.txt| Field | Meaning |
|---|---|
include |
Files and directories to upload. At least one item is required. |
exclude |
Files to skip during upload. |
setup_command |
Command run after upload and before run.start_command. |
Keep uploads small. Skip local virtualenvs, dependency folders, build output, caches, and any file that contains a key.
Same shape for a Node app, with NVM-driven setup:
source:
include:
- package.json
- package-lock.json
- src
- public
- server.mjs
- instavm.yaml
exclude:
- node_modules
- dist
setup_command: >-
export NVM_DIR="$HOME/.nvm" &&
if [ ! -s "$NVM_DIR/nvm.sh" ]; then
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash;
fi &&
. "$NVM_DIR/nvm.sh" &&
nvm install 22 &&
nvm use 22 &&
npm ci &&
npm run buildvm
The vm block sizes the microVM and tags it with metadata.
vm:
memory_mb: 2048
vcpu_count: 2
timeout_seconds: 7200
profile: microvm
metadata:
team: agents
env: demo| Field | Meaning |
|---|---|
memory_mb |
Memory requested for the VM. |
vcpu_count |
Number of vCPUs requested. |
timeout_seconds |
Optional maximum VM lifetime. |
image_variant |
Optional base image variant. |
profile |
Optional VM profile: microvm or computer_use. |
metadata |
Key-value metadata attached to the VM. |
Start small. Reach for more memory and CPU when the app runs a browser, builds during setup, hosts a local database, or executes user-generated code.
app
The app block is required for kind: service. It tells InstaVM how to verify the service is healthy.
app:
port: 8000
healthcheck_path: /health
readiness_timeout_seconds: 120
share_public_default: true| Field | Meaning |
|---|---|
port |
Port your service listens on inside the VM. |
healthcheck_path |
Path polled before deploy returns success. Must start with /. |
readiness_timeout_seconds |
Health-check timeout. |
share_public_default |
Whether deploy creates a public share URL by default. |
The port must match your start command. Bind the service to 0.0.0.0, never localhost, or the readiness check will fail from outside the VM.
run
The run block is the long-running command for the service or task.
run:
workdir: .
start_command: python -m uvicorn app:app --host 0.0.0.0 --port 8000
logs_hint: uvicorn| Field | Meaning |
|---|---|
workdir |
Directory where commands run after upload. |
start_command |
Long-running command for the service or task. |
logs_hint |
Optional note for log lookup. |
pre_start_command |
Optional hook before the start command. |
shutdown_command |
Optional hook for shutdown. |
env
Use env for non-secret configuration the app reads from environment variables.
env:
OPENAI_MODEL: gpt-5.5
RESEARCH_MAX_TURNS: "12"Values are parsed as scalars and passed as strings. Never put API keys here. See vault below.
secrets
Use secrets for values the app must read as environment variables inside the VM, but that you don't want committed to the manifest.
secrets:
- name: editor_password
required: true
prompt: Password for the browser editor
env_name: PASSWORD| Field | Meaning |
|---|---|
name |
Manifest-level secret name. |
required |
Whether deploy must collect the value. |
prompt |
Prompt shown by the CLI. |
env_name |
Environment variable name written for the service. |
For API keys that should only ever leave the VM as outbound request headers, and never sit in the process environment, use vault instead.
vault
The vault block declares which third-party hosts get credentials injected at egress. Use it for API keys that should never enter the process environment. See the deep-research cookbook for a worked walkthrough.
vault:
required: true
hosts:
- api.openai.com
- api.exa.aiThe app sends a placeholder string (e.g. OPENAI_KEY, EXA_KEY) on every outbound request to a bound host. The egress proxy substitutes the real credential, but only for that host. A prompt injection that tells the agent to send the same header to a different URL gets a placeholder.
Set up the vault once:
instavm vault create agent-apis
instavm vault secret set <vault-id> OPENAI_KEY --value-file ./openai.key
instavm vault secret set <vault-id> EXA_KEY --value-file ./exa.key
instavm vault service add <vault-id> --template openai --credential OPENAI_KEY
instavm vault service add <vault-id> --host api.exa.ai --auth-type api-key --header x-api-key --credential EXA_KEYWhen vault.required: true and no matching vault is bound, instavm deploy launches the setup flow instead of starting an app that can't reach its upstream API.
egress
The egress block controls which outbound hosts the VM can reach.
egress:
mode: allowlist
include_vault_hosts: true
allowed_domains:
- api.openai.com
- api.exa.ai
allowed_cidrs: []
allow_package_managers: true| Field | Meaning |
|---|---|
mode |
open, deny, or allowlist. |
include_vault_hosts |
Include vault.hosts in the effective allowlist. |
allowed_domains |
Domain allowlist. |
allowed_cidrs |
CIDR allowlist. |
allow_package_managers |
Allow package-manager traffic needed during setup. |
For agents, use mode: allowlist plus the exact API hosts the app calls. open and deny exist but are rarely the right answer for production.
volumes
Use volumes for persistent storage that survives across runs.
volumes:
- name: research-cache
mount: /data
mode: rw
size_gb: 10
create_if_missing: true
auto_checkpoint:
on_success: true
retention: 5| Field | Meaning |
|---|---|
name |
Volume name. |
mount |
Absolute mount path inside the VM. Cannot be /. |
mode |
rw or ro. |
size_gb |
Optional size for created volumes. |
create_if_missing |
Create the volume when it does not exist. |
checkpoint_id |
Mount a checkpoint. Valid only with mode: ro. |
auto_checkpoint |
Create checkpoints after successful runs. |
schedules
Use kind: cron for scheduled work that runs without a request.
schema_version: 2
kind: cron
slug: nightly-sync
title: Nightly Sync
version: "0.1.0"
summary: Nightly cleanup job.
category: automation
runtime: python
deploy:
kind: published_snapshot
artifact:
oci_image: docker.io/acme/nightly-sync:0.1.0
snapshot_name: nightly-sync:0.1.0
snapshot_visibility: private
vm:
memory_mb: 1024
vcpu_count: 1
schedule:
cron: "0 2 * * *"
timezone: UTC
concurrency: skip
catchup: run-once
timeout_seconds: 1800
retries:
max: 2
backoff_seconds: 60
run:
workdir: .
start_command: python jobs/nightly_sync.py
secrets: []
post_deploy_notes: []instavm deploy --plan validates the manifest and prints the resolved plan. This is useful for catching cron typos before they fire at 2am.
jobs and triggers
Use kind: job for work that runs on demand: manual invocation, webhook trigger, or both.
schema_version: 2
kind: job
slug: import-customer-file
title: Import Customer File
version: "0.1.0"
summary: Imports a customer CSV into the database.
category: automation
runtime: python
deploy:
kind: published_snapshot
artifact:
oci_image: docker.io/acme/importer:0.1.0
snapshot_name: importer:0.1.0
snapshot_visibility: private
vm:
memory_mb: 2048
vcpu_count: 2
triggers:
- type: manual
timeout_seconds: 1800
- type: webhook
path: /hooks/import
auth: hmac
timeout_seconds: 1800
run:
workdir: .
start_command: python jobs/import_customer_file.py
secrets: []
post_deploy_notes: []Webhook paths must start with /.
callbacks
Use callbacks to notify another service when a run finishes.
callbacks:
on_success:
- url: https://example.com/hooks/success
retries: 2
signing_secret: WEBHOOK_SIGNING_SECRET
on_failure:
- url: https://example.com/hooks/failure
retries: 2
signing_secret: WEBHOOK_SIGNING_SECRET
secrets:
- name: webhook_signing_secret
required: true
prompt: Webhook signing secret
env_name: WEBHOOK_SIGNING_SECRETsigning_secret must reference a declared secrets[].env_name.
share
Service deploys use app.port and app.share_public_default for the default share URL.
Use the explicit share block when you need to override the default share policy or pin auth requirements:
share:
port: 8080
public: true
auth: noneauth accepts none, password, or api_key. Password auth requires password_secret to reference a declared secret env name.
desktop, sandbox, and domain
Three optional blocks for less-common runtime modes:
desktop:
viewer:
enabled: true
sandbox:
ttl_seconds: 3600
domain:
hostname: demo.example.comIf desktop is present and vm.profile is omitted, the deploy treats the VM as a computer-use VM. If vm.profile: computer_use and desktop.viewer.enabled is omitted, the viewer defaults to enabled.
Full web-agent example
A complete manifest combining most of the blocks above. This is the same shape used by the deep-research cookbook:
schema_version: 2
kind: service
slug: deep-research-exa
title: Deep Research Exa
version: "0.1.0"
summary: Research agent using Exa and the OpenAI Agents SDK.
category: agents
runtime: python-fastapi
deploy:
kind: upload_and_run
source:
include:
- app.py
- requirements.txt
- instavm.yaml
- README.md
exclude:
- .venv
- __pycache__
setup_command: python -m pip install --no-cache-dir -r requirements.txt
vm:
memory_mb: 2048
vcpu_count: 2
timeout_seconds: 86400
metadata:
app: deep-research-exa
app:
port: 8000
healthcheck_path: /health
readiness_timeout_seconds: 240
share_public_default: true
run:
workdir: .
start_command: python -m uvicorn app:app --host 0.0.0.0 --port 8000
env:
OPENAI_MODEL: gpt-5.5
RESEARCH_MAX_TURNS: "12"
vault:
required: true
hosts:
- api.openai.com
- api.exa.ai
egress:
mode: allowlist
include_vault_hosts: true
allowed_domains:
- api.openai.com
- api.exa.ai
allowed_cidrs: []
allow_package_managers: true
secrets: []
post_deploy_notes:
- Open the share URL and ask a research question.
- Check /health for vault_mode before sending traffic.Deploy:
instavm deploy .Override common values from the CLI without editing the manifest:
instavm deploy . --port 9090 --health-path /_health --privateJSON output for scripts:
instavm deploy . --jsonChecklist
Run through this before every deploy:
kind: servicefor web appssource.includecontains every file needed at runtime- local caches, dependency folders, and key files are excluded
source.setup_commandinstalls dependenciesrun.start_commandbinds to0.0.0.0app.portmatches the listening portapp.healthcheck_pathstarts with/- non-secret config goes in
env - app-readable secrets go in
secrets - API keys that should only touch outbound requests go in
vault egress.mode: allowlistlists only the hosts the app needsinstavm deploy . --planshows the expected command, port, and secrets