# dokploy-deploy-action Trigger a [Dokploy](https://dokploy.com) deployment from a CI job and wait for it to finish. Works on **GitHub Actions** and **Gitea Actions**. Dokploy's built-in Git integration deploys on every push, before anything has had a chance to fail. This action makes the deployment an ordinary job, so it can sit behind your linters and tests, deploy the exact image you just built, and fail the run when the deployment fails. ```yaml - uses: maxvojtkov/dokploy-deploy-action@v1 with: host: ${{ secrets.DOKPLOY_HOST }} api-key: ${{ secrets.DOKPLOY_API_KEY }} project: shop service: api docker-image: ghcr.io/acme/api:${{ github.sha }} ``` ## Contents - [Why not the built-in webhook?](#why-not-the-built-in-webhook) - [Quick start](#quick-start) - [Gitea](#gitea) - [Choosing the service](#choosing-the-service) - [Deploying a new image](#deploying-a-new-image) - [Compose stacks](#compose-stacks) - [Other actions](#other-actions) - [Inputs](#inputs) - [Outputs](#outputs) - [How it decides the deployment failed](#how-it-decides-the-deployment-failed) - [Troubleshooting](#troubleshooting) - [Development](#development) - [Related](#related) ## Why not the built-in webhook? Dokploy's webhook is the right tool when a push *is* the deployment. It stops being the right tool as soon as you want something between the two: | | Webhook | This action | | --- | --- | --- | | Runs after your tests | no | yes | | Deploys an image built in CI | no | yes, via `docker-image` | | Fails the CI run on a bad deploy | no | yes | | Build log in the CI job | no | yes, on failure or always | | Deploy on a tag, on dispatch, on a schedule | no | yes | | Promote the same artifact to staging then production | no | yes | ## Quick start 1. In Dokploy, go to **Settings → Profile → API/CLI** and create a token. 2. Add two repository secrets: `DOKPLOY_HOST` (e.g. `https://dokploy.example.com`) and `DOKPLOY_API_KEY`. 3. Add a deploy job. ```yaml jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci && npm test deploy: needs: test runs-on: ubuntu-latest steps: - uses: maxvojtkov/dokploy-deploy-action@v1 with: host: ${{ secrets.DOKPLOY_HOST }} api-key: ${{ secrets.DOKPLOY_API_KEY }} project: shop service: api ``` `host` and `api-key` may also come from `DOKPLOY_HOST` and `DOKPLOY_API_KEY` in the environment, which is handy when a whole job talks to Dokploy: ```yaml env: DOKPLOY_HOST: ${{ secrets.DOKPLOY_HOST }} DOKPLOY_API_KEY: ${{ secrets.DOKPLOY_API_KEY }} ``` Worked examples, from lint through image build to deploy: - [`examples/github-build-and-deploy.yml`](examples/github-build-and-deploy.yml) - [`examples/gitea-build-and-deploy.yml`](examples/gitea-build-and-deploy.yml) - [`examples/manual-promote.yml`](examples/manual-promote.yml) — promote or roll back an existing tag from the Actions tab. ## Gitea Nothing changes. The action is plain JavaScript with no dependencies and no bundled `dist/`, so Gitea's `act_runner` executes it straight from the repository, and Gitea sets the same `GITHUB_*` variables the defaults are built from. How you reference it depends on your instance's `DEFAULT_ACTIONS_URL`. If it is the default (`https://github.com`), the short form works: ```yaml - uses: maxvojtkov/dokploy-deploy-action@v1 ``` Otherwise give the full URL, or mirror this repository into your instance: ```yaml - uses: https://github.com/maxvojtkov/dokploy-deploy-action@v1 ``` ## Choosing the service Either name the service, or give an id. ```yaml # By name. `environment` defaults to the project's default environment. project: shop service: api environment: staging ``` `service` matches either the name shown in the UI or the underlying Docker service name, case-insensitively. If the name is wrong the error lists what does exist in that environment, which is usually enough to fix it. ```yaml # By id — from the Dokploy URL, or from a Terraform/Pulumi output. application-id: kJ3n8xQ2vB ``` Ids never go stale in the way names do, but they change when a service is recreated. Names read better in a workflow file; ids are better when the id is already an output of whatever created the service. ## Deploying a new image `docker-image` points the application at a tag and then deploys it. This is the whole reason to run a deployment from CI: the image that gets deployed is the one your tests just passed against. ```yaml - uses: maxvojtkov/dokploy-deploy-action@v1 with: project: shop service: api docker-image: ghcr.io/acme/api:${{ needs.build.outputs.tag }} registry-url: ghcr.io registry-username: ${{ github.actor }} registry-password: ${{ secrets.GHCR_PULL_TOKEN }} ``` Two things to know: - This switches the application's source type to **Docker**. An application Dokploy builds from Git should not be given a `docker-image`; use `redeploy` instead and let Dokploy build it. - Dokploy **stores** the registry credentials and reuses them every time it restarts the container. A job-scoped token like `GITHUB_TOKEN` will work for the deployment and then stop working, so use a long-lived token — or configure a registry in Dokploy once and leave the credentials out here. Always deploy an immutable tag (a commit SHA or a digest) rather than `latest`. With `latest`, nothing about the application changes between deploys, and what you get depends on the node's pull policy. ## Compose stacks Same thing, with `compose-id` or a `service` that names a stack. Images come from the Compose file, so `docker-image` does not apply: ```yaml - uses: maxvojtkov/dokploy-deploy-action@v1 with: project: shop service: worker action: redeploy ``` ## Other actions `action` selects the operation. `deploy` and `redeploy` queue a build and are followed to completion; the rest are immediate. | `action` | Effect | | --- | --- | | `deploy` (default) | Build and deploy. | | `redeploy` | Rebuild and deploy, without changing the source. | | `start` | Start a stopped service. | | `stop` | Stop a running service. | | `reload` | Recreate the container from the current image. Applications only. | ## Inputs Everything is optional; the required combinations are described above. ### Connection | Input | Default | Description | | --- | --- | --- | | `host` | `$DOKPLOY_HOST` | Instance URL, e.g. `https://dokploy.example.com`. A trailing `/api` is tolerated. | | `api-key` | `$DOKPLOY_API_KEY` | Token from Settings → Profile → API/CLI. | | `insecure` | `false` | Skip TLS verification. Self-signed certificates only. | | `request-timeout` | `60` | Seconds before a single API request is abandoned. | ### Target | Input | Default | Description | | --- | --- | --- | | `application-id` | | Application to act on. | | `compose-id` | | Compose stack to act on. | | `project` | | Project name, for lookup by name. | | `service` | | Application or stack name, for lookup by name. | | `environment` | project default | Environment name. | ### Deployment | Input | Default | Description | | --- | --- | --- | | `action` | `deploy` | `deploy`, `redeploy`, `start`, `stop` or `reload`. | | `docker-image` | | Image to deploy. Applications only. | | `registry-username` | | Registry username, alongside `docker-image`. | | `registry-password` | | Registry password or token. Masked in the log. | | `registry-url` | | Registry URL. | | `title` | short commit SHA | Deployment title shown in Dokploy. | | `description` | repo, ref, actor, run link | Deployment description. | ### Waiting | Input | Default | Description | | --- | --- | --- | | `wait` | `true` | Follow the deployment and fail the step if it errors. | | `timeout` | `600` | Seconds to wait for the deployment to finish. | | `poll-interval` | `5` | Seconds between status checks. | | `cancel-on-timeout` | `false` | Ask Dokploy to cancel the build when the timeout is hit. | | `logs` | `on-failure` | `on-failure`, `always` or `never`. | | `log-tail` | `200` | Number of log lines to print. | ## Outputs | Output | Description | | --- | --- | | `status` | `done`, `error`, `cancelled`, `timed-out`, or `triggered` when `wait: false`. | | `succeeded` | `"true"` or `"false"`. | | `deployment-id` | The deployment this run started. | | `service-kind` | `application` or `compose`. | | `service-id` | Resolved service id. | | `application-id` | Resolved application id; empty for a stack. | | `compose-id` | Resolved stack id; empty for an application. | | `app-name` | Docker service name. | | `duration` | Seconds spent triggering and waiting. | ## How it decides the deployment failed `application.deploy` is fire-and-forget: it puts a job on Dokploy's queue and returns immediately. Reporting on that alone would mean every deployment "succeeds". So the step: 1. Lists the service's existing deployments. 2. Triggers the action. 3. Polls until a deployment appears that was not in step 1, and follows it to `done`, `error` or `cancelled`. Because the snapshot is taken first, a deployment somebody else started is never mistaken for this one. `done` passes; anything else fails the step and prints the build log. If `timeout` elapses first, the step fails with `status: timed-out` and the build keeps running on Dokploy — set `cancel-on-timeout: true` to stop it instead. Give `timeout` room: it covers the queue wait as well as the build. Set `wait: false` to go back to fire-and-forget. The step then reports `triggered` and cannot tell you whether the deployment worked. ## Troubleshooting **`Host must start with http:// or https://`** — `host` needs a scheme. **`dokploy API error (HTTP 401) UNAUTHORIZED`** — the token is wrong, expired, or belongs to a different organization than the project. **`... returned a non-JSON response; is host pointing at a Dokploy instance?`** — a reverse proxy answered instead of Dokploy. Check the host, and that `/api` is reachable from the runner. **`No deployment appeared within Ns`** — the trigger was accepted but no build started. Usually Dokploy's queue is stuck or the target server is unreachable; check the service in the UI. **A self-hosted Dokploy behind a private network** — the runner needs to reach it. Either use a self-hosted runner or expose the API. **A self-signed certificate** — `insecure: true`. It disables verification for the whole step, so prefer a real certificate. ## Development No dependencies, no build step, no committed `dist/`. Node 20 or newer. ```bash node --test # 47 unit tests, no network ``` `src/client.js` is the API client, `src/resolve.js` turns names into ids, `src/deploy.js` triggers and follows a deployment, `src/core.js` is a small stand-in for `@actions/core`, and `src/index.js` wires them together. The action is deliberately dependency-free so both runners can execute it straight from the repository — CI enforces that. Releases: tag `vX.Y.Z`, and the release workflow moves the `vX` tag that workflows pin. ## Related Same Dokploy API, different jobs: - [terraform-provider-dokploy](https://github.com/maxvojtkov/terraform-provider-dokploy) — declare projects, applications, databases and domains. - [pulumi-dokploy](https://github.com/maxvojtkov/pulumi-dokploy) — the same, in TypeScript, Python, Go or .NET. The usual division of labour: the provider creates the service and owns its configuration, this action deploys code into it. Pass the provider's `application_id` output straight into `application-id` here. ## License [MPL-2.0](LICENSE).