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/new?ref=docs, 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.
- Build source, first match wins: a Dockerfile at the repo root; a dockhold.json with {"build":{"dockerfile":"path/to/Dockerfile"}}; on EVERY account, one of the stacks Dockhold recognises (Next.js with output: "standalone" in next.config.js, Vite, Node with a start script and a package-lock.json and no build script, a static site with index.html at the root, FastAPI with requirements.txt naming fastapi and main.py defining app); on any account with purchased compute, automatic detection of any stack (Node, Python, Go, Rust, Ruby, Deno, Bun, Java, PHP). A FREE account whose repo matches none of these fails within seconds with "Could not build this automatically." followed by the fix: apply the fix, or ship a Dockerfile. The recognised stacks use npm; commit package-lock.json, not pnpm or yarn lockfiles.

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) under Secrets in the Dockhold dashboard sidebar; they are injected at runtime. The set_app_secret MCP tool exists only over the remote HTTP path with a secrets-scoped dashboard token (see the MCP rules below); the browser sign-in the bridge uses never has it, so over the bridge, send the user to the dashboard.
- 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.
- Backups: paid accounts get the managed database backed up nightly (newest 7 kept, encrypted per app, stored in a separate location in Germany, locked 7 days). Free accounts get none. To restore, the user writes to [email protected] with the app and date; it is restored into a second database beside the live one. App storage (DATA_DIR) is NOT backed up.
- 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 under Secrets in the dashboard sidebar. 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?ref=docs → "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):
- Any MCP client can deploy and manage apps on Dockhold through the stdio bridge `npx -y dockhold mcp` (Node 18+). Claude Code: `/plugin marketplace add dockhold/mcp` then `/plugin install dockhold@dockhold`. VS Code: `code --add-mcp '{"name":"dockhold","command":"npx","args":["-y","dockhold","mcp"],"env":{"DOCKHOLD_REF":"vscode"}}'`. Cursor, Windsurf, any JSON-config client: {"mcpServers":{"dockhold":{"command":"npx","args":["-y","dockhold","mcp"],"env":{"DOCKHOLD_REF":"<client name>"}}}}. Codex: `codex mcp add dockhold --env DOCKHOLD_REF=codex -- npx -y dockhold mcp`. Then the user signs in once from a terminal with `npx dockhold login` (opens a browser; the same sign-in serves `npx dockhold deploy`; no token goes into any config file). Before sign-in every tool call returns "Not signed in to Dockhold. Run "npx dockhold login" in a terminal, then try again.": relay or run it, then retry without restarting the client. If the client times out on the first start while npx downloads the package: `npm i -g dockhold`, then command `dockhold`, args ["mcp"]. Full page: https://dockhold.eu/docs/recipes/deploy-from-your-ai-tool
- Tools. Read: list_apps, get_app_status, get_app_logs, list_github_repos, get_resource_usage. Read+deploy: deploy_app, redeploy_app, rebuild_app, set_app_variable, deploy_group, resize_app, resize_database, resize_database_storage, set_app_storage, remove_app_storage. The browser sign-in the bridge uses has the deploy scope only: no secrets tool exists on it, so when a secret is needed, send the user to Secrets in the dashboard sidebar.
- 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.
- 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.
- Remote HTTP path, only for a client that cannot start a command: server https://api.dockhold.eu/mcp, authenticated with a dashboard API token from https://app.dockhold.eu/settings/api-tokens (dh_mcp_ prefix; read-only or read+deploy, plus an additive secrets checkbox), passed as Authorization: Bearer. Only this path can carry the secrets scope; the browser sign-in never has it. With that scope the client also gets set_app_secret, bind_app_secret, unset_app_secret, list_app_secrets, list_secrets.
- Over the remote HTTP path with a secrets-scoped dashboard token 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.

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/new?ref=docs, 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.
- Build source, first match wins: a Dockerfile at the repo root; a dockhold.json with {"build":{"dockerfile":"path/to/Dockerfile"}}; on EVERY account, one of the stacks Dockhold recognises (Next.js with output: "standalone" in next.config.js, Vite, Node with a start script and a package-lock.json and no build script, a static site with index.html at the root, FastAPI with requirements.txt naming fastapi and main.py defining app); on any account with purchased compute, automatic detection of any stack (Node, Python, Go, Rust, Ruby, Deno, Bun, Java, PHP). A FREE account whose repo matches none of these fails within seconds with "Could not build this automatically." followed by the fix: apply the fix, or ship a Dockerfile. The recognised stacks use npm; commit package-lock.json, not pnpm or yarn lockfiles.

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) under Secrets in the Dockhold dashboard sidebar; they are injected at runtime. The set_app_secret MCP tool exists only over the remote HTTP path with a secrets-scoped dashboard token (see the MCP rules below); the browser sign-in the bridge uses never has it, so over the bridge, send the user to the dashboard.
- 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.
- Backups: paid accounts get the managed database backed up nightly (newest 7 kept, encrypted per app, stored in a separate location in Germany, locked 7 days). Free accounts get none. To restore, the user writes to [email protected] with the app and date; it is restored into a second database beside the live one. App storage (DATA_DIR) is NOT backed up.
- 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 under Secrets in the dashboard sidebar. 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?ref=docs → "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):
- Any MCP client can deploy and manage apps on Dockhold through the stdio bridge `npx -y dockhold mcp` (Node 18+). Claude Code: `/plugin marketplace add dockhold/mcp` then `/plugin install dockhold@dockhold`. VS Code: `code --add-mcp '{"name":"dockhold","command":"npx","args":["-y","dockhold","mcp"],"env":{"DOCKHOLD_REF":"vscode"}}'`. Cursor, Windsurf, any JSON-config client: {"mcpServers":{"dockhold":{"command":"npx","args":["-y","dockhold","mcp"],"env":{"DOCKHOLD_REF":"<client name>"}}}}. Codex: `codex mcp add dockhold --env DOCKHOLD_REF=codex -- npx -y dockhold mcp`. Then the user signs in once from a terminal with `npx dockhold login` (opens a browser; the same sign-in serves `npx dockhold deploy`; no token goes into any config file). Before sign-in every tool call returns "Not signed in to Dockhold. Run "npx dockhold login" in a terminal, then try again.": relay or run it, then retry without restarting the client. If the client times out on the first start while npx downloads the package: `npm i -g dockhold`, then command `dockhold`, args ["mcp"]. Full page: https://dockhold.eu/docs/recipes/deploy-from-your-ai-tool
- Tools. Read: list_apps, get_app_status, get_app_logs, list_github_repos, get_resource_usage. Read+deploy: deploy_app, redeploy_app, rebuild_app, set_app_variable, deploy_group, resize_app, resize_database, resize_database_storage, set_app_storage, remove_app_storage. The browser sign-in the bridge uses has the deploy scope only: no secrets tool exists on it, so when a secret is needed, send the user to Secrets in the dashboard sidebar.
- 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.
- 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.
- Remote HTTP path, only for a client that cannot start a command: server https://api.dockhold.eu/mcp, authenticated with a dashboard API token from https://app.dockhold.eu/settings/api-tokens (dh_mcp_ prefix; read-only or read+deploy, plus an additive secrets checkbox), passed as Authorization: Bearer. Only this path can carry the secrets scope; the browser sign-in never has it. With that scope the client also gets set_app_secret, bind_app_secret, unset_app_secret, list_app_secrets, list_secrets.
- Over the remote HTTP path with a secrets-scoped dashboard token 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.

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/new?ref=docs, 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.
- Build source, first match wins: a Dockerfile at the repo root; a dockhold.json with {"build":{"dockerfile":"path/to/Dockerfile"}}; on EVERY account, one of the stacks Dockhold recognises (Next.js with output: "standalone" in next.config.js, Vite, Node with a start script and a package-lock.json and no build script, a static site with index.html at the root, FastAPI with requirements.txt naming fastapi and main.py defining app); on any account with purchased compute, automatic detection of any stack (Node, Python, Go, Rust, Ruby, Deno, Bun, Java, PHP). A FREE account whose repo matches none of these fails within seconds with "Could not build this automatically." followed by the fix: apply the fix, or ship a Dockerfile. The recognised stacks use npm; commit package-lock.json, not pnpm or yarn lockfiles.

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) under Secrets in the Dockhold dashboard sidebar; they are injected at runtime. The set_app_secret MCP tool exists only over the remote HTTP path with a secrets-scoped dashboard token (see the MCP rules below); the browser sign-in the bridge uses never has it, so over the bridge, send the user to the dashboard.
- 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.
- Backups: paid accounts get the managed database backed up nightly (newest 7 kept, encrypted per app, stored in a separate location in Germany, locked 7 days). Free accounts get none. To restore, the user writes to [email protected] with the app and date; it is restored into a second database beside the live one. App storage (DATA_DIR) is NOT backed up.
- 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 under Secrets in the dashboard sidebar. 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?ref=docs → "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):
- Any MCP client can deploy and manage apps on Dockhold through the stdio bridge `npx -y dockhold mcp` (Node 18+). Claude Code: `/plugin marketplace add dockhold/mcp` then `/plugin install dockhold@dockhold`. VS Code: `code --add-mcp '{"name":"dockhold","command":"npx","args":["-y","dockhold","mcp"],"env":{"DOCKHOLD_REF":"vscode"}}'`. Cursor, Windsurf, any JSON-config client: {"mcpServers":{"dockhold":{"command":"npx","args":["-y","dockhold","mcp"],"env":{"DOCKHOLD_REF":"<client name>"}}}}. Codex: `codex mcp add dockhold --env DOCKHOLD_REF=codex -- npx -y dockhold mcp`. Then the user signs in once from a terminal with `npx dockhold login` (opens a browser; the same sign-in serves `npx dockhold deploy`; no token goes into any config file). Before sign-in every tool call returns "Not signed in to Dockhold. Run "npx dockhold login" in a terminal, then try again.": relay or run it, then retry without restarting the client. If the client times out on the first start while npx downloads the package: `npm i -g dockhold`, then command `dockhold`, args ["mcp"]. Full page: https://dockhold.eu/docs/recipes/deploy-from-your-ai-tool
- Tools. Read: list_apps, get_app_status, get_app_logs, list_github_repos, get_resource_usage. Read+deploy: deploy_app, redeploy_app, rebuild_app, set_app_variable, deploy_group, resize_app, resize_database, resize_database_storage, set_app_storage, remove_app_storage. The browser sign-in the bridge uses has the deploy scope only: no secrets tool exists on it, so when a secret is needed, send the user to Secrets in the dashboard sidebar.
- 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.
- 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.
- Remote HTTP path, only for a client that cannot start a command: server https://api.dockhold.eu/mcp, authenticated with a dashboard API token from https://app.dockhold.eu/settings/api-tokens (dh_mcp_ prefix; read-only or read+deploy, plus an additive secrets checkbox), passed as Authorization: Bearer. Only this path can carry the secrets scope; the browser sign-in never has it. With that scope the client also gets set_app_secret, bind_app_secret, unset_app_secret, list_app_secrets, list_secrets.
- Over the remote HTTP path with a secrets-scoped dashboard token 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.

See also