Dockflow

Notifications

Send a signed JSON webhook when a deploy, a rollback or another change to an environment finishes.

Use notify: to post to a chat, open an incident, or record deploys in another system. It lists webhooks that Dockflow calls when a run that changes an environment ends, such as a deploy, a rollback or a restart.

Each call is an HTTP POST with a JSON body. When you give the webhook a secret, the body is signed with HMAC-SHA256.

Example

dockflow.yml
version: 1
app: shop
notify:
  - url: https://hooks.example.com/dockflow
    secret: ${WEBHOOK_SECRET}
  - url: https://alerts.example.com/hooks/deploys
    on: [failure]
    headers:
      Authorization: Bearer ${ALERTS_TOKEN}
    timeout: 5s
environments:
  production: {}

The first webhook receives every outcome, signed with the WEBHOOK_SECRET secret (PRODUCTION_WEBHOOK_SECRET in CI for production). The second receives failures only, with an extra header.

Options

KeyTypeDefaultDescription
urlstringrequiredAn http or https URL
onlistbothsuccess, failure, or both: the statuses this webhook receives
secretstringnoneSigns the body: the request carries X-Dockflow-Signature
headersmapnoneExtra request headers
timeoutduration10sHow long to wait for the response

String values are interpolated like the rest of dockflow.yml, so ${WEBHOOK_SECRET} reads a secret. A missing secret stops the command with exit code 3. Secret values are masked wherever Dockflow prints them.

notify: can also be set inside an environment. There it replaces the top-level list for that environment.

When webhooks fire

A webhook fires at the end of a run of the agent on the server, for these operations: deploy, rollback, restart, stop, scale, destroy, and maintenance on and off. It does not fire:

  • when the run changed nothing (a deploy whose plan is empty);
  • when the run could not start because another run holds the lock;
  • for failures before the agent runs: a failed build, a fatal before_* local or host hook, an invalid configuration;
  • for read commands (status, plan, logs…) and for prune.

When a local after_deploy hook fails and triggers a rollback, two webhooks are sent: one for the deploy (success), and one for the rollback run that follows.

The CLI sends the webhooks, after the agent reports the result. If the CLI is stopped before that, they are not sent.

Request

POST /dockflow HTTP/1.1
Content-Type: application/json
User-Agent: dockflow
X-Dockflow-Event: run.finished
X-Dockflow-Signature: sha256=5d1e9c…

Your headers are added after these, and replace one of them when they share a name.

Payload

{
  "event": "run.finished",
  "app": "shop",
  "env": "production",
  "server": "main",
  "op": "deploy",
  "run": "01JBF3K9Q2M8XW4T6Y7Z1N5R0C",
  "version": "1.4.0",
  "release": 42,
  "outcome": "rolled_back",
  "status": "failure",
  "exit_code": 8,
  "duration_ms": 73120,
  "error": "service web is unhealthy: …",
  "at": "2026-10-26T14:03:12.481Z"
}
FieldDescription
eventAlways run.finished
app, envThe app and the environment
serverThe server's name (main unless the environment names it)
opdeploy, rollback, restart, stop, scale, destroy, maintenance.on or maintenance.off
runThe run's ID, for dockflow attach <env> <run-id>
versionThe version label the CLI computed for the run (--version, the CI tag, or the commit). For a deploy, the release's version. For other operations, the version of the local checkout, which is not necessarily what runs (dockflow releases shows what runs)
releaseThe release number the run created; absent when it created none
outcomesuccess, rolled_back, failed, cancelled, lock_lost or hook_failed (see events)
statussuccess when outcome is success, failure otherwise. This is what on filters on
exit_codeThe CLI's exit code for this outcome
duration_msHow long the run took
errorThe error message, with secret values masked; absent on success
atWhen the webhook was sent, in UTC

Verifying the signature

With a secret, the X-Dockflow-Signature header is sha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with the secret. To verify it:

  1. Read the body as bytes, before any JSON parsing.
  2. Compute the HMAC-SHA256 of those bytes with the same secret.
  3. Compare it with the header in constant time, and reject the request when they differ.
server.mjs
import { createServer } from 'node:http';
import { createHmac, timingSafeEqual } from 'node:crypto';

const secret = process.env.WEBHOOK_SECRET;

function valid(body, header) {
  const expected = 'sha256=' + createHmac('sha256', secret).update(body).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(header ?? '');
  return a.length === b.length && timingSafeEqual(a, b);
}

createServer((req, res) => {
  const chunks = [];
  req.on('data', (c) => chunks.push(c));
  req.on('end', () => {
    const body = Buffer.concat(chunks);
    if (!valid(body, req.headers['x-dockflow-signature'])) {
      res.writeHead(401).end();
      return;
    }
    const p = JSON.parse(body);
    console.log(`${p.app} ${p.env}: ${p.op} ${p.status} (${p.version})`);
    res.writeHead(204).end();
  });
}).listen(8000);

The payload has no unique delivery ID. To ignore replays, check that at is recent, or keep the run IDs you have already processed.

Failures

Each webhook is called once per run, one after the other.

A webhook that cannot be reached, times out, or answers with a status of 300 or more is reported as a warning (notify.failed in --json output). It is not retried, and it never changes the outcome or the exit code of the run.

Redirects are not followed, so the payload, its signature and your headers reach only the URL you wrote. A webhook that answers with a redirect fails, and the warning names where it pointed: write that URL in url.

Edit on GitHub

On this page