CLI reference
Every deplane command with its flags, a human example, and its --json output.
Run deplane help for a short list. Commands that act on an app use the app in ./deplane.toml, or the one you pass with --app NAME.
Global flags
These flags work with every command, anywhere on the command line.
| Flag | What it does |
|---|---|
--json | Print exactly one JSON document on stdout. Streams print one JSON object per line. Progress messages still go to stderr. |
--yes | Answer yes to every confirmation. |
--debug | Log each HTTP request and response to stderr. Tokens, cookies, Authorization headers, and secret values are replaced with [REDACTED]. |
--app NAME | The app to act on. Default: app in ./deplane.toml. |
--help | Show the command list. |
Without a terminal, the CLI never asks a question. A command that needs confirmation fails with confirmation_required and tells you to pass --yes.
Flags that take a value accept --flag VALUE or --flag=VALUE.
Environment variables
| Variable | What it does |
|---|---|
DEPLANE_TOKEN | Use this token instead of the stored login. |
DEPLANE_API_URL | The API to use. Default https://api.deplane.io. |
DEPLANE_ID_TOKEN | In a GitLab CI job: the ID token (id_tokens:, audience your API URL). Used when DEPLANE_TOKEN is not set, exchanged for a 15-minute pipeline token, never printed. |
BITBUCKET_STEP_OIDC_TOKEN | In a Bitbucket Pipelines step with oidc: true: used like DEPLANE_ID_TOKEN. |
DEPLANE_TEAM | In a pipeline job: the team's slug, when one repository is trusted by apps of several teams. |
HEROKU_API_KEY | The Heroku token for deplane import heroku when none is piped in. Sent only to Heroku. |
RENDER_API_KEY | The Render API key for deplane import render when none is piped in. Sent only to Render. |
RAILWAY_TOKEN | The Railway token for deplane import railway when none is piped in. Sent only to Railway. |
CLOUDFLARE_API_TOKEN | The Cloudflare API token for deplane domains switch when none is piped in. Sent only to Cloudflare. |
FLY_API_TOKEN | The Fly.io token for deplane import fly when none is piped in. Sent only to Fly.io. |
VERCEL_TOKEN | The Vercel token for deplane import vercel when none is piped in. Sent only to Vercel. VERCEL_TEAM_ID names the project's team when the argument does not. |
NETLIFY_AUTH_TOKEN | The Netlify token for deplane import netlify when none is piped in. Sent only to Netlify. |
CPANEL_API_TOKEN | The cPanel API token for deplane import cpanel when none is piped in. Sent only to cPanel. |
Exit codes
The exit codes are stable. Scripts and agents can rely on them.
| Code | Meaning |
|---|---|
| 0 | Success. |
| 1 | User error: a bad flag, a bad value, a missing app, a cancelled confirmation, a suspended app (app_suspended), and any other refusal. |
| 2 | Authentication or permission error: not logged in, login expired, a fresh passkey confirmation is needed, or your role or token may not do this. |
| 3 | The build failed. |
| 4 | The deploy failed: the new release did not become healthy. |
| 5 | Platform error: the API could not be reached, or something failed on our side. |
| 6 | Quota exceeded: every error code that starts with limit_ exits with 6. |
Errors
Every error says what happened and how to fix it. Without --json it goes to stderr:
$ deplane scale 9
Error: This needs 9 instances in total and your plan allows 3.
Fix: Use fewer instances, scale down another app, or email support@deplane.io to raise the limit.
With --json the error is the one JSON document on stdout:
{
"error": {
"code": "limit_instances",
"message": "This needs 9 instances in total and your plan allows 3.",
"fix": "Use fewer instances, scale down another app, or email support@deplane.io to raise the limit.",
"exit_code": 6
}
}
Every code is listed in Troubleshooting and error codes.
Account
deplane login
Log in through your browser with your passkey. The CLI shows a code, opens https://app.deplane.io/device, and waits until you approve. The token is stored in your OS keychain, or in a file only you can read if there is no keychain. It lasts 30 days. See Install for the full flow.
No flags besides the global ones. It refuses to run while DEPLANE_TOKEN is set.
$ deplane login
To log in, open https://app.deplane.io/device and type this code:
WDJB-MJHT
Only approve it if you just ran this command yourself. The code expires in 10 minutes.
Logged in as you@example.com (organization you-example).
{
"email": "you@example.com",
"logged_in": true,
"org": "you-example",
"stored_in": "keychain"
}
stored_in is keychain or file.
deplane logout
Revoke this computer's token and remove it.
$ deplane logout
Logged out. The token was revoked and removed from this computer.
{
"logged_out": true,
"was_logged_in": true
}
deplane whoami
Show who you are logged in as, your organization, and its plan limits.
$ deplane whoami
Logged in as you@example.com
Organization: You (you-example)
{
"entitlements": {
"memory_mb": 1024,
"apps": 1,
"instances": 2,
"sizes": ["small"],
"build_minutes_month": 500,
"concurrent_builds": 1,
"log_retention_days": 7,
"log_streams": 2,
"assistant_enabled": true,
"assistant_requests_month": 100,
"backup_retention_days": 1,
"disk_gb": 5,
"redis": 1,
"mysql": 0,
"clickhouse": 0,
"databases": 1,
"members": 1,
"custom_domains": 3,
"previews": false,
"always_on": false
},
"org": {
"id": "org_2b7k9m4x1q8w5e3r6t0y",
"name": "You",
"plan_id": "pln_free",
"plan_name": "Free",
"slug": "you-example"
},
"user": {
"email": "you@example.com",
"id": "usr_8c3v6b1n9m4k2j7h5g0f",
"is_admin": false
}
}
deplane version
Print the CLI version.
$ deplane version
deplane 0.1.0
{
"version": "0.1.0"
}
deplane doctor
Check that the API answers, that you are logged in, whether this folder is a project Deplane can build, and whether deplane.toml is valid. Each failed check has a fix. The exit code is the code of the first failed check, or 0.
$ deplane doctor
ok api The Deplane API at https://api.deplane.io answers.
FAIL login You are not logged in, or the login expired.
Fix: Run `deplane login`, or set DEPLANE_TOKEN.
ok project Detected Flask 3, Python 3.12, port 8000.
ok config deplane.toml is valid (app my-api).
{
"ok": false,
"checks": [
{"name": "api", "ok": true, "detail": "The Deplane API at https://api.deplane.io answers."},
{"name": "login", "ok": false, "detail": "You are not logged in, or the login expired.", "fix": "Run `deplane login`, or set DEPLANE_TOKEN."},
{"name": "project", "ok": true, "detail": "Detected Flask 3, Python 3.12, port 8000."},
{"name": "config", "ok": true, "detail": "deplane.toml is valid (app my-api)."}
]
}
Tokens
deplane tokens list
List your API tokens. kind is cli for logins, ci for tokens made with tokens create, and agent for tokens from agent login.
$ deplane tokens list
ID NAME KIND EXPIRES LAST USED
tok_5h2j8k1l4z7x9c3v6b0n macbook cli 2026-11-12 2026-10-13
tok_1q9w3e7r5t2y8u4i6o0p github-actions ci 2027-01-11 never
{
"tokens": [
{
"id": "tok_1q9w3e7r5t2y8u4i6o0p",
"name": "github-actions",
"kind": "ci",
"expires_at": "2027-01-11T09:00:00Z",
"last_used_at": null,
"created_at": "2026-10-13T09:00:00Z"
}
]
}
deplane tokens create
Create a CI token for CI, agents, and scripts: deplane tokens create NAME. The token is shown once. Store it as DEPLANE_TOKEN in your CI secrets.
Creating a token needs a fresh confirmation with your passkey from the last 5 minutes. In a terminal, the CLI opens your browser for it and then continues. CI tokens cannot create tokens.
| Flag | What it does |
|---|---|
--name NAME | The token's name, instead of the positional argument. |
--expires-in-days N | Lifetime from 1 to 365 days. Default 90. |
$ deplane tokens create github-actions --expires-in-days 90
Token "github-actions" (tok_1q9w3e7r5t2y8u4i6o0p) created; it expires on 2027-01-11. It is shown only once: store it in your CI secrets as DEPLANE_TOKEN.
dpl_4Fh7Kq2Lm9Np3Rs6Tv8Wx1Yz5Ab0Cd
The message goes to stderr and only the token goes to stdout, so deplane tokens create ci > token.txt saves just the token.
{
"token": "dpl_4Fh7Kq2Lm9Np3Rs6Tv8Wx1Yz5Ab0Cd",
"id": "tok_1q9w3e7r5t2y8u4i6o0p",
"name": "github-actions",
"kind": "ci",
"expires_at": "2027-01-11T09:00:00Z"
}
deplane tokens revoke
Revoke a token at once: deplane tokens revoke ID. Asks for confirmation; pass --yes to skip it.
$ deplane tokens revoke tok_1q9w3e7r5t2y8u4i6o0p --yes
Revoked tok_1q9w3e7r5t2y8u4i6o0p.
{
"revoked": "tok_1q9w3e7r5t2y8u4i6o0p"
}
deplane agent login
Get a scoped token for an AI agent: deplane agent login --role ROLE. You approve it in your browser, which shows the role, the apps and how long it lasts. The token is printed once, for the agent's DEPLANE_TOKEN, and never saved. Roles and limits: Agent tokens.
| Flag | What it does |
|---|---|
--role ROLE | viewer, deployer or operator. Required. |
--app A,B | Limit the token to these apps. Default: all the team's apps. |
--name NAME | The agent's name, shown on the approval page, in the email and in the activity log. Default agent. |
--ttl D | Lifetime: 12h, 7d, up to 30d. Default 7 days. |
$ deplane agent login --role deployer --app web --name claude-code
Approved: a deployer token for dev@example.com (organization acme). It is shown only once; use it as DEPLANE_TOKEN. Revoke it with `deplane tokens revoke`.
dpl_4Fh7Kq2Lm9Np3Rs6Tv8Wx1Yz5Ab0Cd
{
"token": "dpl_4Fh7Kq2Lm9Np3Rs6Tv8Wx1Yz5Ab0Cd",
"role": "deployer",
"email": "dev@example.com",
"org": "acme"
}
Apps and deploys
deplane launch
Detect how to build this folder, create the app, write deplane.toml, build, deploy, and print the URL. See the Quickstart for what it detects.
| Flag | What it does |
|---|---|
--name NAME | The new app's name. Default: the folder name, with a random suffix if that name is taken. |
--app NAME | Deploy this folder to an app you already have (the same as deplane deploy --app NAME). |
--repo OWNER/REPO[@REF] | Deploy a public GitHub repository that has a deplane.template.json, instead of the folder. See below. |
--image REF | Deploy a public Docker image instead of the folder. See below. |
--port PORT | With --image: the port the image listens on. It goes with the build and becomes the app's port when that build is released, so an image that is refused or fails to build leaves the port as it was; once released, the port stays even if that release fails its health check or is rolled back. Default 8080. |
deplane launch --image REF [--name APP] [--port PORT] creates the app from a public Docker image, with no source code. REF is [registry/]name[:tag][@sha256:DIGEST], for example nginxinc/nginx-unprivileged:1.27; the app's name defaults to the image's name. Deplane builds the image into a release like any deploy, so the release command runs too. Only public images work, and Deplane runs them as user 10001, not root; see Deploy a Docker image. No deplane.toml is written, because an image has no folder: pass --app NAME to later commands. To update the app to a new image, run deplane deploy --image REF --app NAME. --image cannot be combined with a folder, a path or --template; it fails with image_with_source. A bad reference fails with invalid_image, and a private registry with private_registry_unsupported.
$ deplane launch --image nginxinc/nginx-unprivileged:1.27 --port 8080
Created app nginx-unprivileged.
Build bld_7c3x9k2m4q8w1e5r6t0y queued for nginxinc/nginx-unprivileged:1.27.
...
Deployed nginx-unprivileged v1: https://nginx-unprivileged.deplane.app
deplane launch --template NAME [--name APP] deploys a template instead of the folder: Deplane creates the app (and a database and disk if the template needs them), sets its secrets, builds it from Deplane's own copy of the template and rolls it out, while the command shows each step. It ends with the app's URL and what to do next, for example "Open the URL and create the first admin account". If a step fails, everything made so far is kept and the command exits with code 4 and tells you how to retry or delete it. --no-wait returns at once. See Templates.
deplane launch --repo OWNER/REPO[@REF] [--name APP] deploys a public GitHub repository that carries a deplane.template.json at its root, pinned to a commit: the command first prints what the file asks for (the app, its database, Redis, MySQL, ClickHouse database or disk, the variables it sets, its warning) and a reminder that Deplane has not reviewed the repository, then deploys and follows the steps exactly like --template. REF is a branch, a tag or a commit (default: the default branch); OWNER/REPO may also be written as github.com/owner/repo or the repository's URL. It cannot be combined with --template or --image. See Templates.
Run again in the same folder, launch deploys that folder's app again. It never deploys a folder over a different existing app: --name with the name of an app you already have stops with an error, and a folder whose name matches an existing app gets a new app with a suffix.
$ deplane launch
Detected Next.js 15, Node 22, port 3000
Created app my-shop.
Wrote deplane.toml.
Uploading 214 files (1.3 MB)...
Build bld_4m8q2k7x9c1v5b3n6z0a queued.
Building...
...
Built 4f1c2a9e8b7d. Rolling out release v1...
Running: 1 of 1 instances ready.
Deployed my-shop v1: https://my-shop.deplane.app
With --json, progress and the build log still go to stderr, and stdout gets one document:
{
"app": "my-shop",
"build": "bld_4m8q2k7x9c1v5b3n6z0a",
"created": true,
"image_digest": "sha256:4f1c2a9e8b7d...",
"release": 1,
"status": "running",
"url": "https://my-shop.deplane.app"
}
A failed build exits with 3. A release that does not become healthy exits with 4.
deplane deploy
Build and deploy this folder's app again. It applies deplane.toml first. It does not create apps: without a deplane.toml and without --app, it fails with no_app.
| Flag | What it does |
|---|---|
--app NAME | Deploy this folder to this app. |
--image REF | Deploy a public Docker image to the app instead of the folder, for example deplane deploy --image ghcr.io/acme/web:2.4 --app web. The image replaces the app's current release when it is healthy. |
--port PORT | With --image: the port the image listens on; it becomes the app's port when the build is released. With a deplane.toml, its other [service] settings (health check, release command) are still applied before the build. |
deplane.toml's release_command is sent with the other [service] settings, and a deploy that has a release command prints "Running the release command..." while it runs, waits up to 15 minutes more, and on failure prints the command's output and exits with code 4. --image takes no folder or path: see deplane launch for the image rules.
$ deplane deploy
Detected Next.js 15, Node 22, port 3000
Uploading 214 files (1.3 MB)...
Build bld_9t2w6r1y8u3i5o7p4a0s queued.
Building...
...
Built 0b9e7d61a2c3. Rolling out release v2...
Running: 1 of 1 instances ready.
Deployed my-shop v2: https://my-shop.deplane.app
{
"app": "my-shop",
"build": "bld_9t2w6r1y8u3i5o7p4a0s",
"created": false,
"image_digest": "sha256:0b9e7d61a2c3...",
"release": 2,
"status": "running",
"url": "https://my-shop.deplane.app"
}
deplane redeploy
Build the app's last source again (an upload or a GitHub commit) and release it, for example after changing a build variable with deplane secrets set --build. It counts toward the 10 deploys per hour.
$ deplane redeploy
Rebuilding shop from its last source. Build bld_… queued.
Deployed shop v8: https://shop.deplane.app
deplane apps
List your organization's apps. An autoscaled app shows its range in INSTANCES (for example 1-5), and an app that is asleep shows sleeping in STATUS.
$ deplane apps
NAME STATUS RELEASE INSTANCES SIZE URL
my-shop active v2 1 small https://my-shop.deplane.app
my-api active v5 2 small https://my-api.deplane.app
{
"apps": [
{
"id": "app_7k2m9x4q1w8e5r3t6y0u",
"name": "my-shop",
"region": "fra",
"status": "active",
"url": "https://my-shop.deplane.app",
"current_release": 2,
"instances": 1,
"size": "small",
"port": 3000,
"health_check": {"type": "tcp"},
"created_at": "2026-10-13T09:12:00Z"
}
]
}
status is active or suspended. health_check is {"type": "tcp"} or {"type": "http", "path": "/healthz"}.
deplane apps destroy
Destroy any app by name: deplane apps destroy NAME. Its instances, releases, and secrets are deleted, and its disk if it has one (the question says so), and the name becomes available to others. Asks for confirmation; pass --yes to skip it. It also needs a confirmation in your browser: the CLI prints a link and a code (and opens the link), you approve with your passkey, and the command continues. --yes does not skip this, and CI tokens cannot do it at all.
$ deplane apps destroy my-old-app --yes
Destroyed my-old-app.
{
"destroyed": "my-old-app"
}
deplane destroy
Destroy this folder's app, or the one named by --app. Same as deplane apps destroy, including the browser confirmation.
$ deplane destroy --app my-shop --yes
Destroyed my-shop.
{
"destroyed": "my-shop"
}
deplane status
Show the app's URL, release, instances, health, the last build, and the last error with its fix.
$ deplane status
App: my-shop (active)
URL: https://my-shop.deplane.app
Release: v3
Instances: 0 of 1 ready, size small
State: Failed
Problem: HealthCheckFailed: The app started but did not pass its health check on port 3000. Make sure it listens on $PORT.
Fix: Make the app listen on 0.0.0.0 at the port in $PORT, and make health_check (if set) answer 200. Run `deplane logs` to see why it failed.
Last build: bld_9t2w6r1y8u3i5o7p4a0s, succeeded
State is Pending (no release yet), Deploying, Running, or Failed.
{
"app": "my-shop",
"url": "https://my-shop.deplane.app",
"status": "active",
"release": 2,
"phase": "Running",
"reason": "",
"message": "1 of 1 instances ready.",
"fix": "",
"instances": 1,
"ready_instances": 1,
"size": "small",
"last_build": {
"id": "bld_9t2w6r1y8u3i5o7p4a0s",
"status": "succeeded",
"builder": "railpack",
"position": 0,
"image_digest": "sha256:0b9e7d61a2c3...",
"release": 2,
"created_at": "2026-10-13T09:40:00Z",
"finished_at": "2026-10-13T09:41:10Z",
"seconds": 70,
"failure": null
}
}
reason is empty, or one of Unschedulable, ImagePullFailed, HealthCheckFailed, Crashing, Timeout, and ReleaseCommandFailed. last_build is null before the first build. A failed build has failure set to {"code", "message", "fix"}.
deplane scale
Change the number of instances, the size, autoscaling, or when the app sleeps: deplane scale [N] [--size small|medium|large] [--max M] [--sleep 5m|15m|60m|off]. deplane scale 0 stops the app without deleting it. The change starts at once; it does not create a release.
| Flag | What it does |
|---|---|
--size SIZE | small, medium, or large. See Limits. |
--max M | Autoscale between N and M instances. --max 0 turns autoscaling off. |
--sleep TIME | Put the app to sleep after 5m, 15m, or 60m without requests; the next request wakes it. off turns sleeping off. On Free, only 5m or 15m (limit_sleep). |
deplane status shows the autoscale range, and Sleeping since HH:MM UTC while the app sleeps. The JSON for scale, apps, and status includes max_instances (null when off), sleep_after_minutes, sleep_state, and slept_at.
$ deplane scale 2 --size medium
my-shop now runs 2 medium instance(s).
Autoscale between 1 and 5 instances and sleep after 15 idle minutes (see Scaling):
$ deplane scale 1 --max 5 --sleep 15m
The JSON is the app, as in deplane apps:
{
"id": "app_7k2m9x4q1w8e5r3t6y0u",
"name": "my-shop",
"region": "fra",
"status": "active",
"url": "https://my-shop.deplane.app",
"current_release": 2,
"instances": 2,
"size": "medium",
"port": 3000,
"health_check": {"type": "tcp"},
"created_at": "2026-10-13T09:12:00Z"
}
deplane releases
List the app's releases, newest first (up to 50). A release is a build plus its settings and secret names. STATUS is pending, active, superseded (it was live and a newer release took over), replaced (a newer release took over before it went live), or failed.
$ deplane releases
VERSION STATUS IMAGE CREATED DESCRIPTION
v3 failed 7a1b2c3d4e5f 2026-10-13 10:02 Deploy (HealthCheckFailed)
v2 active 0b9e7d61a2c3 2026-10-13 09:41 Deploy
v1 superseded 4f1c2a9e8b7d 2026-10-13 09:12 Deploy
{
"releases": [
{
"version": 2,
"status": "active",
"image_digest": "sha256:0b9e7d61a2c3...",
"description": "Deploy",
"failure_reason": "",
"created_at": "2026-10-13T09:41:00Z"
}
]
}
When any release has a release command, a RELEASE COMMAND column shows its state: pending, running, succeeded, failed, skipped, or - for none. With --json, each release has release_command_status and, when it failed, release_command_output (its last lines).
Descriptions are Deploy, Set secrets NAMES, Unset secrets NAMES, Rollback to vN, or Automatic rollback to vN: X% of requests failed after vM (see Automatic rollback). In --json, automatic_rollback is true for a release Deplane made by rolling back automatically, and released_at is when a release went live.
deplane rollback
Roll back to the previous release that ran successfully, or to a version: deplane rollback [VERSION]. Both 3 and v3 work. A rollback creates a new release from the earlier release's build and settings. Asks for confirmation; pass --yes to skip it.
$ deplane rollback 1 --yes
Created release v4 (Rollback to v1). Run `deplane status` to follow the rollout.
{
"version": 4,
"status": "pending",
"image_digest": "sha256:4f1c2a9e8b7d...",
"description": "Rollback to v1",
"failure_reason": "",
"created_at": "2026-10-13T10:05:00Z"
}
Automatic rollback
When a new release starts failing right after it goes live, Deplane rolls it back for you. For the first 10 minutes after a release goes live, Deplane checks the errors your app returns every minute and rolls back to the previous good release when all of these hold:
- at least 50 requests and at least 10 errors from the app over at least two minutes of its own traffic (the minute it went live and the next one are left out: they can still hold the previous release's requests);
- more of them failed than your team's error-rate threshold (5% unless you changed it on Alerts);
- the rate is at least 3 times the previous release's over its last 30 minutes (or the previous release had no errors);
- the errors reached visitors on at least 3 different networks (a /24 for IPv4, a /48 for IPv6).
Only 5xx responses your app returned count. 502, 503 and 504 do not: Deplane's router sends those itself when no instance answers in time, which a flood of requests or a rollout can cause. Requiring several networks means one visitor who keeps opening a page that fails cannot roll your app back on their own. To count networks, Deplane keeps a keyed hash of each network that got an error, never the address, for one day. The errors alert still counts every 5xx.
The rollback is a new release, exactly like deplane rollback, described as Automatic rollback to v1: 42% of requests failed after v2. Your team's alert channels get "Rolled back my-shop to v1", and it shows on Alerts. If the GitHub commit's status said success, it becomes a failure. Deplane never rolls back the same release twice, and never rolls back an automatic rollback; deplane rollback without a version skips a release that was rolled back automatically. If the rollback's own release does not start, the release before it keeps serving and your team gets "Automatic rollback of my-shop failed: still on v2" with the reason. It is on for every app; turn it off with deplane config --auto-rollback off or in the app's Settings tab.
deplane config
Show the app's port, health check, automatic rollback, release command and private network. Three settings can be changed here, and they apply right away (the release command from the next deploy). The port and health check come from deplane.toml and apply from the next deploy.
| Flag | What it does |
|---|---|
--auto-rollback on or off | Turn automatic rollback on or off. |
--release-command "CMD" | Set the release command that runs before each new build serves. --release-command "" removes it. It is one line, and it takes effect with the next deploy. |
--private-network on or off | Let the team's other apps reach this app at http://APP: see Private networking. It cannot be on while the app sleeps (private_network_sleep), and a pull request preview cannot use it. |
The flags can be combined in one command. Another value than on or off fails with invalid_auto_rollback or invalid_private_network.
$ deplane config
Port 8080
Health check HTTP /healthz
Auto rollback on
Release command python manage.py migrate
Private network on, at http://my-shop
$ deplane config --auto-rollback off
Automatic rollback is off for my-shop. Roll back by hand with `deplane rollback`.
$ deplane config --release-command "npm run migrate"
Release command for my-shop: npm run migrate
It runs before each new build's release starts serving, beginning with your next deploy.
$ deplane config --private-network on
Private network is on for my-shop: your team's other apps reach it at http://my-shop.
With --json, all of them print the app, including "auto_rollback": false, "release_command": "npm run migrate", "private_network": true, and "private_url": "http://my-shop" (empty when the private network is off).
deplane open
Open the app's URL in your browser and print it. Without a terminal it only prints it.
$ deplane open
https://my-shop.deplane.app
{
"url": "https://my-shop.deplane.app"
}
deplane logs
Show the app's recent log lines, or follow them live.
| Flag | What it does |
|---|---|
-f, --follow | Keep streaming new lines until you press Ctrl-C. |
--tail N | Lines per instance, from 0 to 1000. Default 100. |
--process NAME | Only the lines of one process: web, a worker or cron job's name, or release for the release command. With a run's ID (run_...), only that run's output. |
$ deplane logs --tail 2
x7k2p | ready - started server on 0.0.0.0:3000
x7k2p | GET / 200 in 12ms
Each line starts with the short name of the instance that wrote it. With --json, each line is its own JSON object:
{"instance": "x7k2p", "line": "ready - started server on 0.0.0.0:3000"}
{"instance": "x7k2p", "line": "GET / 200 in 12ms"}
Your organization can follow at most 3 live streams at once; see limit_log_streams.
deplane run
Run a command once in the app's live release and show its output: deplane run [--app NAME] [--size SIZE] [--no-wait] -- COMMAND ARGUMENTS.
| Flag | What it does |
|---|---|
--size SIZE | The size to run at: small (default), medium or large, up to your plan's largest. |
--no-wait | Start the run and return at once, printing its ID. |
Everything after -- is the command, and each word reaches it as you typed it: spaces, quotes, $ and ; are kept, not interpreted. To use a pipe, && or variables, run a shell yourself: deplane run -- sh -c "cmd1 && cmd2".
The command runs in the release that was live when the run started, with the app's variables and database access, and no disk. It does not take traffic, and it has no input and no terminal: an interactive shell is not available. Its output goes to stdout, and the progress to stderr. The exit code of deplane run is the command's. A run that ended without one, because it was cancelled, ran past its hour, or found no room to start, exits with 1 and says why. When the team's slots are busy, a run waits up to an hour for a free one before it starts. Only the first 10 MB of output is shown; past that, deplane run says so and waits for the command to finish. Press Ctrl-C to cancel the run; it exits with 130.
$ deplane run -- python manage.py seed
Started run run_4m8q2k7x9c1v5b3n6z0a in my-shop (release v3).
seeded 3 rows
$ deplane run --size medium -- sh -c "python backfill.py && echo done"
$ echo $?
0
$ deplane run --no-wait -- node cleanup.js
Started run run_9t2w6r1y8u3i5o7p4a0s in my-shop (release v3).
See its output while it runs with `deplane logs -f --process run_9t2w6r1y8u3i5o7p4a0s --app my-shop`.
With --json, the output goes to stderr and stdout gets one document, the finished run (with --no-wait, the started one):
{
"id": "run_9t2w6r1y8u3i5o7p4a0s",
"app": "my-shop",
"release": 3,
"command": "python manage.py seed",
"size": "small",
"status": "succeeded",
"exit_code": 0,
"message": ""
}
status is pending, running, succeeded, failed or cancelled. The output is shown while the run exists and for an hour after it ends; it is not archived. A run lasts at most an hour. An app can have 2 runs at once and a team 5, a team can start 30 an hour, and a team's runs and release commands share 2 slots at once. The refusals are run_busy, run_limit, run_rate_limited, run_no_release and run_on_preview. You and CI tokens can run commands; AI tools cannot.
Secrets
See Secrets for how secrets work.
deplane secrets set
Set one or more secrets: deplane secrets set NAME=VALUE [NAME=VALUE...]. Changing a runtime secret on an app that has a release creates a new release.
| Flag | What it does |
|---|---|
--build | Set build secrets, used only while building. |
$ deplane secrets set DATABASE_URL=postgres://u:p@db.example.com/shop API_KEY=sk_live_123
Set runtime secret(s) API_KEY, DATABASE_URL.
Created release v5 with the new secrets.
{
"names": ["API_KEY", "DATABASE_URL"],
"scope": "runtime",
"release": 5
}
release is null when no release was created: for build secrets, and for apps that have not been deployed yet.
deplane secrets unset
Remove secrets: deplane secrets unset NAME [NAME...]. Pass --build to remove build secrets.
$ deplane secrets unset API_KEY
Removed runtime secret(s) API_KEY.
Created release v6 without them.
{
"names": ["API_KEY"],
"scope": "runtime",
"release": 6
}
deplane secrets list
List secret names and when they changed. Values are never shown.
$ deplane secrets list
NAME SCOPE UPDATED
NPM_TOKEN build 2026-10-13 09:30
DATABASE_URL runtime 2026-10-13 09:50
{
"secrets": [
{"name": "DATABASE_URL", "scope": "runtime", "updated_at": "2026-10-13T09:50:00Z"},
{"name": "NPM_TOKEN", "scope": "build", "updated_at": "2026-10-13T09:30:00Z"}
]
}
GitHub
Connect a repository in the dashboard (an app's Settings, or Import from GitHub on New app). See GitHub.
deplane github
Show the app's connected repository, its branch and root folder, whether pull requests get previews (and their own databases), the open previews with their databases, and the last problem deploying from GitHub, if any.
$ deplane github
Repository: acme/shop
Branch: main (every push deploys)
Root: /
Previews: on, for pull requests from the repository
#12 Faster checkout https://shop-pr-12.deplane.app Running
deplane github deploy
Deploy the latest commit of the connected branch now, without a push, and follow it like deplane deploy.
deplane previews
List the app's live pull request previews and the pull requests waiting for one, with why each waits. See Choose which pull requests get previews.
$ deplane previews
Previews (3 of 3):
#12 Faster checkout https://shop-pr-12.deplane.app Running
#15 New footer https://shop-pr-15.deplane.app Running
#18 Fix tax https://shop-pr-18.deplane.app Running
Waiting for a preview:
#21 Dark mode (the app's previews were full)
The app has 3 previews: build one with `deplane previews build PR --replace PR`.
With --json: {"previews": [...], "waiting": [{"pull_request", "title", "author", "branch", "sha", "reason", "refused_at"}], "max_previews": 3}. reason is slots_full, plan_apps, plan_resources, capacity_full, replaced or removed.
deplane previews build
Build a waiting pull request's preview from its latest commit: deplane previews build 21. When the app already has 3 previews, add --replace PR to delete one first, in the same step: deplane previews build 21 --replace 12. Replacing asks for confirmation (--yes without a terminal), and says when the replaced preview's database is deleted with it. If the new preview is still refused, nothing changes. Errors: preview_slots_full, preview_not_waiting, preview_not_found, previews_off.
{"pull_request": 21, "replaced": 12, "preview": {"name": "shop-pr-21", "url": "https://shop-pr-21.deplane.app"}}
deplane previews remove
Delete a pull request's preview: deplane previews remove 12. The pull request waits for a preview again. Asks for confirmation (--yes without a terminal). With --json: {"removed": 12}.
In a pipeline's stop job (with DEPLANE_ID_TOKEN or BITBUCKET_STEP_OIDC_TOKEN) it removes the preview of that merge or pull request without asking; a preview already gone is not an error.
GitLab
An app can be connected to one GitLab project with your own fine-grained personal access token, in the dashboard (the app's Settings, GitLab): every push to its branch deploys, and merge requests get previews. The token is sealed, renewed every 7 days, and revoked when you disconnect.
deplane gitlab
Show the app's connected GitLab project, its branch, the token's account and expiry, whether the webhook is set up, and the merge request previews. With --json, the API's answer.
deplane gitlab deploy
Deploy the head of the connected branch now and follow the build.
Bitbucket
An app can be connected to one Bitbucket repository with a repository access token with only Repositories: Read and Pull requests: Read, in the dashboard (the app's Settings, Bitbucket). Deplane makes the webhook's secret and shows it once. Bitbucket cannot revoke a token from outside: when you disconnect, delete the token and the webhook on Bitbucket.
deplane bitbucket
Show the app's connected Bitbucket repository, its branch, whether the webhook is set up, and the pull request previews. With --json, the API's answer.
deplane bitbucket deploy
Deploy the head of the connected branch now and follow the build.
GitLab CI and Bitbucket Pipelines
A job deploys with its CI host's signed ID token: set DEPLANE_ID_TOKEN (GitLab) or use BITBUCKET_STEP_OIDC_TOKEN (Bitbucket), leave DEPLANE_TOKEN unset, and run deplane deploy --app NAME [--path DIR]. From the trust's deploy branch it deploys the app; from other branches deplane deploy --preview N deploys the preview of merge or pull request N. A pipeline only uploads: it never creates the app or changes its settings.
deplane pipelines trust
Trust a repository's jobs to deploy the app: deplane pipelines trust gitlab --app web --project 123456 --branch main [--path group/project] [--no-previews], or deplane pipelines trust bitbucket --app web --workspace '{uuid}' --repository '{uuid}' --name workspace/repo --branch main [--no-previews | --preview-database]. The repository is named by its IDs, never its path. Previews from other branches are on unless --no-previews; --preview-database gives each its own empty database, as GitHub previews can. At most 3 per app. Needs your passkey.
deplane pipelines list
List the app's pipeline trusts with their ID, repository, branch, previews and last use.
deplane pipelines remove
Remove a trust: deplane pipelines remove plt_.... Its pipelines stop deploying at once. Asks for confirmation (--yes without a terminal).
Assistant
deplane ask
Ask the assistant a question about your apps. In a folder with a deplane.toml, or with --app, that app is the one it may change; without one it only reads and explains. It makes ordinary changes itself; before one that takes something away it asks Run this? [y/N] (a delete: type the name), and --yes never skips that. Without a terminal it changes nothing and exits 1 if the answer needs a change, listing the commands. It never sees variable values or commands. See Assistant.
$ deplane ask "why did my last deploy fail?"
The build failed in `npm install`: package.json asks for react@19.4.0, which does not exist yet.
Change it to ^19.2.0 and deploy again.
Alerts
Rules and channels (Slack, Discord, webhooks; email is not delivered yet) are set in the dashboard. See Alerts.
deplane alerts
List your team's firing alerts, then recent ones.
$ deplane alerts
1 firing:
STATE ALERT STARTED
FIRING shop is down: 0 of 2 instances ready: CrashLoopBackOff Oct 14 12:03
resolved shop is slow: p95 response time above 2000 ms for 5 minutes Oct 13 18:40
Usage
deplane usage
Show what your team used: requests, bandwidth, CPU, memory, build minutes, database disk and app disks, then each app, database and disk. Pick the period with --range: 24h, 7d, 30d, month (the default, since the 1st) or prev_month. Dates are in UTC. Build minutes are always this month's, counted against your plan. Deleted apps still show what they used, marked with *. Databases and disks have no requests, bandwidth or builds, so those columns show –; disks have no CPU or memory either. DISK GB-H is the storage a database or disk held. Add --json for the full data, including per-day or per-hour series.
$ deplane usage
Usage this month (Oct 1 – Oct 18)
Requests 12,345
Bandwidth out 1.2 GB
CPU 3.4 hours
Memory 120.5 GB-hours reserved (31.0 used)
Build minutes 12.5 of 300
Database disk 4,200 GB-hours
App disks 120 GB-hours
APP REQUESTS BANDWIDTH CPU H MEMORY GB-H BUILD MIN DISK GB-H
shop 12,000 1.1 GB 3.0 100.0 10.0 –
old-api* 345 100.0 MB 0.4 20.5 2.5 –
shopdb – – 0.2 4.0 – 4200.0
shop disk – – – – – 120.0
* deleted
Custom domains
Serve an app at your own domain, with HTTPS. Deplane routes a domain only after DNS proves it points at the app, then gets its certificate automatically.
deplane domains
List the app's custom domains: waiting for DNS, issuing certificate, or active.
$ deplane domains
DOMAIN STATE
www.example.com active
example.com waiting for DNS
deplane domains add
Connect a domain and show the DNS records to set. For a subdomain such as www.example.com, add a CNAME to <app>.deplane.app. A root domain such as example.com cannot have a CNAME: add an A record to the address shown instead. Either way, add the TXT record _deplane-challenge.<domain> with the value shown, which proves the domain is yours. An app can have up to 10 domains.
$ deplane domains add www.example.com
www.example.com is waiting for DNS. No DNS record for www.example.com yet. Add a CNAME record pointing to shop.deplane.app and a TXT record _deplane-challenge.www.example.com with the value dpl-… (DNS changes can take a few minutes).
Set these records at your DNS provider (the TXT record proves the domain is yours):
CNAME www.example.com shop.deplane.app
TXT _deplane-challenge.www.example.com dpl-…
deplane domains check
Check a domain's DNS and certificate now. Deplane also checks every 2 minutes for 48 hours after you add it.
deplane domains remove
Disconnect a domain. It stops serving the app right away. Asks for confirmation; pass --yes in scripts.
deplane domains switch
Set a domain's DNS records on Cloudflare for you: deplane domains switch DOMAIN [--provider cloudflare] [--check] [--yes] [--no-wait] [--rollback FILE]. It points the domain at the app (a CNAME, or A records to Deplane where a CNAME cannot go) and adds the TXT record that proves it is yours, adding the domain to the app first if needed. --check shows the records now and each change, with conflicting and proxied records flagged, and changes nothing. Without it, after a confirmation, it saves the current records to deplane-dns-DOMAIN-DATE.json, adds the TXT record and waits until public DNS shows it, changes the records at the domain all together (Cloudflare's batch API), and waits until Deplane verifies the domain; --no-wait skips the waits. --rollback FILE puts the records back exactly. MX records and every other name in the zone are never changed. See Switch DNS on Cloudflare.
The Cloudflare API token (Edit zone DNS, for this zone only) is read from stdin or CLOUDFLARE_API_TOKEN, never a flag. The CLI makes the Cloudflare calls itself: the token is never sent to Deplane, printed or written, and --debug shows Cloudflare requests as method, path and status only.
$ export CLOUDFLARE_API_TOKEN=...
$ deplane domains switch www.example.com --check
$ deplane domains switch www.example.com --yes
Switched www.example.com on Cloudflare (3 changes). To undo: deplane domains switch www.example.com --rollback deplane-dns-www.example.com-20261031-140501.json
Forms
Forms marked data-auto-form in your HTML, or listed under [[forms]] in deplane.toml. See Forms. AI tools can only list forms (the list_forms tool): submissions are your visitors' personal data (forms_private). Members read and export; only owners delete submissions or change notifications and CAPTCHA (owners_only).
deplane forms
List the app's forms: their submission and spam counts, CAPTCHA, the deployed version's fields, and the path each posts to.
$ deplane forms
FORM SUBMISSIONS SPAM CAPTCHA FIELDS POSTS TO STATE
contact 12 3 off email, message /__auto_forms/3f9c2a1b7d4e5f60a1b2 active
deplane forms submissions
Show a form's newest 100 submissions, or its spam with --status spam: deplane forms submissions FORM [--status spam].
deplane forms export
Write every submission that is not spam, with every field any version of the form had, as CSV on stdout: deplane forms export FORM > FILE.csv.
deplane forms delete
Delete one submission, or with no ID all of a form's, spam included: deplane forms delete FORM [ID] [--yes]. It cannot be undone.
deplane forms notify
Show or set where a form's submissions are announced: deplane forms notify FORM [--email A,B|off] [--slack -|off] [--webhook -|off]. Email goes only to team members, as digests; Slack and webhooks get each submission. URLs are read from stdin (-), never taken as arguments. A new webhook's signing secret is shown once.
$ pbpaste | deplane forms notify contact --slack -
Email digests: you@example.com
Slack: on
Webhook: off
deplane forms captcha
Turn Cloudflare Turnstile on or off for a form: deplane forms captcha FORM on|off [--secret -|off]. Without --secret, the form keeps the key it has; --secret - reads your own Turnstile secret from stdin (needed on a custom domain); --secret off goes back to the platform's key, which works on the app's own address only.
Databases
Managed Postgres (17, or 18 where Deplane offers it) next to your apps. Each database runs in its own microVM on a 10 GB disk that only it uses, reachable only by your team's apps. Attaching it to an app sets the app's DATABASE_URL, a write-only variable, and rolls out a new release. A database counts against your plan as one instance of its size.
deplane db
List your team's databases, their Postgres version, their state, whether each has a live copy on another server, and the apps they are attached to. COPY is live (the copy follows the database), none (the copy is down because its server is; it comes back when the server returns) or – (no copy to show, as for a pull request preview). See Live copy and failover.
deplane db create
Create a database. It is ready in about a minute. --size is small (default), medium or large. --version is its Postgres major version: 17 (the default) or 18 where Deplane offers it (otherwise refused with postgres_version_unavailable). A database keeps its version for its whole life: there is no in-place upgrade (see Postgres versions).
$ deplane db create main
$ deplane db create next --version 18
$ deplane db attach main --app shop
deplane db attach
Give an app the database as DATABASE_URL (postgres://app:…@pg--main:5432/app). An app uses one database at a time.
deplane db detach
Remove the app's DATABASE_URL. The database and its data stay.
deplane db destroy
Delete a database and all its data, after detaching it from its apps. Asks for confirmation; pass --yes in scripts. It also needs a confirmation in your browser: the CLI prints a link and a code (and opens the link), you approve with your passkey, and the command continues. --yes does not skip this, and CI tokens cannot do it at all. Its backups are kept for your plan's backup window; restore it with deplane db restore NAME --deleted --at latest.
deplane db backups
Show a database's backup status (Protected, Behind, Starting or Off), the window it can be restored to (your local time and UTC; gaps show as more than one window), the last full copy, the space used and recent restores. --deleted shows a deleted database's backups.
deplane db restore
Restore a database into a new database; the original is never changed. --at is latest, a time such as 2026-10-15 16:31 (your local time) or 2026-10-15T14:31:20Z, or a time ago: -10m, 2h, 1d. The new name is --to, or <name>-restored-<HHMM>. --deleted restores a deleted database. It waits up to 15 minutes, showing progress (--no-wait returns at once), then prints the deplane db switch command to run.
Restoring and switching need a confirmation in your browser with your passkey: the CLI prints a link and a code, waits for you to approve, and continues. Each restore or switch uses up its confirmation, so the next one asks again. CI tokens and AI tools cannot restore or switch databases.
$ deplane db restore main --at -10m Restoring from main as of 2026-10-15 14:42:31 UTC. main-restored-1442 is ready. Check it, then move your apps to it: deplane db switch main-restored-1442 --from main
deplane db switch
Give every app attached to --from this database as DATABASE_URL instead (a new release each, no downtime). The original database is kept until you delete it.
deplane db import
Copy a Postgres database from anywhere into a new Deplane database. The source is only read; nothing there is changed except, in live mode, a publication and a replication slot that Deplane removes when the import ends.
The source's URL is asked for at a hidden prompt, or read from stdin when no terminal is attached. It is never taken from an argument (it would land in your shell history), and deplane db import postgres://... is refused. The URL is never printed, and --debug hides it.
The command checks the source, prints what it found (size, tables, extensions, warnings, and whether copy and live are possible, with reasons), then asks for a name and a mode, starts, and follows the import. --name, --size, --mode and --exclude-schema answer those questions; --version sets the new database's Postgres version (left out: 17, or the source's own major when it is newer, so a Postgres 18 source goes into Postgres 18; asking for an older major than the source's is refused with source_newer_than_target); --yes skips the confirmation; --no-wait returns right after the start.
- copy copies the data once. It ends with the new database ready.
- live copies the data, then keeps the new database following the source until you stop writes to the source and run
deplane db import finish. Nothing written in the meantime is lost.
Without --mode, a terminal is asked (live is the default when it is possible); a script gets live when it is possible, otherwise copy, and the CLI says which. Without a terminal, --name and --yes are required. --exclude-schema a,b leaves out schemas by name, replacing the check's default list; --exclude-schema "" leaves nothing out. A live import stops following at "Syncing" and returns; --follow keeps watching it.
An import is for people: CI tokens and AI tools cannot run it. A team has one import at a time. The source can be at most 5 GB.
$ printf '%s' "$SOURCE_URL" | deplane db import --name shop --mode live --yes
Checking db.example.com:5432/shop ...
Checked db.example.com:5432/shop
Postgres 16.4, 953 MB, 12 tables
Largest: public.orders 572 MB
Copy: possible
Live: possible
Copying the data into shop ...
The data is copied. shop now follows the source.
Syncing: 12 KB behind, last change applied 10:00:14 UTC.
Keep your app on the source until you are ready. Then stop writes to the source and run `deplane db import finish`.
deplane db import status
Show an import: its state, source, new database and, while a live import is syncing, how far behind it is. ID defaults to the newest active import (or, with none, the newest). With --json, one document with the full report.
deplane db import finish
Finish a live import: the last changes are copied, every table's rows are compared with the source, and the link to the source is removed. Stop writes to the source first (scale your app to 0, or turn on maintenance mode); the CLI asks you to confirm that, and --yes skips the question. If the schema changed, the new database had not caught up, or a count differs, nothing is changed and the import goes back to syncing with the reason. ID defaults to the newest active import. --no-wait returns right after the request.
deplane db import cancel
Cancel an import, in any state before it is done. Copying stops, and Deplane removes what a live import created on the source. The new database stays: empty after a copy or a live import cancelled before its copy finished, with the data copied so far after a live import cancelled while in sync; delete it with deplane db destroy. Asks for confirmation; --yes skips it. If Deplane could not reach the source to clean up, the CLI prints the two SQL lines to run there. ID defaults to the newest active import.
Migrations
deplane import heroku
Move a Heroku app to Deplane: deplane import heroku HEROKU_APP [--source DIR] [--name NAME] [--check] [--yes]. See Moving from Heroku for what moves, what stays, and how to switch, or import from the dashboard instead (From the dashboard).
The command reads the Heroku app with a Heroku token (only GET requests, to api.heroku.com) and prints a report: what is copied, what is recreated and what stays on Heroku, with ✓, ⚠ and ✗ lines. Config var names are listed; values never are. When you are logged in, the report also checks your plan's limits (workers, sizes, apps, databases, Redis, instances, memory) against what your team uses, with read-only requests, and a limit the import would cross is a ✗ line. --check stops there and creates nothing; without a Deplane login it skips the plan and says so; --check --json prints the report as one JSON document. Without --check, after a confirmation (--yes skips it), it has Deplane check the Postgres database first, so a database Deplane cannot copy stops the import before anything is created, then creates the app (named --name, or after the Heroku app), sets the copied config vars as secrets, the release command, the web instances and size and the workers, copies the database (Postgres import, copy mode) and attaches it, creates and attaches a Redis, and deploys --source (default: the current folder) as deplane launch does. Only process types that run dynos on Heroku become workers. It ends with the URL and the next steps, including the switch-over with deplane db switch.
The Heroku token is read from stdin (a pipe, or a hidden prompt in a terminal) or from HEROKU_API_KEY. A piped token wins over the variable, which is used when nothing is piped in; the CLI says which one it used, never the token. There is no flag for it, and a token typed as the argument is refused (token_in_argument). It is sent only to Heroku: never to Deplane, never printed or written, and --debug shows Heroku requests as method, path and status only. Create a one-hour, read-only token for it:
$ heroku authorizations:create --scope read-protected --expires-in 3600 --short | deplane import heroku shop --check
$ heroku authorizations:create --scope read-protected --expires-in 3600 --short | deplane import heroku shop --yes
Reading the Heroku app shop (read-only) ...
...
Deployed shop v1: https://shop.deplane.dev
Imported the Heroku app shop into shop: https://shop.deplane.dev
Heroku still serves your users; nothing changed there.
If a step fails, the command stops, lists what it created, the commands to finish by hand and the commands to delete it, and exits with the failed step's code (6 for a plan limit, 3 for a failed build, 4 for a failed deploy). A report with a ✗ line exits with 1 (heroku_import_refused).
deplane import render
Move a Render web service, with the background workers and cron jobs built from the same repository, to Deplane: deplane import render SERVICE [--source DIR] [--name NAME] [--check] [--yes]. SERVICE is the service's ID (srv-...) or its name. See Moving from Render, or import from the dashboard instead (From the dashboard).
It works like deplane import heroku: a read-only check (only GET requests, to api.render.com), a report with ✓, ⚠ and ✗ lines that names variables but never shows their values, your plan's limits checked before anything is created, then, after a confirmation (--yes skips it), the app with its secrets, release command (Render's pre-deploy command), instances, size, workers and cron jobs, the Postgres database copied through its external URL (Postgres import, copy mode), a new Redis for a Key Value, and the deploy of --source. A database whose IP allow list does not let Deplane in is a ✗ line that names the addresses to add.
The Render API key is read from a pipe on stdin, then from RENDER_API_KEY, then from a hidden prompt in a terminal; a piped key wins. There is no flag for it, and a key typed as an argument is refused (token_in_argument). It is sent only to Render, never to Deplane, never printed or written. Render keys can change everything your account can: delete it when the import is done.
$ deplane import render shop --check
Render API key (input is hidden; create one in the Render Dashboard under Account Settings → API Keys):
Render service shop (web service, region frankfurt, repo github.com/acme/shop@main, commit 1a2b3c4) → Deplane app shop
...
A report with a ✗ line exits with 1 (render_import_refused); a failed step stops the import as for Heroku.
deplane import railway
Move a Railway service, with the worker and cron services of the same environment, to Deplane: deplane import railway PROJECT[/SERVICE] [--source DIR] [--name NAME] [--check] [--yes]. PROJECT is the project's name or ID; SERVICE is needed when more than one service has a public domain. See Moving from Railway, or import from the dashboard instead (From the dashboard).
It works like deplane import heroku: a read-only check (GraphQL queries only, to backboard.railway.com), the same report and plan check, then the app with its secrets, release command (the pre-deploy command), replicas, workers and cron jobs, the Postgres database copied through its public URL (DATABASE_PUBLIC_URL, which needs a TCP proxy on the Postgres service), a new Redis, and the deploy of --source. Sealed variables cannot be read through Railway's API; the report says so.
The token is read from a pipe on stdin, then from RAILWAY_TOKEN, then from a hidden prompt; a piped token wins. Use a project token (the project's Settings → Tokens): it reaches one environment of one project, and that environment is the one imported. An account token also works; the CLI then imports the production environment, or the only one. Workspace tokens are not supported yet. There is no flag for the token, and an argument that holds it is refused. It is sent only to Railway, never to Deplane, never printed or written.
$ RAILWAY_TOKEN=... deplane import railway shop --check
Railway service shop (project shop, environment production, region europe-west4-drams3a) → Deplane app shop
...
A report with a ✗ line exits with 1 (railway_import_refused).
deplane import compose
Move a Docker Compose project to Deplane: deplane import compose [-f FILE] [--prefix NAME] [--check] [--yes]. See Moving from Docker Compose for what each service becomes and what cannot move.
The command reads the compose file (-f FILE, default compose.yaml, compose.yml, docker-compose.yaml or docker-compose.yml in the current folder), fills in its ${VAR} variables from your environment and the .env beside it, reads its env_files, and prints a report with ✓, ⚠ and ✗ lines: services with ports: become apps (from their image:, or built from their build: folder), well-known Postgres, MySQL/MariaDB, Redis/Valkey and ClickHouse images become managed ones attached to the services that use them, services that share an app's image or build become its workers, other services become apps on the private network, named volumes become disks, and variables become secrets. Variable names are listed; values never are. Variables that pointed at a database (its host, URL, user, password or name) are filled in from the managed one on Deplane's servers, so its password never reaches your computer. When you are logged in, the report also checks your plan's limits against what your team uses, with read-only requests. --check stops there and creates nothing; --check --json prints the report as one JSON document.
Without --check, after a confirmation (--yes skips it), it creates the apps, sets their port, health check and private network, scales them, creates and attaches the databases, sets the secrets, adds the workers and disks, waits for the databases, and deploys each app: an image with deploy --image, a folder as deplane deploy does (writing deplane.toml there if it has none). --prefix NAME names the apps: the main app (the first with a port) is NAME, the others NAME-SERVICE; the default is the file's name:, else its folder's name. Data is not copied: the report says how to copy it.
$ deplane import compose --check
$ deplane import compose -f deploy/docker-compose.prod.yml --prefix shop --yes
...
Imported 1 app(s):
shop https://shop.deplane.dev
-f names the compose file only after import compose (and the stack file after stack); elsewhere it is --follow. If a step fails, the command stops, lists what it created, the commands to finish by hand and the commands to delete it, and exits with the failed step's code. A report with a ✗ line exits with 1 (compose_import_refused); nothing is created before that.
deplane import fly
Move a Fly.io app, with its process groups and scheduled Machines, to Deplane: deplane import fly APP [--source DIR] [--name NAME] [--check] [--yes]. See Moving from Fly.io, or import from the dashboard instead (From the dashboard).
It works like deplane import heroku: a read-only check (only GET requests, to Fly.io's Machines API at api.machines.dev), the same report and plan check, then the app with its variables as secrets, release command, instances, size, workers and cron jobs. fly.toml in --source is the main source: its [processes], [http_service] or [[services]] (internal_port), [deploy] release_command, [[mounts]] and [env]; the Machines give the counts and sizes (by memory: up to 512 MB small, 1 GB medium, 2 GB large). Fly.io never gives secret values through its API, so secrets are listed by name: the app is then created without being deployed, and the next steps say which secrets to set before deplane deploy. No database is copied: the report says how to move Fly Postgres.
The token is read from a pipe on stdin, then from FLY_API_TOKEN, then from a hidden prompt; a piped token wins. Use a read-only one: fly tokens create readonly --expiry 1h | deplane import fly shop --check. There is no flag for it, and an argument that holds it is refused. It is sent only to Fly.io, never to Deplane, never printed or written.
A report with a ✗ line exits with 1 (fly_import_refused).
deplane import vercel
Move a Vercel project, with its cron jobs, to Deplane: deplane import vercel [TEAM/]PROJECT [--source DIR] [--name NAME] [--check] [--yes]. PROJECT is the project's name or ID; TEAM is the team's slug or ID, also read from VERCEL_TEAM_ID or the folder's .vercel/project.json. See Moving from Vercel, or import from the dashboard instead (From the dashboard).
It works like deplane import heroku: a read-only check (only GET requests, to api.vercel.com), the same report and plan check, then the app with its production variables as secrets and as build secrets, a cron job for each Vercel cron (a request to the app's path, over the private network), and the deploy of --source. Secret (sensitive) variables cannot be read through Vercel's API: they are listed by name, and the app is then created without being deployed. The report says what does not move: Vercel Functions outside Next.js, vercel.json rewrites, redirects and headers, and Vercel's own services.
The token is read from a pipe on stdin, then from VERCEL_TOKEN, then from a hidden prompt; a piped token wins. There is no flag for it, and an argument that holds it is refused. It is sent only to Vercel, never to Deplane, never printed or written.
A report with a ✗ line exits with 1 (vercel_import_refused).
deplane import netlify
Move a Netlify site to Deplane: deplane import netlify SITE [--source DIR] [--name NAME] [--check] [--yes]. SITE is the site's name (or name.netlify.app) or ID. See Moving from Netlify, or import from the dashboard instead (From the dashboard).
It works like deplane import heroku: a read-only check (only GET requests, to api.netlify.com), the same report and plan check, then the app with its production variables as secrets (and the ones Netlify gives to builds as build secrets, with netlify.toml's), and the deploy of --source. Secret variables cannot be read through Netlify's API: they are listed by name, and the app is then created without being deployed. Functions, Edge Functions, redirects, Forms and Identity are listed, not moved.
The token (a personal access token) is read from a pipe on stdin, then from NETLIFY_AUTH_TOKEN, then from a hidden prompt; a piped token wins. There is no flag for it, and an argument that holds it is refused. It is sent only to Netlify, never to Deplane, never printed or written.
A report with a ✗ line exits with 1 (netlify_import_refused).
deplane import cpanel
Move a WordPress site from cPanel to Deplane: deplane import cpanel --host HOST --user USER [--site DOMAIN] [--name NAME] [--backup FILE] [--db-dump FILE] [--check] [--cutover] [--replace-live] [--yes]. See Moving from cPanel for what moves, what stays (email), and how to switch DNS. CLI only for now: the dashboard has no cPanel import yet.
The command reads the cPanel account with a cPanel API token (read-only calls to https://HOST:2083): the site's document root (--site, default the main domain), its wp-config.php and WordPress version, the PHP version, the database's size, and the domain's mailboxes and MX records. It prints a report with ✓, ⚠ and ✗ lines. Setting names are listed; values from wp-config.php (the database password, keys) never are. When you are logged in, the report also checks that the WordPress template fits your plan (a small app, a medium MySQL and a 5 GB disk; Free has no MySQL) and that the app name (--name, default from the domain: example.com becomes example-com) is free. --check stops there and creates nothing; --check --json prints the report as one JSON document.
Without --check, after a confirmation (--yes skips it), it has cPanel write a full account backup into your home directory, downloads it, and keeps only the site's files and database dump in a private temporary folder; sizes are checked here (site files up to 4.5 GB, database up to 5 GB), so a site that does not fit stops before anything is created. With --backup FILE it reads a backup you downloaded yourself instead (with --db-dump FILE for a home directory backup's database). Then it creates the app from the WordPress template with its MySQL and disk, turns on the app's one-time import receiver, replaces the app's files and database with the site's (site addresses rewritten to the app's own address, PHP serialized values kept valid), turns the receiver off, and prints the test address and the cutover checklist. While the files and database are replaced, the site shows a maintenance page. Running the same command again replaces the files and database of the app an earlier import made (of the same site only), after a disk backup and a MySQL backup.
--cutover copies the site again into that app without the address rewrite, sets the address to the real domain, and adds the domain and www. to the app, printing their DNS records. Run it when you are ready to change DNS. After a cutover the app is the live site: running the import or --cutover again asks you to type the domain (--yes does not skip that), or needs --replace-live (cpanel_live_site).
The token is read from stdin (a pipe, or a hidden prompt in a terminal) or from CPANEL_API_TOKEN. There is no flag for it, and a token typed as an argument is refused (token_in_argument). It is sent only to cPanel: never to Deplane or to the app, never printed or written, and --debug shows cPanel calls by name and status only.
$ deplane import cpanel --host server1.host.example --user shop --check < token.txt
$ deplane import cpanel --host server1.host.example --user shop --yes < token.txt
...
example.com's copy is live at https://example-com.deplane.app, with the site's files, database and users.
$ deplane import cpanel --host server1.host.example --user shop --cutover --yes < token.txt
A report with a ✗ line exits with 1 (cpanel_import_refused). If a step after the app was created fails, the command says how to run it again and how to delete what it made, and turns the import receiver off.
Stacks
Several apps, their workers, cron jobs, databases, disks, domains and variables, described once in stack.yaml and applied together. See Stacks for the file and what an apply changes or leaves. -f FILE picks the file (default: stack.yaml in the current folder). AI tools can read, check and plan stacks (the stacks and check_stack tools), never apply them.
deplane stack check
Check a stack file: deplane stack check [-f FILE]. It reads the file and reports every problem, not only the first, as ✗ (stops the stack) and ⚠ (worth reading) lines, each with the place in the file and the fix. When you are logged in, it then plans the file against your team, as deplane stack plan does; logged out, it checks on your computer only and says so. It changes nothing.
$ deplane stack check
✓ stack.yaml: stack shop, 2 services, 2 engines.
A file Deplane cannot read at all fails at once with the line (stack_invalid). A file with a ✗, or a plan that cannot be applied, exits with 1. --json prints {"valid": true|false, "problems": [{"path", "code", "message", "fix", "warning"}]}, with "plan" when logged in. A path: folder's deplane.toml fills in what the stack leaves out; where both set a field, the stack wins (set_in_both).
deplane stack plan
Show what applying the file would do to your team now: deplane stack plan [-f FILE] [--adopt] [--move] [--prune]. It changes nothing. Each object gets a line, + create, ~ update, = unchanged, → adopt, ⇢ move (with --move), - left (no longer in the file; it keeps running and stays in the stack until pruned), ✗ delete (with --prune) or ! refused, with the settings that change below it. Variables show names only, never values. Then come the ⚠ and ✗ lines, whether the stack fits your plan, and the values the apply will ask for.
$ deplane stack plan
= postgres shop-db (unchanged)
= app shop-web (unchanged)
~ app shop-api (update)
instances: 1 → 2
env.LOG_LEVEL: set by hand → set
⚠ services.shop-api.env.LOG_LEVEL: LOG_LEVEL was set by hand since the last apply; an apply with --yes sets it back.
Keep the change in the file instead, or leave it out of the file to manage it by hand.
--adopt and --prune plan what deplane stack apply would do with them. --json prints the plan: stack, new, can_apply, changes, problems, steps, fit, prompts and kept. The command exits with 1 when the plan cannot be applied (a ✗ line, or it does not fit your plan), so a CI job can stop on it; --json still prints the plan.
deplane stack apply
Create or update the stack the file describes: deplane stack apply [-f FILE] [--adopt] [--move] [--prune] [--yes] [--no-wait]. It plans, prints the plan, asks for a confirmation (--yes skips it; when the plan deletes anything, with --prune, it asks you to type the stack's name instead, and --yes does not skip that; the apply is refused if it would then delete anything else, stack_plan_changed), asks for the values the file marks {prompt: true}, starts the apply on Deplane's servers and prints each step as it finishes. At the end it uploads and deploys the services whose source is a path: folder, as deplane deploy does.
$ deplane stack apply
Stack shop is new.
+ postgres shop-db (create)
...
Apply stack shop (18 steps)? [y/N] y
STRIPE_KEY for shop-web (hidden):
✓ postgres shop-db: create small
✓ app shop-web: create
...
Applied stack shop.
A {prompt: true} value comes from DEPLANE_STACK_<SERVICE>__<NAME> or DEPLANE_STACK_<NAME> when one is set (the CLI says which it used, never the value), else from a hidden prompt in a terminal; a shell variable that only has the same name is never read; there is no flag for it (stack_prompt_missing). Deplane sets it and does not keep it.
--adopt: apps and databases your team already has outside any stack, under a name the file uses, join the stack and are changed to match the file. Without it such a name is refused (name_taken).--move: apps and databases of another stack that the file names move into this stack, with their values and whether a stack created them. Without it such a name is refused (in_another_stack). Needs your own login; CI tokens cannot move.--prune: delete the apps and databases the stack created that the file no longer has, with their data. Without it they keep running and stay in the stack, shown as left on every plan, so a later--prunestill deletes them. It needs a fresh confirmation with your passkey in the browser; CI tokens cannot prune. Objects that joined with--adoptare never deleted.--yes: also set back variables changed by hand since the stack set them. Without it, such a plan stops before anything changes (stack_changed_outside).--no-wait: return once the apply has started; follow it withdeplane stack status.path:folders are then not uploaded.
A plan with a ✗ stops before anything changes (stack_cannot_apply). A failed step stops the apply and exits with 1 (stack_apply_failed); fix the cause and run deplane stack retry. With --json, it prints the finished apply: id, stack, status, step, steps and message. A CI token can apply, but cannot connect a GitHub repository to an app for the first time (ci_token_not_allowed).
deplane stack list
List your team's stacks: name, who manages it (blueprint for a stack.yaml), how many apps and databases it holds, its revision (one more for each apply), and its last apply's status. --json prints the list.
$ deplane stack list
shop blueprint 4 objects revision 3 apply done
deplane stack status
Show one stack: deplane stack status NAME. Its apps with their state, ready instances and address, its databases, which objects joined with --adopt, and its last apply: its ID, status, the step it is on while running, and the message of a failed one. --json prints the stack with members, status and apply.
$ deplane stack status shop
Stack shop (blueprint, revision 3)
postgres shop-db
app shop-web Running, 2 ready https://shop-web.deplane.app
Last apply sta_8f2k...: failed
The app shop-web was deleted while the stack was being applied. Apply again.
deplane stack export
Print a stack as stack.yaml: deplane stack export NAME [--format yaml]. It is built from the last file applied, and never contains a secret: values come back only when neither their name nor a password in a URL looks secret, the rest as {prompt: true}, generated values as {generate: N}. --format yaml is the only format for now (invalid_format).
$ deplane stack export shop > stack.yaml
deplane stack retry
Continue a failed apply from the step that failed: deplane stack retry APPLY-ID (deplane stack status NAME shows the ID). Steps already done are kept. After a failed build or release, apply again instead: a retry waits for the same build. The retry runs as you and prints each step as deplane stack apply does; --no-wait returns at once. Values asked for at apply time are not kept, so if the remaining steps need them, run deplane stack apply again instead (stack_retry_needs_values). Only a failed apply can be retried (stack_apply_not_failed). When deletes are still to run, it asks you to type the stack's name, as apply does.
deplane stack forget
Forget a stack: deplane stack forget NAME. Its apps and databases keep running, outside any stack; nothing is deleted. It first lists what it releases (no stack command deletes those afterwards), then asks (--yes skips the question). Only a person can forget a stack: CI tokens are refused (ci_token_not_allowed). A stack being applied cannot be forgotten until the apply ends (stack_apply_running).
deplane stack destroy
Delete what a stack created, then the stack: deplane stack destroy NAME. It lists the apps and databases that go, with their data, and asks you to type the stack's name (always, even with --yes). Then it needs a fresh confirmation with your passkey in the browser, and follows the deletes. Apps and databases that joined with --adopt are left running, outside any stack. CI tokens and AI tools cannot destroy a stack.
$ deplane stack destroy shop
This deletes, with their data: app shop-web, app shop-api, postgres shop-db, redis shop-cache.
Type the stack's name (shop) to confirm shop
Destroyed stack shop.
Team
Work on apps together. Everyone signs in with their own passkey, and everything they do appears in the activity log with their name. Members deploy and manage apps; owners can also invite and remove people.
deplane team
List your team's members and open invitations.
deplane team invite
Invite someone by email. You get a link to send them yourself: it works once, for that address, for 7 days, and they set up a passkey when they join. Needs a fresh confirmation with your passkey. --role owner makes them an owner.
$ deplane team invite colleague@company.com
Send this link to colleague@company.com. It works once, for 7 days, and is shown only now:
https://app.deplane.io/invite?t=…
deplane team remove
Remove a member. Their tokens for the team stop working. Asks for confirmation; pass --yes in scripts. Needs a fresh confirmation with your passkey.
Workers and cron jobs
Run more processes from the same app: workers that run all the time (a queue consumer, a bot) and cron jobs that run on a schedule (a nightly report). They use the app's current image and its variables, each in its own microVM, and their output appears in deplane logs. The command runs through the image's shell (/bin/sh -c). Workers count against your plan's instances and memory; a cron job counts as one instance of its size. Free has cron jobs but no workers: adding a worker, or starting or growing one, is refused with limit_sleep; a worker from before a move to Free can be scaled down or removed.
deplane processes
List the app's workers and cron jobs.
$ deplane processes
NAME KIND RUNS SIZE COMMAND
emails worker 1 instance(s) small node emails.js
nightly cron 0 3 * * * UTC small node report.js
deplane processes add
Add a worker, or with --schedule a cron job. Schedules have five fields in UTC (minute, hour, day of month, month, day of week), or @hourly, @daily, @weekly, @monthly. A cron job runs one instance at a time, for at most an hour, and a failed run is not retried.
$ deplane processes add emails --command "node emails.js"
$ deplane processes add nightly --command "node report.js" --schedule "0 3 * * *"
deplane processes scale
Change a worker's instances (0 stops it), or any process's --size, --command, or --schedule.
deplane processes remove
Remove a worker or cron job. Asks for confirmation; pass --yes in scripts.
App disks
A 5 GB disk for an app, mounted at a path, whose files stay across deploys and restarts. An app with a disk runs one instance and does not autoscale, and each deploy stops it for a few seconds. See Disks.
deplane disks create
Give an app a disk: deplane disks create APP [--path /data]. The app must run one instance without autoscaling; if it runs more, scale it first with deplane scale 1 --max 0 --app APP.
$ deplane disks create my-app
Added a 5 GB disk to my-app at /data. Files there survive deploys and restarts, and are backed up every night.
my-app now runs one instance and cannot autoscale. Each deploy stops it for a few seconds while the new release takes the disk.
deplane disks list
List your team's disks with their path and last backup. --json prints {"disks": [...]}.
deplane disks backups
List an app's disk backups, newest first: ID, status, nightly or on request, start time (UTC), and size.
deplane disks backup
Back up an app's disk now. At most once an hour; the app keeps running.
deplane disks restore
Replace all files on an app's disk with a backup: deplane disks restore APP BACKUP-ID. Files written since that backup are lost, and the app stops while the restore runs. Asks for confirmation; pass --yes in scripts. It also needs a confirmation in your browser with your passkey, each time; CI tokens and AI tools cannot restore disks.
deplane disks destroy
Destroy an app's disk and the files on it. Asks for confirmation; pass --yes in scripts. Like restore, it needs your passkey in the browser each time. Its backups are kept for 3 days.
deplane templates list
Lists the templates with what each needs (memory, Postgres, Redis, MySQL, ClickHouse, a disk) and whether your team can deploy it now: ready, "no room now" (free some room: the message says how much is needed and free) or "needs a larger plan". Below, your team's recent template deploys with their ID, status and step. --category Starters|Analytics|Monitoring|Productivity|Developer tools|Data narrows the list. --json prints both lists.
deplane templates show
deplane templates show NAME shows one template: its description, what it needs, whether your team has room, its licence, source and version, the variables it sets (names only) and the trademark notice.
deplane templates retry
deplane templates retry DEPLOY-ID continues a failed template deploy from the step that failed. What was already made is kept and its generated secrets stay the same; at the last step it builds again.
deplane templates credentials
deplane templates credentials DEPLOY-ID shows, once, the admin sign-in a template set up at deploy time (for example Grafana's generated admin password), for a deploy you started and that is done. deplane launch --template prints it by itself when it waits for the deploy; use this for a deploy started with --no-wait, from the dashboard or by an AI tool. Deplane never shows it a second time (template_credentials_shown).
deplane templates delete
deplane templates delete DEPLOY-ID deletes the app (with its disk) and the database, Redis, MySQL and ClickHouse database that this deploy made, and nothing else; one that another app uses is kept. It asks first (--yes skips the question) and needs your browser confirmation, like other deletions. A running deploy cannot be deleted; wait for it to finish or fail.
Redis
A managed Redis (Valkey 8, 256 MB) for caches and queues. See Redis.
deplane redis create
Create a Redis: deplane redis create NAME. It counts 256 MB against your plan's memory.
deplane redis list
List your team's Redis with their address, apps and last backup. --json prints {"redis": [...]}. Passwords are never shown.
deplane redis attach
Give an app the Redis as REDIS_URL: deplane redis attach NAME --app APP. A new release rolls out. An app has one Redis.
deplane redis detach
Remove an app's REDIS_URL: deplane redis detach NAME --app APP. The Redis and its data are kept.
deplane redis backups
List a Redis's backups, newest first: ID, status, nightly or on request, start time (UTC), size.
deplane redis backup
Back up a Redis now. At most once an hour; it keeps running.
deplane redis restore
Replace a Redis's data with a backup: deplane redis restore NAME BACKUP-ID. Data written since the backup is lost, and the Redis stops while the restore runs. Asks for confirmation; pass --yes in scripts. It also needs your passkey in the browser each time; CI tokens and AI tools cannot restore.
deplane redis destroy
Destroy a Redis and its data; its apps lose REDIS_URL. Asks for confirmation; pass --yes in scripts. Needs your passkey each time. Its backups are kept for 3 days.
MySQL
A managed MySQL 8.4 (medium or large, 10 GB) for apps that need MySQL. See MySQL.
deplane mysql create
Create a MySQL: deplane mysql create NAME [--size medium|large]. Without --size it is medium (1 GB). It counts as one instance of its size against your plan.
deplane mysql list
List your team's MySQL with their status, size, apps and last backup. --json prints {"mysql": [...]}, with each one's host, port and version. Passwords are never shown.
deplane mysql attach
Give an app the MySQL as MYSQL_URL: deplane mysql attach NAME --app APP. A new release rolls out. An app has one MySQL.
deplane mysql detach
Remove an app's MYSQL_URL: deplane mysql detach NAME --app APP. The MySQL and its data are kept.
deplane mysql backups
List a MySQL's backups, newest first: ID, status, nightly or on request, start time (UTC), size. deplane mysql backups --destroyed lists instead the backups of your team's destroyed MySQL, with the name each came from; you can restore them into another MySQL until they expire.
deplane mysql backup
Back up a MySQL now. At most once an hour; it keeps running.
deplane mysql restore
Replace a MySQL's data with a backup: deplane mysql restore NAME BACKUP-ID. The backup can be from any MySQL of your team, including a destroyed one. Data written since the backup is lost, and the MySQL stops while the restore runs. Asks for confirmation; pass --yes in scripts. It also needs your passkey in the browser each time; CI tokens and AI tools cannot restore.
deplane mysql destroy
Destroy a MySQL and its data; its apps lose MYSQL_URL. Asks for confirmation; pass --yes in scripts. Needs your passkey each time. Its backups are kept for 3 days at most; deplane mysql backups --destroyed lists them.
There is no command to open a MySQL session from your computer: the MySQL has no public address. To run SQL, use deplane run in an app whose image has a client; see MySQL.
ClickHouse
A managed ClickHouse 26.8 database (large, 2 GB memory, 10 GB) for analytics and event data. See ClickHouse®.
deplane clickhouse create
Create a ClickHouse database: deplane clickhouse create NAME [--size large]. Large is the only size, and the default. It counts as one large instance against your plan and needs 2 GB of your plan's memory.
deplane clickhouse list
List your team's ClickHouse databases with their status, size, disk used (as of the last backup), apps and last backup. If your plan cannot hold one, it says what to upgrade, and links https://deplane.io/pricing. --json prints {"clickhouse": [...], "plan": {"allowed", "fits", "message"}}, with each one's host, ports and version. Passwords are never shown.
deplane clickhouse attach
Give an app the ClickHouse database as CLICKHOUSE_URL: deplane clickhouse attach NAME --app APP. A new release rolls out. An app has one ClickHouse database.
deplane clickhouse detach
Remove an app's CLICKHOUSE_URL: deplane clickhouse detach NAME --app APP. The ClickHouse database and its data are kept.
deplane clickhouse backups
List a ClickHouse database's backups, newest first: ID, status, nightly or on request, start time (UTC), size, and why one failed. It ends with the disk used at the last backup, against the 4.5 GB line above which backups have no room. deplane clickhouse backups --destroyed lists instead the backups of your team's destroyed ClickHouse databases, with the name each came from; you can restore them into another one until they expire.
deplane clickhouse backup
Back up a ClickHouse database now. At most once an hour; it keeps running. At most 2 backups are kept, and this one counts.
deplane clickhouse restore
Replace a ClickHouse database's data with a backup: deplane clickhouse restore NAME BACKUP-ID. The backup can be from any ClickHouse database of your team, including a destroyed one; another one's backup brings its password, so the apps get a new CLICKHOUSE_URL and restart. Data written since the backup is lost, and the database stops while the restore runs. Asks for confirmation; pass --yes in scripts. It also needs your passkey in the browser each time; CI tokens and AI tools cannot restore.
deplane clickhouse destroy
Destroy a ClickHouse database and its data; its apps lose CLICKHOUSE_URL. Asks for confirmation; pass --yes in scripts. Needs your passkey each time. Its backups are kept for 3 days at most; deplane clickhouse backups --destroyed lists them.
There is no command to open a session with a ClickHouse database from your computer: it has no public address. To run SQL, use deplane run in an app whose image has a client; see ClickHouse.
Admin commands
These commands work only for Deplane administrators. Everyone else gets not_found. Each one needs a fresh passkey confirmation from the last 5 minutes; in a terminal the CLI opens the browser for it.
deplane admin requests
List beta access requests.
| Flag | What it does |
|---|---|
--status STATUS | pending (default), approved, or denied. |
$ deplane admin requests
ID EMAIL NAME COMPANY REQUESTED
acr_3n8m2b5v9c1x7z4l6k0j dev@example.com Dana Lee Example 2026-10-12
{
"access_requests": [
{
"id": "acr_3n8m2b5v9c1x7z4l6k0j",
"email": "dev@example.com",
"name": "Dana Lee",
"company": "Example",
"use_case": "Internal tools",
"status": "pending",
"created_at": "2026-10-12T16:20:00Z"
}
]
}
deplane admin approve
Approve a pending access request by email or ID: deplane admin approve EMAIL|ID. This creates the user and a personal organization on the default plan, and tries to email them a sign-in link. Email sending is not set up yet, so the email may not arrive; contact them another way.
$ deplane admin approve dev@example.com
Approved dev@example.com. They were emailed a sign-in link.
{
"user_id": "usr_6f1d9s4a8p2o7i3u5y0t",
"org_id": "org_9r4e7w2q5t8y1u6i3o0p",
"email": "dev@example.com"
}
deplane admin deny
Deny a pending access request by email or ID: deplane admin deny EMAIL|ID. Asks for confirmation; pass --yes to skip it.
$ deplane admin deny dev@example.com --yes
Denied dev@example.com.
{
"denied": "acr_3n8m2b5v9c1x7z4l6k0j"
}
deplane admin abuse list
List abuse reports, oldest first, so the queue is worked in order.
| Flag | What it does |
|---|---|
--status STATUS | open (default), investigating, actioned, rejected, or all. |
$ deplane admin abuse list
ID STATUS CATEGORY APP URL REPORTED
abr_2k7j4h9g1f6d3s8a5p0o1q2w open phishing pay-verify-01 https://pay-verify-01.deplane.app/x 2026-10-13 07:55
{
"abuse_reports": [
{
"id": "abr_2k7j4h9g1f6d3s8a5p0o1q2w",
"status": "open",
"category": "phishing",
"url": "https://pay-verify-01.deplane.app/x",
"details": "This page copies our bank's login form.",
"reporter_email": "victim@example.org",
"reporter_ip": "203.0.113.9",
"app_id": "app_8d2k4j6h1g3f5s7a9p0o1q2w",
"app": "pay-verify-01",
"org_id": "org_3j5h7g9f1d2s4a6p8o0i1u2y",
"org_slug": "7f3k2p",
"actions": [],
"created_at": "2026-10-13T07:55:00Z",
"resolved_at": null,
"resolution_note": ""
}
]
}
app, app_id, org_id, and org_slug are empty when the reported address is not a Deplane app. reporter_email is empty for an anonymous report.
deplane admin abuse show
Show one abuse report in full: deplane admin abuse show ID. This includes the details, the reporter, the reported app and its organization, and the history of the report.
$ deplane admin abuse show abr_2k7j4h9g1f6d3s8a5p0o1q2w
Report: abr_2k7j4h9g1f6d3s8a5p0o1q2w
Status: open
Category: phishing
URL: https://pay-verify-01.deplane.app/x
App: pay-verify-01 (organization 7f3k2p, org_3j5h7g9f1d2s4a6p8o0i1u2y)
Reporter: victim@example.org from 203.0.113.9
Received: 2026-10-13T07:55:00Z
Details:
This page copies our bank's login form.
The JSON is one report, with the same fields as in deplane admin abuse list.
deplane admin abuse resolve
Move a report to a new status: deplane admin abuse resolve ID --status investigating|actioned|rejected [--note TEXT]. Each step is added to the report's actions history. actioned and rejected also record who resolved it, when, and the note. Resolving a report does not suspend anything; use suspend-app or suspend-org for that.
| Flag | What it does |
|---|---|
--status STATUS | Required. investigating, actioned, or rejected. |
--note TEXT | What you found or did. Stored with the report. |
$ deplane admin abuse resolve abr_2k7j4h9g1f6d3s8a5p0o1q2w --status actioned --note "Phishing confirmed; app suspended."
Report abr_2k7j4h9g1f6d3s8a5p0o1q2w is now actioned.
The JSON is the updated report, as in deplane admin abuse show, with status, resolved_at, resolution_note, and a new entry in actions (at, by, status, note).
deplane admin suspend-app
Suspend an app by name, in any organization: deplane admin suspend-app NAME --reason TEXT [--yes]. The app runs zero instances and can no longer be deployed or changed. Nothing is deleted. The reason is recorded in the organization's audit log. Without a terminal, pass --yes.
| Flag | What it does |
|---|---|
--reason TEXT | Required, at most 500 characters. Why the app is suspended; include the report ID. |
$ deplane admin suspend-app pay-verify-01 --reason "Phishing, report abr_2k7j4h9g1f6d3s8a5p0o1q2w" --yes
Suspended app pay-verify-01. Undo with `deplane admin unsuspend --app pay-verify-01`.
{
"id": "sus_4h6j8k0l2z4x6c8v0b2n4m6q",
"target_type": "app",
"target_id": "app_8d2k4j6h1g3f5s7a9p0o1q2w",
"reason": "Phishing, report abr_2k7j4h9g1f6d3s8a5p0o1q2w",
"created_at": "2026-10-13T08:10:00Z",
"lifted_at": null
}
deplane admin suspend-org
Suspend an organization by slug or ID: deplane admin suspend-org SLUG --reason TEXT [--yes]. All its apps run zero instances, and all its logins and tokens stop working (account_suspended). You cannot suspend your own organization.
| Flag | What it does |
|---|---|
--reason TEXT | Required, at most 500 characters. Why the organization is suspended. |
$ deplane admin suspend-org 7f3k2p --reason "Repeated phishing" --yes
Suspended organization 7f3k2p. Undo with `deplane admin unsuspend --org 7f3k2p`.
The JSON has the same shape as for suspend-app, with "target_type": "org" and the organization's ID as target_id.
deplane admin unsuspend
Lift a suspension: deplane admin unsuspend --app NAME or deplane admin unsuspend --org SLUG. Pass exactly one of the two. Apps return to the instance count they had. Without a terminal, pass --yes.
| Flag | What it does |
|---|---|
--app NAME | Unsuspend this app. |
--org SLUG | Unsuspend this organization (slug or ID). |
$ deplane admin unsuspend --org 7f3k2p --yes
Unsuspended organization 7f3k2p.
The JSON is the lifted suspension, with lifted_at set.
Help
deplane help prints the command list, the global flags, the environment variables, and the exit codes. deplane help --json prints:
{
"commands": [
{"command": "apps", "usage": "List your apps."},
{"command": "deploy", "usage": "Build and deploy this folder's app again [--app NAME]."}
]
}
This page is also available as Markdown. AI agents can start at /llms.txt.