Dockflow

JSON events

The event stream dockflow prints with --json - the envelope, every event type and its data, run outcomes, guarantees and an example.

Every operation reports its progress as a stream of events. The agent on the server writes them and the CLI relays them. With --json, the CLI prints them as they are: one JSON object per line (JSON Lines). Use this stream to drive a CI job, a dashboard or a bot without parsing human output.

dockflow deploy production --json > deploy.jsonl

This page describes the envelope every line shares, then every event type, the outcomes of a run, and what stays stable between versions. It mirrors the events specification.

Which commands print events:

  • Events. The commands that change something: plan, deploy, rollback, restart, stop, scale, destroy, cancel, attach, build, backup run, backup restore, backup prune, secrets revoke, and the server commands that change the server (init, edge, upgrade, uninstall, network create|remove, plugin install|remove).
  • A single JSON document. The read commands: init, validate, config show, status, releases, version, ps, details, diagnose, list envs|services|images, lock, prune, backup list, secrets list, plugins list, server status, server check, server network list, server plugin list, server connection.

Envelope

Every line has the same fields:

{"v":1,"ts":"2026-10-26T14:03:11.123Z","run":"01jbx6k3m8w0v7c9q2r5t4y6z8","seq":17,"type":"service.healthy","level":"info","msg":"web: 2/2 healthy","data":{"desired":2,"healthy":2,"service":"web"}}
FieldTypeDescription
vintegerThe schema version, 1
tsstringWhen the event was emitted, in UTC, RFC 3339 with millisecond precision, by the clock of the machine that emitted it (the server for the agent's events, your machine for the CLI's)
runstringThe id of the run that emitted the event. Events from the agent carry the agent's run id, the one dockflow attach takes. Events the CLI emits itself (builds, pushes, local hooks) carry the id of the CLI invocation. The watchdog's events carry watchdog.
seqinteger1, 2, 3, … within the stream you read. A gap means lost events
typestringOne of the types below
levelstringdebug, info, warn or error
msgstringA human-readable sentence. You may show it as is
dataobjectThe fields of the type. Absent when the type has none

With --json, every event is printed, debug ones included. Without it, debug events are shown only with -v.

Event types

Fields marked ? are present only when they apply.

Run

TypeLeveldata
run.startedinfoapp, env, op, dockflow_version, server
run.finishedinfo or erroroutcome, exit_code, release_seq?, duration_ms, error?; see below
resultinfoThe document of a read operation, as the agent returns it. The CLI prints it alone, so you rarely see this type in a stream
config.loadeddebugfiles (the Compose files, dockflow.yml and the secrets file), services: emitted once connected, by the commands that reach the server
cancel.requestedwarnby: signal (this version emits no other value; cancel is reserved)
loginfo or debugline: anything else worth showing (an image pull, the agent being installed, files received)

op is the operation the agent runs, a dotted name:

