Files
dokploy-deploy-action/README.md
Max Vojtkov c23b44983b Deploy Dokploy applications from GitHub and Gitea Actions
A dependency-free node20 action that triggers application.deploy or
compose.deploy, then follows the deployment to a terminal status so a
failed build fails the CI run. Snapshots the deployment list before
triggering, so a concurrent deployment is never mistaken for this one.

Optionally points the application at a freshly built image first, which
is what lets a workflow lint, test, push and deploy the same artifact.
2026-08-09 03:37:35 +03:00

11 KiB

dokploy-deploy-action

Trigger a Dokploy 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.

- 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?

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.
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:

    env:
      DOKPLOY_HOST: ${{ secrets.DOKPLOY_HOST }}
      DOKPLOY_API_KEY: ${{ secrets.DOKPLOY_API_KEY }}

Worked examples, from lint through image build to deploy:

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:

- uses: maxvojtkov/dokploy-deploy-action@v1

Otherwise give the full URL, or mirror this repository into your instance:

- uses: https://github.com/maxvojtkov/dokploy-deploy-action@v1

Choosing the service

Either name the service, or give an id.

# 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.

# 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.

- 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:

- 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 certificateinsecure: 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.

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.

Same Dokploy API, different jobs:

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.