Dokploy API from the CLI
Operate Dokploy entirely from curl when the dashboard isn't practical (scripting, agents, headless ops).
Auth
Generate an API token in dashboard → Profile → API. Pass on every request:
-H "x-api-key: $TOKEN"
Other auth headers (Authorization: Bearer …) return 401 Unauthorized. There is no public OpenAPI/swagger endpoint — discover routes by probing.
Endpoint Catalog (verified)
All endpoints are under https://<dokploy-host>/api/<router>.<procedure>. Reads are GET with query params, writes are POST with JSON body.
Discovery
| Endpoint |
Notes |
GET /api/project.all |
List projects |
GET /api/project.one?projectId=<id> |
Project + nested environments[].{applications, mongo, postgres, redis, compose} |
GET /api/postgres.one?postgresId=<id> |
Includes appName (internal hostname), databasePassword, externalPort |
GET /api/application.one?applicationId=<id> |
Includes env (multiline string), sourceType, dockerImage, customGitUrl |
GET /api/compose.one?composeId=<id> |
Includes composeFile, sourceType |
GET /api/deployment.all?applicationId=<id> |
Deploy history (most recent first); composeId works too |
GET /api/docker.getContainersByAppNameMatch?appName=<appName> |
Live container state — use to check actual run status |
Postgres
| Endpoint |
Required body |
POST /api/postgres.create |
{name, appName, databaseName, databaseUser, databasePassword, dockerImage, environmentId} |
POST /api/postgres.update |
{postgresId, ...fields-to-change} (e.g. externalPort: 5433 or null) |
POST /api/postgres.deploy |
{postgresId} — required after update to apply port/image changes |
Compose (one-shot or long-running stacks)
| Endpoint |
Required body |
POST /api/compose.create |
{name, appName, environmentId, composeType: "docker-compose"} |
POST /api/compose.update |
{composeId, name, description, env, composeFile, sourceType: "raw", composeType, autoDeploy, command} — full replace, all fields required |
POST /api/compose.deploy |
{composeId} |
POST /api/compose.stop |
{composeId} |
POST /api/compose.delete |
{composeId} — also removes containers |
Application (long-running services)
| Endpoint |
Required body |
POST /api/application.saveDockerProvider |
{applicationId, dockerImage, username, password, registryUrl} (use empty strings for public images) |
POST /api/application.saveGitProvider |
{applicationId, customGitUrl, customGitBranch, customGitBuildPath, watchPaths, customGitSSHKeyId} |
POST /api/application.saveBuildType |
{applicationId, buildType: "dockerfile"|"nixpacks"|"heroku"|…, dockerfile, dockerContextPath, dockerBuildStage, herokuVersion, railpackVersion} |
POST /api/application.saveEnvironment |
{applicationId, env, buildArgs, buildSecrets, createEnvFile} (env is the entire multiline KEY=VALUE string) |
POST /api/application.deploy |
{applicationId} |
Critical Gotcha: deploy status ≠ container status
deployment.all returns status: "done" as soon as Dokploy's outer compose-up exits successfully. The actual workload container may still be initializing, building, or even crashed afterwards.
For a true pass/fail signal, poll docker.getContainersByAppNameMatch and watch the container's state (running → exited) and status text.
# Wrong: only watches deploy step
poll deployment.all → done = ❌ may still be building inside
# Right: watch the actual container
poll docker.getContainersByAppNameMatch → state=running → state=exited → check exit code
Switching application source: docker registry ↔ git build
Useful when the registry image is outdated (e.g., you need today's master before tagging a release):
application.saveGitProvider — point at the repo + branch
application.saveBuildType — set buildType: "dockerfile", dockerfile: "<path>" (e.g. dockerfile)
application.deploy — Dokploy clones, builds, swaps service
To switch back after the image is published:
application.saveDockerProvider — set dockerImage: "<repo>:<tag>", registryUrl: "index.docker.io"
application.deploy — pulls and swaps
sourceType and related fields update automatically based on which save*Provider you called last.
Probing for unknown endpoint shapes
POST with empty body to discover the required fields via the Zod error response:
curl -sS -X POST -H "x-api-key: $TOKEN" -H "content-type: application/json" \
"https://<host>/api/postgres.create" -d '{}'
# → {"zodError":{"fieldErrors":{"name":[...],"databaseName":[...],...}}}
This is faster than guessing and works for every mutation route.
Common Mistakes
| Mistake |
Fix |
Trusting deployment.all status alone |
Cross-check docker.getContainersByAppNameMatch |
Calling compose.deploy with stale exited containers from a prior run |
ssh host docker rm -f <appName>-<service>-1 first, or compose.stop then compose.deploy |
| Hostname mismatch when service names contain hyphens |
Use the auto-generated appName (e.g. mx-space-pg-7pacdz), not the user-friendly name |
| Updating compose YAML and expecting current container to reflect it |
YAML applies on next deploy; running container keeps the YAML it started with |
Updating externalPort without redeploy |
Must call postgres.deploy (or equivalent) to apply port mapping |
1---2name: dokploy-api-cli3description: Use when operating a Dokploy-managed deployment via REST API from the shell — creating/updating/deploying postgres/redis/compose/application services, switching an application's source between docker registry and git build, or scripting redeploys. Covers auth, the working endpoint catalog, and a critical gotcha about deployment status semantics.4---56# Dokploy API from the CLI78Operate Dokploy entirely from `curl` when the dashboard isn't practical (scripting, agents, headless ops).910## Auth1112Generate an API token in dashboard → Profile → API. Pass on every request:1314```15-H "x-api-key: $TOKEN"16```1718Other auth headers (`Authorization: Bearer …`) return `401 Unauthorized`. There is no public OpenAPI/swagger endpoint — discover routes by probing.1920## Endpoint Catalog (verified)2122All endpoints are under `https://<dokploy-host>/api/<router>.<procedure>`. Reads are GET with query params, writes are POST with JSON body.2324### Discovery2526| Endpoint | Notes |27|---|---|28| `GET /api/project.all` | List projects |29| `GET /api/project.one?projectId=<id>` | Project + nested `environments[].{applications, mongo, postgres, redis, compose}` |30| `GET /api/postgres.one?postgresId=<id>` | Includes `appName` (internal hostname), `databasePassword`, `externalPort` |31| `GET /api/application.one?applicationId=<id>` | Includes `env` (multiline string), `sourceType`, `dockerImage`, `customGitUrl` |32| `GET /api/compose.one?composeId=<id>` | Includes `composeFile`, `sourceType` |33| `GET /api/deployment.all?applicationId=<id>` | Deploy history (most recent first); `composeId` works too |34| `GET /api/docker.getContainersByAppNameMatch?appName=<appName>` | Live container state — use to check actual run status |3536### Postgres3738| Endpoint | Required body |39|---|---|40| `POST /api/postgres.create` | `{name, appName, databaseName, databaseUser, databasePassword, dockerImage, environmentId}` |41| `POST /api/postgres.update` | `{postgresId, ...fields-to-change}` (e.g. `externalPort: 5433` or `null`) |42| `POST /api/postgres.deploy` | `{postgresId}` — required after `update` to apply port/image changes |4344### Compose (one-shot or long-running stacks)4546| Endpoint | Required body |47|---|---|48| `POST /api/compose.create` | `{name, appName, environmentId, composeType: "docker-compose"}` |49| `POST /api/compose.update` | `{composeId, name, description, env, composeFile, sourceType: "raw", composeType, autoDeploy, command}` — full replace, all fields required |50| `POST /api/compose.deploy` | `{composeId}` |51| `POST /api/compose.stop` | `{composeId}` |52| `POST /api/compose.delete` | `{composeId}` — also removes containers |5354### Application (long-running services)5556| Endpoint | Required body |57|---|---|58| `POST /api/application.saveDockerProvider` | `{applicationId, dockerImage, username, password, registryUrl}` (use empty strings for public images) |59| `POST /api/application.saveGitProvider` | `{applicationId, customGitUrl, customGitBranch, customGitBuildPath, watchPaths, customGitSSHKeyId}` |60| `POST /api/application.saveBuildType` | `{applicationId, buildType: "dockerfile"\|"nixpacks"\|"heroku"\|…, dockerfile, dockerContextPath, dockerBuildStage, herokuVersion, railpackVersion}` |61| `POST /api/application.saveEnvironment` | `{applicationId, env, buildArgs, buildSecrets, createEnvFile}` (`env` is the entire multiline `KEY=VALUE` string) |62| `POST /api/application.deploy` | `{applicationId}` |6364## Critical Gotcha: deploy status ≠ container status6566`deployment.all` returns `status: "done"` as soon as Dokploy's outer compose-up exits successfully. **The actual workload container may still be initializing, building, or even crashed afterwards.**6768For a true pass/fail signal, poll `docker.getContainersByAppNameMatch` and watch the container's `state` (`running` → `exited`) and `status` text.6970```bash71# Wrong: only watches deploy step72poll deployment.all → done = ❌ may still be building inside7374# Right: watch the actual container75poll docker.getContainersByAppNameMatch → state=running → state=exited → check exit code76```7778## Switching application source: docker registry ↔ git build7980Useful when the registry image is outdated (e.g., you need today's master before tagging a release):81821. `application.saveGitProvider` — point at the repo + branch832. `application.saveBuildType` — set `buildType: "dockerfile"`, `dockerfile: "<path>"` (e.g. `dockerfile`)843. `application.deploy` — Dokploy clones, builds, swaps service8586To switch back after the image is published:87881. `application.saveDockerProvider` — set `dockerImage: "<repo>:<tag>"`, `registryUrl: "index.docker.io"`892. `application.deploy` — pulls and swaps9091`sourceType` and related fields update automatically based on which `save*Provider` you called last.9293## Probing for unknown endpoint shapes9495POST with empty body to discover the required fields via the Zod error response:9697```bash98curl -sS -X POST -H "x-api-key: $TOKEN" -H "content-type: application/json" \99 "https://<host>/api/postgres.create" -d '{}'100# → {"zodError":{"fieldErrors":{"name":[...],"databaseName":[...],...}}}101```102103This is faster than guessing and works for every mutation route.104105## Common Mistakes106107| Mistake | Fix |108|---|---|109| Trusting `deployment.all` status alone | Cross-check `docker.getContainersByAppNameMatch` |110| Calling `compose.deploy` with stale exited containers from a prior run | `ssh host docker rm -f <appName>-<service>-1` first, or `compose.stop` then `compose.deploy` |111| Hostname mismatch when service names contain hyphens | Use the auto-generated `appName` (e.g. `mx-space-pg-7pacdz`), not the user-friendly `name` |112| Updating compose YAML and expecting current container to reflect it | YAML applies on **next** deploy; running container keeps the YAML it started with |113| Updating `externalPort` without redeploy | Must call `postgres.deploy` (or equivalent) to apply port mapping |