Groupop
An environmentplan, deploy, rollback, restart, stop, scale, destroy, prune, details, status, releases, upload.have (which files a deploy must send), secrets.revoke
Its locklock.acquire, lock.release, lock.status
Backupsbackup.run, backup.list, backup.restore, backup.prune
The serverserver.init (also for server upgrade and server edge), server.status, server.check, server.uninstall, server.cloudflare.refresh (the weekly refresh of Cloudflare's ranges), server.network.create, server.network.list, server.network.remove, server.plugin.install, server.plugin.list, server.plugin.remove

New operations may appear in a minor release: treat an op you do not know as a name to show, not as an error.

Warning

warning (level warn, or error for a failed service in backup run) reports something that does not stop the run. Its data has a stable code, usually a detail, and service when it concerns one:

{"v":1,"ts":"2026-10-26T14:03:12.5Z","run":"01jbx6k3m8w0v7c9q2r5t4y6z8","seq":9,"type":"warning","level":"warn","msg":"webhook https://hooks.example.com/dockflow: status 500","data":{"code":"notify.failed"}}

The codes are listed under warning codes.

Lock

TypeLeveldata
lock.waitinginfoholder (its run id), op, by, since: emitted once, when --wait-lock starts waiting for another run
lock.acquiredinfoscope: the stack (<app>-<env>)
lock.taken_overwarnscope (manual), previous_holder: dockflow lock acquire --force took over someone else's manual lock
lock.releasedinfoscope

Build and push

These events come from the CLI, before the agent starts.

TypeLeveldata
build.startedinfoservices, platforms
build.logdebugservice, line: one line of build output. When a build fails, its last lines are emitted again at level error, with line only
build.finishedinfoservice, digest
push.progressdebugimage, bytes_sent, bytes_total: at most once a second per image, and once at the end
push.finishedinfoimage (pinned by digest), digest, blobs_sent, blobs_skipped. An image built on the server has image and digest only

Plan and release

TypeLeveldata
plan.computedinfoThe plan: app, env, fingerprint, summary (create, update, remove), actions (each with id, kind, target, reason, strategy?), unchanged? (services left as they are), warnings?
plan.emptyinfoNone: there is nothing to change
release.createdinfoseq, version

An action's kind is one of network.create, volume.create, secret.create, config.create, service.create, service.update, service.remove, routes.apply, ports.apply, job.run and upload.apply. Its id is stable within a plan, for example 01-service.update-web, and strategy is switch, start-first or stop-first.

Actions and containers

TypeLeveldata
action.startedinfoaction: the action's id
action.finishedinfoaction, duration_ms
action.failederroraction, error (code, message, hint?)
container.statedebugservice, container, release, state: created, running, healthy or exited
service.healthyinfoservice, healthy, desired
service.failederrorservice, reason: crash_loop, exited, unhealthy, timeout or port_in_use
health.checkinfo or debugservice, url, attempt, ok, status?: a passing HTTP check at info, each failed attempt at debug

Switch and routes

TypeLeveldata
switch.changedinfoservice, from_release, to_release, routes (each domain followed by its path)
drain.startedinfoservice, release (the old one), timeout_ms
routes.applieddebugdomains
backup.finishedinfoservice, or server for the server's state, snapshot (restic's short id), bytes_added (new data stored), duration_ms
filter.appliedinfoports: what the port filter lets in, each {port, protocol, from, why}; why is SSH, the switch or the app that lists it

Jobs, hooks and uploads

TypeLeveldata
job.loginfoservice, line
job.finishedinfoservice, exit_code
hook.startedinfophase, name, kind (local, host or exec), target
hook.loginfoname, target, line
hook.finishedinfoname, target, exit_code, fatal
upload.progressdebugfiles_sent, files_total, bytes_sent, bytes_total: from the CLI, at most once a second and once at the end. It counts the files of every destination, sent together: only those the server lacks. A dest field is reserved; this version does not set it
upload.finishedinfodest, files_changed, files_deleted

A hook's target is local for a local hook, server for a host hook, and the container's name for an exec hook, which emits one hook.started and hook.finished per container.

Rollback

TypeLeveldata
rollback.startedwarnreason, target_seq: the release being restored. When a fatal after_deploy hook triggers the rollback, reason only
rollback.finishedinfook: whether the previous containers and routes are back

Watchdog

TypeLeveldata
watchdog.restartedwarnstack, service, container, reason: unhealthy, or boot for an app with Compose secrets started again after a reboot. In the watchdog's own output (docker logs dockflow-watchdog), with run set to watchdog

Reserved types

Typedata
lock.lostscope, new_holderNever emitted: the lock is held until the agent exits, so it cannot be lost while its holder lives. The outcome lock_lost is reserved for the same reason

run.finished

The agent's last event says how its run ended:

