Agent rules
Drop one of these into your project so your AI coding tool generates apps that deploy on Dockhold with no edits.
They all say the same thing in each tool's format: how Dockhold deploys, where config and secrets go, and the one rule that trips up most generated apps: listen on 0.0.0.0:$PORT. Pick the file your tool reads.
.cursorrules (Cursor)
Save at your repo root as .cursorrules:
# Dockhold deploy rules
Deploy target: Dockhold (https://dockhold.eu).
How deploys work:
- Two deploy paths. Code in a GitHub repo: connect it once at https://app.dockhold.eu, then deploy by pushing to the main branch. Code only local (no git remote): deploy with the Dockhold CLI (see the local-code block below). Never invent another deploy method.
- Dockhold auto-detects the stack (Node, Python, Go, Rust, Ruby, Deno, Bun, Java, PHP) on any account with purchased compute. On a FREE account, ship a Dockerfile: without one the deploy fails immediately with "No Dockerfile found". To control the build on any plan, add a Dockerfile at the repo root, or a dockhold.json with {"build":{"dockerfile":"path/to/Dockerfile"}}.
App requirements:
- Listen on 0.0.0.0 and the port from the PORT environment variable. Never hardcode a port or bind to localhost.
- Read all configuration from environment variables.
- Do not commit secrets. Set secrets (API keys, tokens, passwords) in the Dockhold dashboard Vault, or, if deploying over MCP with a secrets-scoped token, with the set_app_secret tool; they are injected at runtime either way.
- PORT and DATABASE_URL are set by Dockhold. Read them, never define or override them.
- Dockhold also sets the app's own public address on every app: DOCKHOLD_APP_URL carries the scheme (https://my-app-a1b2c3.dockhold.app), DOCKHOLD_APP_HOSTNAME is the bare authority (my-app-a1b2c3.dockhold.app). Use the hostname one for any allowed-hosts, trusted-hosts, or cookie-domain setting, because those reject a value containing :// or a path; use the URL one for absolute links, OAuth redirect URIs and webhook callbacks. Do not try to derive the address from the container hostname (it is a runtime-assigned id) or from a request Host header (attacker-controlled).
- Moving an app that already reads another platform's variable name: do not edit the code. Add a dashboard variable under the name the app reads and set its value to $(DOCKHOLD_APP_URL) or $(DOCKHOLD_APP_HOSTNAME); it is expanded at start-up. For example APP_URL=$(DOCKHOLD_APP_URL) for Laravel. This works only for variables the user sets, not inside a Dockerfile.
- For a database, enable the managed Postgres add-on and read DATABASE_URL. Do not run or configure your own database.
- Apps are stateless by default: the container filesystem is ephemeral and lost on restart. Persist data in the managed database, not on local disk.
- If the app must keep files (a SQLite database file, uploads, a tool with its own data directory), turn on app storage for it: dashboard Size tab -> App storage, or the MCP set_app_storage tool. Then read the folder path from the DATA_DIR environment variable, which Dockhold sets; never hardcode a path. Files under DATA_DIR survive restarts and deploys, files outside it do not. Storage is opt-in and off unless asked for, so do not assume an app has it.
- An app with storage: runs as a single copy, cannot have scheduled tasks, and is stopped and restarted on each deploy instead of overlapping versions (a few seconds unreachable). Handle SIGTERM and flush open writes within 60 seconds, or the last writes are lost on every deploy.
- Scheduled tasks (dashboard Tasks tab) run a command on a cron schedule as their own separate process, not inside the app. A task can reach the app at DOCKHOLD_APP_URL, the app's managed database, and the internet, and nothing else. Call the app with curl -s "$DOCKHOLD_APP_URL/your/path", never localhost: localhost inside a task is the task's own process, not the app. If the app is private, the request needs a token like any other caller, or it gets a 401. Not available on an app that has its own storage.
- To seed the managed database with existing data (one-time, paid plan): dump it with `pg_dump -Fc --no-owner --no-privileges yourdb > backup.dump` and upload from the app's Database tab. Cap 2 GB after compression; all-or-nothing, so import into an empty database.
Local-only code (no GitHub repo):
- Run `npx dockhold login` (opens a browser once; needs Node 18+), then `npx dockhold deploy` from the project folder. It uploads the folder, builds it (your Dockerfile if present, else auto-detect), and prints the live URL. Same free-account rule as above: without purchased compute the CLI stops before uploading and asks for a Dockerfile at the folder root.
- `.env` files are never uploaded. Set variables with `--env KEY=VALUE` (repeatable) or in the dashboard; secrets in the Vault. Add `--db` for a managed database (read `DATABASE_URL`).
- Always excluded from the upload: `.git`, `node_modules`, `.dockhold`, every `.env*` file. `.gitignore` is respected; a `.dockholdignore` file excludes more. Upload cap 500 MB.
- Ship a new version by running `npx dockhold deploy` again from the same folder. These apps have no git auto-deploy.
Multiple services that talk to each other:
- If you generate several services that call each other (e.g. a frontend and an API), do NOT hardcode one service's URL in another. Read it from an environment variable (e.g. API_URL) and call that.
- On Dockhold you deploy them as one "connected app", either in the dashboard (https://app.dockhold.eu/new → "Deploy them together") or via the MCP deploy_group tool (see MCP rules below). Add each service (a GitHub repo URL + port), then link each such variable to the sibling service by setting it to exactly ${services.<sibling>.url}. Dockhold injects that sibling's live URL into the variable.
- A service in a connected app can have its own managed database: tick "Add a managed database" on that service (usually the API, not the frontend) and read DATABASE_URL. Each database belongs to the service that enabled it; a shared database across services and bring-your-own external database are not supported yet.
- First cut: web services only, one GitHub repo per service.
Deploying via MCP (optional):
- Dockhold runs a remote MCP server at https://api.dockhold.eu/mcp. With a dh_mcp_ API token from https://app.dockhold.eu/settings/api-tokens, passed as Authorization: Bearer, an MCP client can deploy and manage apps directly.
- Tools. Read: list_apps, get_app_status, get_app_logs, list_github_repos, get_resource_usage. Read+deploy: deploy_app, redeploy_app, set_app_variable, deploy_group, resize_app, resize_database, resize_database_storage, set_app_storage, remove_app_storage. Secrets (token needs the additive secrets checkbox, separate from read/deploy): set_app_secret, unset_app_secret, list_app_secrets.
- deploy_app takes a GitHub repo URL and a name; poll get_app_status until the URL is live. Public repo: repo_url + name. Private repo: call list_github_repos first (each connected repo comes with its installation_id), then deploy_app with repo_url + name + github_installation_id; this also turns auto-deploy on.
- Call get_resource_usage before deploy_app and pass memory_mb. Do not assume the account is empty; it may already hold apps, and compute.available_mb is what is actually left. Sizes: 256 MB for a static site or a small API, 512 MB for a typical web app, more for anything that holds data in memory. Omit memory_mb and the app gets the minimum slice, 256 MB, which always fits when any capacity is left. deploy_app reports memory_mb and compute_available_mb; if compute_available_mb is absent the usage read failed, so call get_resource_usage rather than treating it as zero. resize_app changes the size later with no rebuild, applied as a rolling update that replaces the app instances.
- If an app runs out of memory (OOM), resize it: resize_app(app_id, memory_mb). resize_database(app_id, db_ram_mb) grows a managed DB's memory (it restarts briefly); resize_database_storage(app_id, storage_gb) grows its disk (grow-only). Call get_resource_usage first for valid sizes (its *.steps_* arrays) and to confirm the pool has room; an over-pool size is refused.
- set_app_storage(app_id, size_gb) gives the app a folder that survives restarts and deploys, at the path in DATA_DIR; sizes come from get_resource_usage volume.steps_gb and it is grow-only. It is refused while the app runs more than one copy or has any scheduled task, and the reverse is refused too, so read the message rather than retrying the other side. remove_app_storage(app_id, confirm) erases every file on it permanently: ask the user first. The free allowance covers a managed database or app storage, never both.
- deploy_group deploys several connected services at once: pass a services map (each with source repo URL, port, optional env, optional db:"enable"); link services by setting an env value to exactly ${services.<name>.url}; a service with db:"enable" gets its own database and reads DATABASE_URL. Returns each service's app id; poll get_app_status on each. Public repos only.
- set_app_secret(app_id, name, value, vault_key_name?) stores a sensitive value for one app: encrypted at rest, injected under name, app restarts to pick it up. Use it instead of set_app_variable for anything damaging if read by someone else. It is write-only forever: no tool ever returns a stored value back, only the dashboard does, while signed in. A key already used by a different app is refused rather than overwritten; the refusal names an explicit vault_key_name to pass instead. Tell the user two things before calling it: the token this runs under can write secrets to every app on the account, not only this one, and any value they hand you has passed through your own context, so prefer a value read from a local file such as .env over one typed into the chat, and suggest they rotate anything that was pasted. unset_app_secret(app_id, name) stops injecting a secret into one app without deleting the stored value, so another app using it keeps working; report it as removed from the app, not deleted. list_app_secrets(app_id) returns bound names only, never values.
- A token only does what its owner could do in the dashboard, scoped to their account. set_app_variable still refuses a secret-looking key and points at set_app_secret for it.AGENTS.md
Save at your repo root as AGENTS.md:
# Deploying on Dockhold
Deploy target: Dockhold (https://dockhold.eu).
How deploys work:
- Two deploy paths. Code in a GitHub repo: connect it once at https://app.dockhold.eu, then deploy by pushing to the main branch. Code only local (no git remote): deploy with the Dockhold CLI (see the local-code block below). Never invent another deploy method.
- Dockhold auto-detects the stack (Node, Python, Go, Rust, Ruby, Deno, Bun, Java, PHP) on any account with purchased compute. On a FREE account, ship a Dockerfile: without one the deploy fails immediately with "No Dockerfile found". To control the build on any plan, add a Dockerfile at the repo root, or a dockhold.json with {"build":{"dockerfile":"path/to/Dockerfile"}}.
App requirements:
- Listen on 0.0.0.0 and the port from the PORT environment variable. Never hardcode a port or bind to localhost.
- Read all configuration from environment variables.
- Do not commit secrets. Set secrets (API keys, tokens, passwords) in the Dockhold dashboard Vault, or, if deploying over MCP with a secrets-scoped token, with the set_app_secret tool; they are injected at runtime either way.
- PORT and DATABASE_URL are set by Dockhold. Read them, never define or override them.
- Dockhold also sets the app's own public address on every app: DOCKHOLD_APP_URL carries the scheme (https://my-app-a1b2c3.dockhold.app), DOCKHOLD_APP_HOSTNAME is the bare authority (my-app-a1b2c3.dockhold.app). Use the hostname one for any allowed-hosts, trusted-hosts, or cookie-domain setting, because those reject a value containing :// or a path; use the URL one for absolute links, OAuth redirect URIs and webhook callbacks. Do not try to derive the address from the container hostname (it is a runtime-assigned id) or from a request Host header (attacker-controlled).
- Moving an app that already reads another platform's variable name: do not edit the code. Add a dashboard variable under the name the app reads and set its value to $(DOCKHOLD_APP_URL) or $(DOCKHOLD_APP_HOSTNAME); it is expanded at start-up. For example APP_URL=$(DOCKHOLD_APP_URL) for Laravel. This works only for variables the user sets, not inside a Dockerfile.
- For a database, enable the managed Postgres add-on and read DATABASE_URL. Do not run or configure your own database.
- Apps are stateless by default: the container filesystem is ephemeral and lost on restart. Persist data in the managed database, not on local disk.
- If the app must keep files (a SQLite database file, uploads, a tool with its own data directory), turn on app storage for it: dashboard Size tab -> App storage, or the MCP set_app_storage tool. Then read the folder path from the DATA_DIR environment variable, which Dockhold sets; never hardcode a path. Files under DATA_DIR survive restarts and deploys, files outside it do not. Storage is opt-in and off unless asked for, so do not assume an app has it.
- An app with storage: runs as a single copy, cannot have scheduled tasks, and is stopped and restarted on each deploy instead of overlapping versions (a few seconds unreachable). Handle SIGTERM and flush open writes within 60 seconds, or the last writes are lost on every deploy.
- Scheduled tasks (dashboard Tasks tab) run a command on a cron schedule as their own separate process, not inside the app. A task can reach the app at DOCKHOLD_APP_URL, the app's managed database, and the internet, and nothing else. Call the app with curl -s "$DOCKHOLD_APP_URL/your/path", never localhost: localhost inside a task is the task's own process, not the app. If the app is private, the request needs a token like any other caller, or it gets a 401. Not available on an app that has its own storage.
- To seed the managed database with existing data (one-time, paid plan): dump it with `pg_dump -Fc --no-owner --no-privileges yourdb > backup.dump` and upload from the app's Database tab. Cap 2 GB after compression; all-or-nothing, so import into an empty database.
Local-only code (no GitHub repo):
- Run `npx dockhold login` (opens a browser once; needs Node 18+), then `npx dockhold deploy` from the project folder. It uploads the folder, builds it (your Dockerfile if present, else auto-detect), and prints the live URL. Same free-account rule as above: without purchased compute the CLI stops before uploading and asks for a Dockerfile at the folder root.
- `.env` files are never uploaded. Set variables with `--env KEY=VALUE` (repeatable) or in the dashboard; secrets in the Vault. Add `--db` for a managed database (read `DATABASE_URL`).
- Always excluded from the upload: `.git`, `node_modules`, `.dockhold`, every `.env*` file. `.gitignore` is respected; a `.dockholdignore` file excludes more. Upload cap 500 MB.
- Ship a new version by running `npx dockhold deploy` again from the same folder. These apps have no git auto-deploy.
Multiple services that talk to each other:
- If you generate several services that call each other (e.g. a frontend and an API), do NOT hardcode one service's URL in another. Read it from an environment variable (e.g. API_URL) and call that.
- On Dockhold you deploy them as one "connected app", either in the dashboard (https://app.dockhold.eu/new → "Deploy them together") or via the MCP deploy_group tool (see MCP rules below). Add each service (a GitHub repo URL + port), then link each such variable to the sibling service by setting it to exactly ${services.<sibling>.url}. Dockhold injects that sibling's live URL into the variable.
- A service in a connected app can have its own managed database: tick "Add a managed database" on that service (usually the API, not the frontend) and read DATABASE_URL. Each database belongs to the service that enabled it; a shared database across services and bring-your-own external database are not supported yet.
- First cut: web services only, one GitHub repo per service.
Deploying via MCP (optional):
- Dockhold runs a remote MCP server at https://api.dockhold.eu/mcp. With a dh_mcp_ API token from https://app.dockhold.eu/settings/api-tokens, passed as Authorization: Bearer, an MCP client can deploy and manage apps directly.
- Tools. Read: list_apps, get_app_status, get_app_logs, list_github_repos, get_resource_usage. Read+deploy: deploy_app, redeploy_app, set_app_variable, deploy_group, resize_app, resize_database, resize_database_storage, set_app_storage, remove_app_storage. Secrets (token needs the additive secrets checkbox, separate from read/deploy): set_app_secret, unset_app_secret, list_app_secrets.
- deploy_app takes a GitHub repo URL and a name; poll get_app_status until the URL is live. Public repo: repo_url + name. Private repo: call list_github_repos first (each connected repo comes with its installation_id), then deploy_app with repo_url + name + github_installation_id; this also turns auto-deploy on.
- Call get_resource_usage before deploy_app and pass memory_mb. Do not assume the account is empty; it may already hold apps, and compute.available_mb is what is actually left. Sizes: 256 MB for a static site or a small API, 512 MB for a typical web app, more for anything that holds data in memory. Omit memory_mb and the app gets the minimum slice, 256 MB, which always fits when any capacity is left. deploy_app reports memory_mb and compute_available_mb; if compute_available_mb is absent the usage read failed, so call get_resource_usage rather than treating it as zero. resize_app changes the size later with no rebuild, applied as a rolling update that replaces the app instances.
- If an app runs out of memory (OOM), resize it: resize_app(app_id, memory_mb). resize_database(app_id, db_ram_mb) grows a managed DB's memory (it restarts briefly); resize_database_storage(app_id, storage_gb) grows its disk (grow-only). Call get_resource_usage first for valid sizes (its *.steps_* arrays) and to confirm the pool has room; an over-pool size is refused.
- set_app_storage(app_id, size_gb) gives the app a folder that survives restarts and deploys, at the path in DATA_DIR; sizes come from get_resource_usage volume.steps_gb and it is grow-only. It is refused while the app runs more than one copy or has any scheduled task, and the reverse is refused too, so read the message rather than retrying the other side. remove_app_storage(app_id, confirm) erases every file on it permanently: ask the user first. The free allowance covers a managed database or app storage, never both.
- deploy_group deploys several connected services at once: pass a services map (each with source repo URL, port, optional env, optional db:"enable"); link services by setting an env value to exactly ${services.<name>.url}; a service with db:"enable" gets its own database and reads DATABASE_URL. Returns each service's app id; poll get_app_status on each. Public repos only.
- set_app_secret(app_id, name, value, vault_key_name?) stores a sensitive value for one app: encrypted at rest, injected under name, app restarts to pick it up. Use it instead of set_app_variable for anything damaging if read by someone else. It is write-only forever: no tool ever returns a stored value back, only the dashboard does, while signed in. A key already used by a different app is refused rather than overwritten; the refusal names an explicit vault_key_name to pass instead. Tell the user two things before calling it: the token this runs under can write secrets to every app on the account, not only this one, and any value they hand you has passed through your own context, so prefer a value read from a local file such as .env over one typed into the chat, and suggest they rotate anything that was pasted. unset_app_secret(app_id, name) stops injecting a secret into one app without deleting the stored value, so another app using it keeps working; report it as removed from the app, not deleted. list_app_secrets(app_id) returns bound names only, never values.
- A token only does what its owner could do in the dashboard, scoped to their account. set_app_variable still refuses a secret-looking key and points at set_app_secret for it.CLAUDE.md (Claude Code)
Save at your repo root as CLAUDE.md:
# Deploying on Dockhold
Deploy target: Dockhold (https://dockhold.eu).
How deploys work:
- Two deploy paths. Code in a GitHub repo: connect it once at https://app.dockhold.eu, then deploy by pushing to the main branch. Code only local (no git remote): deploy with the Dockhold CLI (see the local-code block below). Never invent another deploy method.
- Dockhold auto-detects the stack (Node, Python, Go, Rust, Ruby, Deno, Bun, Java, PHP) on any account with purchased compute. On a FREE account, ship a Dockerfile: without one the deploy fails immediately with "No Dockerfile found". To control the build on any plan, add a Dockerfile at the repo root, or a dockhold.json with {"build":{"dockerfile":"path/to/Dockerfile"}}.
App requirements:
- Listen on 0.0.0.0 and the port from the PORT environment variable. Never hardcode a port or bind to localhost.
- Read all configuration from environment variables.
- Do not commit secrets. Set secrets (API keys, tokens, passwords) in the Dockhold dashboard Vault, or, if deploying over MCP with a secrets-scoped token, with the set_app_secret tool; they are injected at runtime either way.
- PORT and DATABASE_URL are set by Dockhold. Read them, never define or override them.
- Dockhold also sets the app's own public address on every app: DOCKHOLD_APP_URL carries the scheme (https://my-app-a1b2c3.dockhold.app), DOCKHOLD_APP_HOSTNAME is the bare authority (my-app-a1b2c3.dockhold.app). Use the hostname one for any allowed-hosts, trusted-hosts, or cookie-domain setting, because those reject a value containing :// or a path; use the URL one for absolute links, OAuth redirect URIs and webhook callbacks. Do not try to derive the address from the container hostname (it is a runtime-assigned id) or from a request Host header (attacker-controlled).
- Moving an app that already reads another platform's variable name: do not edit the code. Add a dashboard variable under the name the app reads and set its value to $(DOCKHOLD_APP_URL) or $(DOCKHOLD_APP_HOSTNAME); it is expanded at start-up. For example APP_URL=$(DOCKHOLD_APP_URL) for Laravel. This works only for variables the user sets, not inside a Dockerfile.
- For a database, enable the managed Postgres add-on and read DATABASE_URL. Do not run or configure your own database.
- Apps are stateless by default: the container filesystem is ephemeral and lost on restart. Persist data in the managed database, not on local disk.
- If the app must keep files (a SQLite database file, uploads, a tool with its own data directory), turn on app storage for it: dashboard Size tab -> App storage, or the MCP set_app_storage tool. Then read the folder path from the DATA_DIR environment variable, which Dockhold sets; never hardcode a path. Files under DATA_DIR survive restarts and deploys, files outside it do not. Storage is opt-in and off unless asked for, so do not assume an app has it.
- An app with storage: runs as a single copy, cannot have scheduled tasks, and is stopped and restarted on each deploy instead of overlapping versions (a few seconds unreachable). Handle SIGTERM and flush open writes within 60 seconds, or the last writes are lost on every deploy.
- Scheduled tasks (dashboard Tasks tab) run a command on a cron schedule as their own separate process, not inside the app. A task can reach the app at DOCKHOLD_APP_URL, the app's managed database, and the internet, and nothing else. Call the app with curl -s "$DOCKHOLD_APP_URL/your/path", never localhost: localhost inside a task is the task's own process, not the app. If the app is private, the request needs a token like any other caller, or it gets a 401. Not available on an app that has its own storage.
- To seed the managed database with existing data (one-time, paid plan): dump it with `pg_dump -Fc --no-owner --no-privileges yourdb > backup.dump` and upload from the app's Database tab. Cap 2 GB after compression; all-or-nothing, so import into an empty database.
Local-only code (no GitHub repo):
- Run `npx dockhold login` (opens a browser once; needs Node 18+), then `npx dockhold deploy` from the project folder. It uploads the folder, builds it (your Dockerfile if present, else auto-detect), and prints the live URL. Same free-account rule as above: without purchased compute the CLI stops before uploading and asks for a Dockerfile at the folder root.
- `.env` files are never uploaded. Set variables with `--env KEY=VALUE` (repeatable) or in the dashboard; secrets in the Vault. Add `--db` for a managed database (read `DATABASE_URL`).
- Always excluded from the upload: `.git`, `node_modules`, `.dockhold`, every `.env*` file. `.gitignore` is respected; a `.dockholdignore` file excludes more. Upload cap 500 MB.
- Ship a new version by running `npx dockhold deploy` again from the same folder. These apps have no git auto-deploy.
Multiple services that talk to each other:
- If you generate several services that call each other (e.g. a frontend and an API), do NOT hardcode one service's URL in another. Read it from an environment variable (e.g. API_URL) and call that.
- On Dockhold you deploy them as one "connected app", either in the dashboard (https://app.dockhold.eu/new → "Deploy them together") or via the MCP deploy_group tool (see MCP rules below). Add each service (a GitHub repo URL + port), then link each such variable to the sibling service by setting it to exactly ${services.<sibling>.url}. Dockhold injects that sibling's live URL into the variable.
- A service in a connected app can have its own managed database: tick "Add a managed database" on that service (usually the API, not the frontend) and read DATABASE_URL. Each database belongs to the service that enabled it; a shared database across services and bring-your-own external database are not supported yet.
- First cut: web services only, one GitHub repo per service.
Deploying via MCP (optional):
- Dockhold runs a remote MCP server at https://api.dockhold.eu/mcp. With a dh_mcp_ API token from https://app.dockhold.eu/settings/api-tokens, passed as Authorization: Bearer, an MCP client can deploy and manage apps directly.
- Tools. Read: list_apps, get_app_status, get_app_logs, list_github_repos, get_resource_usage. Read+deploy: deploy_app, redeploy_app, set_app_variable, deploy_group, resize_app, resize_database, resize_database_storage, set_app_storage, remove_app_storage. Secrets (token needs the additive secrets checkbox, separate from read/deploy): set_app_secret, unset_app_secret, list_app_secrets.
- deploy_app takes a GitHub repo URL and a name; poll get_app_status until the URL is live. Public repo: repo_url + name. Private repo: call list_github_repos first (each connected repo comes with its installation_id), then deploy_app with repo_url + name + github_installation_id; this also turns auto-deploy on.
- Call get_resource_usage before deploy_app and pass memory_mb. Do not assume the account is empty; it may already hold apps, and compute.available_mb is what is actually left. Sizes: 256 MB for a static site or a small API, 512 MB for a typical web app, more for anything that holds data in memory. Omit memory_mb and the app gets the minimum slice, 256 MB, which always fits when any capacity is left. deploy_app reports memory_mb and compute_available_mb; if compute_available_mb is absent the usage read failed, so call get_resource_usage rather than treating it as zero. resize_app changes the size later with no rebuild, applied as a rolling update that replaces the app instances.
- If an app runs out of memory (OOM), resize it: resize_app(app_id, memory_mb). resize_database(app_id, db_ram_mb) grows a managed DB's memory (it restarts briefly); resize_database_storage(app_id, storage_gb) grows its disk (grow-only). Call get_resource_usage first for valid sizes (its *.steps_* arrays) and to confirm the pool has room; an over-pool size is refused.
- set_app_storage(app_id, size_gb) gives the app a folder that survives restarts and deploys, at the path in DATA_DIR; sizes come from get_resource_usage volume.steps_gb and it is grow-only. It is refused while the app runs more than one copy or has any scheduled task, and the reverse is refused too, so read the message rather than retrying the other side. remove_app_storage(app_id, confirm) erases every file on it permanently: ask the user first. The free allowance covers a managed database or app storage, never both.
- deploy_group deploys several connected services at once: pass a services map (each with source repo URL, port, optional env, optional db:"enable"); link services by setting an env value to exactly ${services.<name>.url}; a service with db:"enable" gets its own database and reads DATABASE_URL. Returns each service's app id; poll get_app_status on each. Public repos only.
- set_app_secret(app_id, name, value, vault_key_name?) stores a sensitive value for one app: encrypted at rest, injected under name, app restarts to pick it up. Use it instead of set_app_variable for anything damaging if read by someone else. It is write-only forever: no tool ever returns a stored value back, only the dashboard does, while signed in. A key already used by a different app is refused rather than overwritten; the refusal names an explicit vault_key_name to pass instead. Tell the user two things before calling it: the token this runs under can write secrets to every app on the account, not only this one, and any value they hand you has passed through your own context, so prefer a value read from a local file such as .env over one typed into the chat, and suggest they rotate anything that was pasted. unset_app_secret(app_id, name) stops injecting a secret into one app without deleting the stored value, so another app using it keeps working; report it as removed from the app, not deleted. list_app_secrets(app_id) returns bound names only, never values.
- A token only does what its owner could do in the dashboard, scoped to their account. set_app_variable still refuses a secret-looking key and points at set_app_secret for it.See also
- Quickstart: the same steps for a human.
- llms.txt: machine-readable map for tools that ingest it.