FieldTypeDescription
outcomestringSee the table below
exit_codeintegerThe CLI's exit code for this outcome
release_seqintegerThe release the run created or restored. Absent when there is none
duration_msintegerHow long the run took
errorobjectcode, message and hint (when there is one). Absent on success
OutcomeExit codeMeaning
success0The run did what it was asked
noop0There was nothing to change
rolled_back8The run failed, and the previous release is running
failed9, or the error's own codeThe run failed. After a release was created, the environment is left degraded (exit code 9); before, nothing changed, and exit_code is the error's (5 for a held lock, 6 for a refused plan, …)
cancelled130Ctrl+C or dockflow cancel stopped the run, and it was rolled back
hook_failed11A fatal hook failed, and the run was rolled back
lock_lost10Reserved: this version never reports it
{"v":1,"ts":"2026-10-26T14:04:02.311Z","run":"01jbx6k3m8w0v7c9q2r5t4y6z8","seq":31,"type":"run.finished","level":"error","msg":"service web did not become healthy: restarted 3 times (last exit code 1)","data":{"outcome":"rolled_back","exit_code":8,"release_seq":43,"duration_ms":41870,"error":{"code":"health.failed","message":"service web did not become healthy: restarted 3 times (last exit code 1)","hint":"look at its logs with `dockflow logs web`"}}}

Guarantees

  • Order. Events are printed in the order they happen, and seq grows by one per line. The CLI renumbers the agent's events into its own sequence, so the stream you read has no gap unless lines were lost.
  • One agent run, one run.finished. Each agent run starts with run.started and ends with run.finished. A stream with run.started and no run.finished was cut off; the run may still be going on, and dockflow attach follows it.
  • Events around the agent's run. In a deploy, the CLI's own events come before the agent's run (builds, pushes, before_* hooks, files sent) and after it (after_deploy and after_failure hooks, webhook warnings). Read the outcome from run.finished, and the final result from the process's exit code.
  • Errors before the agent. When a command fails before the agent starts (an invalid configuration, a failed build, a refused connection), there is no run.finished: the last line is the error document {"error":{…},"exit_code":N} described in error format.
  • Masking. No event carries a secret value. Every occurrence of a secret's value is replaced with *** in msg and in data, before the line is written. Values shorter than 4 characters are not masked, because masking them would hide ordinary words.
  • Compatibility. Within v: 1, fields and types are only added, never renamed or removed, and their meaning does not change. Ignore types and fields you do not know.
  • Open enumerations. A minor release may add values to level, outcome, a container.state's state, a service.failed's reason, an action's kind, op, and error and warning codes. Treat a value you do not know as opaque: show it, but do not act on it. Count an unknown level as info. A new outcome always comes with an existing exit code; new exit codes only come with a major release.
  • What is stable. These rules cover the event stream, the exit codes and the error document. They do not cover the single JSON documents that read commands print (status, releases, config show, server status, server connection, …): a minor release may add, rename or remove their fields. Scripts should rely on the event stream and the exit codes.

dockflow exec, dockflow run and dockflow ssh connect your terminal to the command they run. Their output is the command's own: it is neither an event stream nor masked.

Example

A deploy of a routed service web, updated with the switch strategy, with --json:

{"v":1,"ts":"2026-10-26T14:03:10.402Z","run":"01jbx6k1z3p4q5r6s7t8v9w0x1","seq":1,"type":"log","level":"info","msg":"uploading 2 of 14 files","data":{"line":"upload"}}
{"v":1,"ts":"2026-10-26T14:03:11.123Z","run":"01jbx6k3m8w0v7c9q2r5t4y6z8","seq":2,"type":"run.started","level":"info","msg":"deploy shop-production","data":{"app":"shop","dockflow_version":"1.0.0-alpha.6","env":"production","op":"deploy","server":"main"}}
{"v":1,"ts":"2026-10-26T14:03:11.130Z","run":"01jbx6k3m8w0v7c9q2r5t4y6z8","seq":3,"type":"lock.acquired","level":"info","msg":"lock acquired","data":{"scope":"shop-production"}}
{"v":1,"ts":"2026-10-26T14:03:11.402Z","run":"01jbx6k3m8w0v7c9q2r5t4y6z8","seq":4,"type":"plan.computed","level":"info","msg":"0 to create, 1 to update, 0 to remove","data":{"app":"shop","env":"production","fingerprint":"sha256:a3b22ab3…","actions":[{"id":"01-service.update-web","kind":"service.update","target":"web","reason":"image web@sha256:bbbb → web@sha256:cccc","strategy":"switch"}],"unchanged":["db"],"summary":{"create":0,"update":1,"remove":0}}}
{"v":1,"ts":"2026-10-26T14:03:11.410Z","run":"01jbx6k3m8w0v7c9q2r5t4y6z8","seq":5,"type":"release.created","level":"info","msg":"release 43 (a1b2c3d)","data":{"seq":43,"version":"a1b2c3d"}}
{"v":1,"ts":"2026-10-26T14:03:11.412Z","run":"01jbx6k3m8w0v7c9q2r5t4y6z8","seq":6,"type":"action.started","level":"info","msg":"updating web (switch)","data":{"action":"01-service.update-web"}}
{"v":1,"ts":"2026-10-26T14:03:11.650Z","run":"01jbx6k3m8w0v7c9q2r5t4y6z8","seq":7,"type":"container.state","level":"debug","msg":"shop-production-web-43-1 created","data":{"container":"shop-production-web-43-1","release":43,"service":"web","state":"created"}}
{"v":1,"ts":"2026-10-26T14:03:11.981Z","run":"01jbx6k3m8w0v7c9q2r5t4y6z8","seq":8,"type":"container.state","level":"debug","msg":"shop-production-web-43-1 running","data":{"container":"shop-production-web-43-1","release":43,"service":"web","state":"running"}}
{"v":1,"ts":"2026-10-26T14:03:16.204Z","run":"01jbx6k3m8w0v7c9q2r5t4y6z8","seq":9,"type":"container.state","level":"debug","msg":"shop-production-web-43-1 healthy","data":{"container":"shop-production-web-43-1","release":43,"service":"web","state":"healthy"}}
{"v":1,"ts":"2026-10-26T14:03:16.205Z","run":"01jbx6k3m8w0v7c9q2r5t4y6z8","seq":10,"type":"service.healthy","level":"info","msg":"web: 1/1 healthy","data":{"desired":1,"healthy":1,"service":"web"}}
{"v":1,"ts":"2026-10-26T14:03:16.231Z","run":"01jbx6k3m8w0v7c9q2r5t4y6z8","seq":11,"type":"routes.applied","level":"debug","msg":"routes applied","data":{"domains":["shop.example.com"]}}
{"v":1,"ts":"2026-10-26T14:03:16.232Z","run":"01jbx6k3m8w0v7c9q2r5t4y6z8","seq":12,"type":"switch.changed","level":"info","msg":"web: traffic now goes to release 43","data":{"from_release":42,"routes":["shop.example.com/"],"service":"web","to_release":43}}
{"v":1,"ts":"2026-10-26T14:03:16.233Z","run":"01jbx6k3m8w0v7c9q2r5t4y6z8","seq":13,"type":"action.finished","level":"info","msg":"service.update web done","data":{"action":"01-service.update-web","duration_ms":4821}}
{"v":1,"ts":"2026-10-26T14:03:16.234Z","run":"01jbx6k3m8w0v7c9q2r5t4y6z8","seq":14,"type":"drain.started","level":"info","msg":"web: draining release 42 for 30s","data":{"release":42,"service":"web","timeout_ms":30000}}
{"v":1,"ts":"2026-10-26T14:03:47.011Z","run":"01jbx6k3m8w0v7c9q2r5t4y6z8","seq":15,"type":"lock.released","level":"info","msg":"lock released","data":{"scope":"shop-production"}}
{"v":1,"ts":"2026-10-26T14:03:47.012Z","run":"01jbx6k3m8w0v7c9q2r5t4y6z8","seq":16,"type":"run.finished","level":"info","msg":"release 43 is live","data":{"duration_ms":35889,"exit_code":0,"outcome":"success","release_seq":43}}

To follow a deploy in a script, read the lines as they arrive and stop at run.finished:

dockflow deploy production --json | jq -r 'select(.type == "run.finished") | "\(.data.outcome) \(.data.exit_code)"'
Edit on GitHub

On this page