docket has eight commands. Running docket with no subcommand prints the list:
| Command | What it does |
|---|---|
docket init |
Scaffold a starter recipe. |
docket validate |
Check a recipe offline, without contacting a server. |
docket fmt |
Canonically format a recipe. |
docket plan |
Preview what apply would change. |
docket apply |
Run the recipe, making changes as needed. |
docket export |
Read a live server and write a recipe describing it. |
docket schema |
Print the machine-readable catalog of task types. |
docket version |
Print the binary's version. |
apply, plan, validate, and fmt all accept either YAML or JSON5 recipes. When --tasks is
omitted they probe tasks.yml, then tasks.yaml, then tasks.json, and use the first that exists.
A directory holding more than one of them is ambiguous, so the probe names the one it took in a
warning on stderr - tasks.yml, tasks.json both exist; using tasks.yml (pass --tasks to choose) -
leaving stdout to the command's own output.
apply, plan, and validate also accept the recipe as a single positional argument (for example
docket validate staging/tasks.yml); passing both a positional path and --tasks, or more than one
positional path, is an error. Naming the recipe either way silences the warning.
All four also read the recipe from stdin when the path is -, so a recipe generated by another
tool can be piped straight in. On apply, plan, and validate that is spelled either
--tasks - or as a bare positional -; fmt takes the positional form. Any inputs: the piped
recipe declares still become real --<name> flags, and a - recipe takes precedence over a
tasks.yml sitting in the working directory:
docket export --output - | docket apply -
docket init --output - | docket validate -
cat tasks.yml | docket plan --tasks - --app api
docket export --output - --format json5 | docket apply --tasks-format json5 -The format normally comes from the file extension, and for stdin from the first non-whitespace byte
([, {, //, or /* means JSON5; anything else means YAML). --tasks-format yaml|json5
overrides both. Reach for it when the extension is absent or misleading (--tasks recipe.txt, or a
URL whose path carries no extension), or when a YAML recipe written in flow style would be sniffed
as JSON5 because it opens with [.
--tasks-format is the reading side. On the writing side, init and export take
--format yaml|json5 to state the format of what they emit. Without it, stdout can only ever be
YAML, since there is no extension to infer from.
Reading the recipe from stdin consumes it, so a dokku command that would otherwise have inherited
the terminal's stdin sees end-of-file instead. No task depends on this - every task that streams
data to dokku supplies it explicitly.
docket init writes a starter recipe from a built-in template. It is offline only: no server
contact and no git subprocess. The default scaffold ships four tasks (dokku_app,
dokku_config, dokku_domains, dokku_git_sync) in a single play with app and repo inputs,
and round-trips cleanly through docket validate.
The output format follows --format when given, otherwise the --output extension: .json /
.json5 writes a JSON5 scaffold with // ... comments, anything else writes YAML. Streaming to
stdout (--output -) has no extension to read, so it writes YAML unless --format json5 says
otherwise. Passing --format json5 without an --output writes ./tasks.json rather than a JSON5
document under a .yml name.
# Use the current directory name as the app and remote.origin.url as the repo.
docket init
# Same scaffold in JSON5, written to ./tasks.json.
docket init --format json5
# Stream the scaffold to stdout for piping.
docket init --output -
# Stream a JSON5 scaffold to stdout.
docket init --output - --format json5init closes by printing the commands to run next. Those run without --tasks and so rely on the
default probe, which only reaches the new scaffold when it was written to tasks.yml. Written
anywhere else (tasks.json from --format json5, or an --output in another directory) the
printed commands name the file, so they cannot end up describing a stale recipe sitting beside it:
$ docket init --format json5
==> Created tasks.json (4 tasks, 1 play)
Next steps:
$ docket validate --tasks tasks.json # offline check
$ docket plan --tasks tasks.json # preview against the server
$ docket apply --tasks tasks.json # apply
| Flag | Effect |
|---|---|
| (default) | Write ./tasks.yml; refuse if it already exists. |
--output <path> |
Write to a path; - writes to stdout. Format inferred from the extension unless --format says otherwise. A stream touches no file on disk, so combining - with --force is an error rather than a silently ignored flag. |
--format <fmt> |
Write yaml or json5 regardless of the --output extension. Without an explicit --output, --format json5 writes ./tasks.json. |
--force |
Overwrite an existing file. Not valid with --output -. |
--name <name> |
Set the play and app input default (defaults to the directory name). |
--repo <url> |
Set the repo input default (defaults to remote.origin.url in ./.git/config). |
--minimal |
A one-task example with no inputs: block. |
docket validate performs offline schema and template checks on a recipe without contacting any
Dokku server. It is built for CI lint jobs that should reject a broken recipe before it reaches a
deploy.
The checks cover: the file parses; the recipe shape is a list of plays; each task entry has the
right envelope keys plus exactly one task-type key; the task type is registered (with a "did you
mean" for typos); required fields decode; a task's conditional/semantic input rules hold (for
example a list that must be non-empty when state: present, or mutually-exclusive fields) - the same
checks plan and apply run, surfaced offline as invalid_task_input; each input name is a valid
{{ .name }} template variable (a hyphenated name is rejected as invalid_input_name); sigil
templates render against the input defaults, and an input value that would break the scalar it is
substituted into is reported as unsafe_input_value (naming the input) rather than a cryptic YAML
error - see special characters in values; expr predicates
parse; and .item / .index are only used inside a loop:.
docket validate --tasks path/to/tasks.ymlExit code is 0 when no problems are found, 1 otherwise.
| Flag | Effect |
|---|---|
--tasks <path> |
Use a specific recipe. Pass - to read it from stdin. |
- |
Read the recipe from stdin, as a positional argument. |
--tasks-format <fmt> |
Parse the recipe as yaml or json5 instead of detecting it. |
--json |
Emit one JSON-lines problem per line with a stable schema, for a CI annotator. |
--strict |
Also flag any required: true input with no default and no supplied value, and verify that --play / --start-at-task references resolve to real names. |
--vars-file <path> |
Load input values from a YAML or JSON file (repeatable). Values here count as overrides for --strict. See inputs. |
--play <name> |
(strict only) Verify the named play exists. |
--start-at-task <name> |
(strict only) Verify a task with this name exists; narrowed by --play. |
docket fmt is a canonical formatter for recipes, in the spirit of gofmt. It reorders task and
play keys into a stable order, normalizes indentation to a 2-space step, and inserts blank lines
between top-level plays and task entries. It works on both YAML and JSON5, detected per file from
the extension, and both formats share the same canonical key order so a YAML recipe and its JSON5
twin lay out identically. Comments are preserved in both formats.
# Rewrite ./tasks.yml in place (probes tasks.yml -> tasks.yaml -> tasks.json with no argument).
docket fmt
# Format a JSON5 recipe in place.
docket fmt tasks.json
# CI gate: print the diff and exit 1 if anything is not canonical.
docket fmt --check --diff
# Read from stdin, write canonical to stdout (format sniffed from the first byte).
cat tasks.yml | docket fmt -
# Same, but keep a flow-style YAML recipe as YAML instead of sniffing it as JSON5.
cat tasks.yml | docket fmt --tasks-format yaml -| Flag | Effect |
|---|---|
| (default) | Format the file in place; a no-op when it is already canonical. |
--check |
Exit 1 if any file is not canonical; no writes. Composes with --diff. |
--diff |
Print a unified diff against canonical; no writes. Composes with --check. |
--color <when> |
Colorize the diff: auto (default), always, or never. |
--tasks-format <fmt> |
Format the recipe as yaml or json5 instead of detecting it. |
- |
Read from stdin, write canonical to stdout. Cannot be combined with other paths. |
<path...> |
Format the named files; each argument is expanded as a glob. |
The diff is a standard unified diff (--- / +++ / @@ headers) and applies with git apply or
patch -p0 once colors are stripped. Before writing, fmt re-parses its own output and aborts if
the result does not match the input, so a formatting bug can never corrupt a recipe.
fmt operates on single-document recipes. An empty or comment-only file is left untouched, and a
YAML file containing more than one document (separated by ---) is rejected rather than having its
trailing documents silently dropped.
docket plan reads each task's current state from the live server and reports what apply would
change, without running any mutating command. The output uses the same play header and column
layout as apply, with a marker set focused on drift:
| Marker | Meaning |
|---|---|
[ok] |
In sync; apply would change nothing. |
[+] |
apply would create new state. |
[~] |
apply would modify existing state. |
[-] |
apply would remove existing state. |
[!] |
The read-state probe itself errored, so drift is unknown. |
A task may also be preceded by an informational [deprecated] or [warning] line (a task-type
deprecation notice, or a property probe diagnostic such as an unknown report key). These do not
count toward the summary or the exit code.
Tasks that perform several operations itemize them under the task line:
==> Play: tasks
[~] configure (2 key(s) to set)
- set KEY_ONE (new)
- set KEY_TWO (was set)
Plan: 1 task(s); 1 would change, 0 in sync, 0 error(s).
The same probe drives apply: each task reads the server once, and apply reuses that read to
decide whether to mutate. How much of its own state a task can read is a per-task property: each
task's reference page carries a Probe support section stating whether it is supported, partial
(some fields have no read command), or not supported. A task that is not supported reports drift on
every run - it can never converge, so a recipe containing one never exits 0 under
--detailed-exitcode, no matter how many times you apply it. The tasks index
marks those with (never converges), and --list-tasks marks them per recipe (see
Inspecting and resuming). Gate a deploy on the rest by moving those
tasks behind a tag and planning with --skip-tags.
When the probe command cannot run at all - for example the local dokku CLI is not installed, or
the configured SSH host is unreachable - the task renders [!] and plan exits 1, rather than
optimistically predicting [+] create for state it never actually read.
| Flag | Effect |
|---|---|
--tasks <path> |
Use a specific recipe. Accepts a local path, an http(s):// URL (fetched over HTTP), or - for stdin. |
- |
Read the recipe from stdin, as a positional argument. |
--tasks-format <fmt> |
Parse the recipe as yaml or json5 instead of detecting it. |
--json |
Emit JSON-lines events instead of the human formatter. See JSON output. |
--detailed-exitcode |
Exit 0 for no drift, 2 for drift, 1 on error. Errors win over drift. Mirrors terraform plan -detailed-exitcode. |
--vars-file <path> |
Load input values from a file (repeatable). See inputs. |
--play <name> |
Plan only the named play. Composes with --tags. |
--tags <list> |
Plan only tasks whose tags intersect the list. See task envelope. |
--skip-tags <list> |
Skip tasks whose tags intersect the list. See task envelope. |
--list-tasks |
Print the resolved plan and exit without contacting the server. See inspecting and resuming. |
--host <user@host:port> |
Plan against a remote server over SSH. Overrides DOKKU_HOST. See remote execution. |
--sudo |
Wrap the remote dokku call in sudo -n. See remote execution. |
--accept-new-host-keys |
Trust an unknown SSH host key on first connect. See remote execution. |
# CI gate: fail the job if any task would change the server.
docket plan --detailed-exitcode || exit $?docket apply runs every task in the recipe, mutating the live server as needed. Each task line
gets a status marker:
| Marker | Meaning |
|---|---|
[ok] |
Ran, no change. |
[changed] |
Ran, changed state. |
[skipped] |
Filtered out by when: or --start-at-task. |
[error] |
Errored. |
As in plan, a task may be preceded by an informational [deprecated] or [warning] line that
does not count toward the summary or the exit code.
A play header precedes the task lines, and a summary closes the run:
==> Play: tasks
[changed] dokku apps:create api
[ok] dokku config:set api KEY=value
Summary: 2 tasks · 1 changed · 1 ok · 0 skipped · 0 errors (took 0.8s)
On error, the message prints on a !-prefixed line and the run aborts with exit 1 (unless
--fail-fast is off and only the play aborts). The
summary still prints with partial counts.
By default apply exits 0 whether or not anything changed, because "the server now matches the
recipe" is the same outcome either way. Pass --detailed-exitcode when the caller needs to know:
0 means nothing changed, 2 means at least one task changed, and 1 still means an error. Errors
win over changes, matching plan. An error swallowed by
ignore_errors is not an error for this
purpose. --list-tasks returns before any task runs, so it never exits 2; it exits 1 when the
recipe cannot be loaded or a when: fails to evaluate, and 0 otherwise.
| Flag | Effect |
|---|---|
--tasks <path> |
Use a specific recipe. Accepts a local path, an http(s):// URL (fetched over HTTP), or - for stdin. |
- |
Read the recipe from stdin, as a positional argument. |
--tasks-format <fmt> |
Parse the recipe as yaml or json5 instead of detecting it. |
--verbose |
After each task, echo every resolved Dokku command it ran, one per → line. Masked against sensitive values. Ignored with --json (which already includes commands). |
--json |
Emit JSON-lines events instead of the human formatter. See JSON output. |
--detailed-exitcode |
Exit 0 when nothing changed, 2 when something did, 1 on error. Errors win over changes. |
--vars-file <path> |
Load input values from a file (repeatable). See inputs. |
--play <name> |
Run only the named play. Composes with --tags. |
--tags <list> |
Run only tasks whose tags intersect the list. See task envelope. |
--skip-tags <list> |
Skip tasks whose tags intersect the list. See task envelope. |
--fail-fast |
Abort the whole run on the first error. Without it, an error aborts only the current play. |
--list-tasks |
Print the resolved plan and exit without running. See inspecting and resuming. |
--start-at-task <name> |
Skip every task before the named one, then run from there. See inspecting and resuming. |
--host <user@host:port> |
Apply against a remote server over SSH. Overrides DOKKU_HOST. See remote execution. |
--sudo |
Wrap the remote dokku call in sudo -n. See remote execution. |
--accept-new-host-keys |
Trust an unknown SSH host key on first connect. See remote execution. |
A multi-command task renders one continuation line per invocation under --verbose:
[changed] add buildpacks
→ dokku --quiet buildpacks:add app https://github.com/heroku/heroku-buildpack-nodejs.git
→ dokku --quiet buildpacks:add app https://github.com/heroku/heroku-buildpack-nginx.git
Color output respects NO_COLOR: set NO_COLOR=1 to disable ANSI escapes.
Output is also plain automatically when piped to a non-TTY.
Two flags help when a recipe grows long. --list-tasks previews the resolved plan without running,
and --start-at-task resumes a partially-applied recipe from a specific task. Both work on apply;
--list-tasks also works on plan.
--list-tasks walks the resolved plan - after --play / --tags filtering, after loop:
expansion, after when: evaluation against inputs - and prints one line per task:
$ docket apply --list-tasks
==> Play: api
[0] dokku apps:create api [tags=core]
[1] dokku git:sync api [tags=deploy]
[2] dokku config:set api [tags=core,deploy]
[3] dokku ports:add api [tags=deploy]
Those are the name: values from the recipe. A task with no name: is listed by its
resource address instead:
$ docket apply --list-tasks
==> Play: api
[0] dokku_app[app=api]
[1] dokku_config[app=api]
[2] dokku_docker_options[app=api,phase=deploy,option=--memory=512m]
A when: that is false against the inputs renders as [skipped]. A when: whose truth value
depends on a value only a run can supply renders as [unknown] rather than guessing: one that
references .registered.<name> cannot be decided without running earlier tasks, and a
rescue: child that branches on
failed_task cannot be decided without a block child having failed.
Everything else is evaluated, and a when: that fails to evaluate renders as [when?] and exits
1. Once the undecidable references are set aside, a predicate that errors here errors the same way
at run time - failed_task outside a rescue: child is nil during a real run too, so
dereferencing it there is a broken predicate, not an artifact of listing offline.
$ docket apply --list-tasks
==> Play: api
[when?] gated
[1] dokku apps:create api
$ echo $?
1
The walk is never short-circuited - every remaining task and play still lists, and the failure is
carried by the exit code alone. A play-level when: that fails to evaluate is the same, except that
the play's header carries the error and its tasks are left unlisted:
$ docket plan --list-tasks
==> Play: broken (when error: cannot fetch tag from <nil> (1:9)
| release.tag == "v1"
| ........^)
==> Play: api
[0] dokku apps:create api
$ echo $?
1
The one exception is reachability. A run never evaluates the children of a group whose own when:
rendered [skipped], [unknown], or [when?], so a broken predicate underneath one still prints
its [when?] marker but leaves the exit code alone - the same reason a skipped play's tasks are not
listed at all.
A task whose type is deprecated is marked (deprecated). A task whose type cannot read its own
state is marked (never converges), and one that can read only part of it is marked
(partial probe) - both come from the same declaration the reference pages render as Probe
support, so this is the way to find out which tasks in a recipe will report drift forever before
running anything:
$ docket apply --list-tasks
==> Play: api
[0] dokku apps:create api [tags=core]
[1] dokku git:auth github.com (never converges)
[2] dokku git:from-image api (partial probe)
With --json, the same information is a probe field (unsupported or partial) plus a
probe_caveat naming what cannot be read; a task that probes everything it manages carries neither.
--start-at-task <name> takes an exact task name:. Earlier tasks render as [skipped] with a
(before --start-at-task) reason and do not run; the matched task and everything after it run:
$ docket apply --start-at-task "dokku config:set api"
==> Play: api
[skipped] dokku apps:create api (before --start-at-task)
[skipped] dokku git:sync api (before --start-at-task)
[ok] dokku config:set api
[changed] dokku ports:add api
Summary: 4 tasks · 1 changed · 1 ok · 2 skipped · 0 errors (took 1.1s)
A task with no name: is targeted by its resource address. Quote it - the brackets are shell glob
characters:
docket apply --start-at-task 'dokku_config[app=api]'Filters apply in this order: --start-at-task selects first, then --tags / --skip-tags, then
per-task when: at execution time. The name search walks every play in source order, narrowed by
--play. An unmatched name exits 1 with the available names listed. A --start-at-task target that
is itself excluded by --tags / --skip-tags still establishes the resume point but does not itself
run - tasks after it that pass the tag filter do.
validate --strict --start-at-task checks the same names offline. It stands down for a play that
contains a loop: entry or an input with no default, because neither can be named without the
values a real run has; the check at apply time is the authoritative one.
docket export reads a live Dokku server and writes a recipe describing it - the inverse of
apply. It enumerates the apps on the server and reconstructs each one's declarative state, so you
can capture an existing server as a recipe instead of hand-writing one. This is the starting point
for a migration.
Because a faithful recipe would otherwise embed secrets, export writes two files: the recipe,
and a companion vars-file holding the sensitive values (every config value, plus any field a
task marks sensitive). The recipe references those values through per-play inputs: and
{{ .name }} templates, so the pair is applied together:
# Export the local server to tasks.yml + tasks.vars.yml.
docket export
# Export a remote server over SSH.
docket export --host deploy@dokku.example.com
# Apply the exported pair somewhere else.
docket apply --tasks tasks.yml --vars-file tasks.vars.yml
# Stream a JSON5 recipe to stdout and pipe it straight back in.
docket export --output - --format json5 | docket apply --tasks-format json5 -
# Read back a single resource instead of a whole app.
docket export --resource 'dokku_config[app=api]' --output -The correctness contract is idempotency: applying an exported pair back to the same server reports
no drift (plan shows every task [ok]).
--resource takes a resource address - the same
string an unnamed task is named after - and exports only what it matches:
docket export --resource 'dokku_config[app=api]' --output -
docket export --resource 'dokku_apps_property[global=true,property=disable-autocreation]' --output -Drop the brackets to take every resource of a type, across every app:
docket export --resource dokku_domains --output -The flag is repeatable and cannot be combined with --app, since an address already names its app.
Each address is checked against the registry before the server is read, so an unknown task type, a
type no exporter reaches, or a key the task does not declare fails immediately. An address that
matches nothing on the server is reported by name and exits non-zero, the same way a nonexistent
--app is.
| Flag | Effect |
|---|---|
--output <path> |
Where to write the recipe (default tasks.yml). Pass - to stream a single self-contained recipe (values inlined, no vars-file) to stdout for inspection. Because a stream has no vars-file and touches no file on disk, combining - with --vars-output or --overwrite is an error rather than a silently ignored flag. |
--format <fmt> |
Write yaml or json5 regardless of the --output extension, for both the recipe and the vars-file. Without an explicit --output, --format json5 writes ./tasks.json and ./tasks.vars.json. Required to stream JSON5 with --output -, which has no extension to read. |
--vars-output <path> |
Where to write the companion vars-file (default <output-base>.vars.<ext>, e.g. tasks.vars.yml). Not valid with --output -. When the server holds nothing sensitive there is no vars-file to write, and an explicit path is reported as unwritten rather than passed over. |
--overwrite |
Overwrite existing output files without prompting. Without it, export prompts before replacing either file, and aborts writing nothing if declined (or if stdin is not interactive). Not valid with --output -. |
--redact |
Write placeholder values into the vars-file instead of real secrets, producing a shareable recipe plus a fill-in-the-blanks vars template. The required inputs mean apply fails loudly until the vars-file is filled in. |
--app <name> |
Restrict the export to the named app. Repeatable. |
--resource <address> |
Restrict the export to a resource address, e.g. dokku_config[app=api]. A bare task type takes every resource of that type. Repeatable; not valid with --app. See exporting one resource. |
--host <user@host:port> |
Read a remote server over SSH. Overrides DOKKU_HOST. See remote execution. |
--sudo |
Wrap the remote dokku call in sudo -n. |
--accept-new-host-keys |
Trust an unknown SSH host key on first connect. |
The output format follows --format when given, otherwise the --output extension (.json /
.json5 writes JSON5, anything else YAML); the vars-file follows the recipe, or its own
--vars-output extension when --format is not given. Which task types export is a per-task property: each task's
reference page carries an Export support section stating whether it is supported, partial (for
example a value that is lifted into the vars-file), or not exportable (write-only credentials such
as dokku_git_auth, or dokku_service_property, which no datastore plugin can read back).
Resources that cannot be read back are reported as warnings and left out of the recipe.
docket schema prints a machine-readable description of every task type docket registers - the
recipe keys each one accepts, their types, defaults, choices and descriptions, which values are
secrets, what resource the task addresses, its export and probe support, its documented examples,
and for a property task the exact set of property names it accepts. It is the same data the
task reference pages are rendered from, in a form something other than a reader
can consume.
The output is a single pretty-printed JSON document on stdout, described by
schemas/task-catalog-v1.schema.json. See
task catalog for the key-by-key contract.
# Print the catalog.
docket schema
# List every task type.
docket schema | jq -r '.tasks[].type'
# Only the task types you name.
docket schema --task dokku_config --task dokku_domains | jq -r '.tasks[].type'
# What fields does dokku_config take?
docket schema | jq '.tasks[] | select(.type=="dokku_config") | .fields'
# Which property names does dokku_nginx_property accept?
docket schema | jq -r '.tasks[] | select(.type=="dokku_nginx_property") | .property_schema.properties[].name'Like init and validate, schema is offline: it opens no subprocess and contacts no server. It
also reads no recipe, so it takes no --tasks and no positional argument - the --task below
names a task type, not a recipe file. Two runs of the same binary emit byte-identical output,
which is what makes diffing catalogs across docket versions useful.
| Flag | Effect |
|---|---|
| (default) | Write the whole catalog to stdout. |
--output <path> |
Write to a path instead; - writes to stdout. An existing file is overwritten, since the catalog is wholly derived and holds nothing of yours. |
--task <type> |
Restrict the catalog to the named task type, such as dokku_config. Repeatable. The document keeps its shape, a version and a tasks array, so anything that reads the whole catalog reads a narrowed one unchanged. Tasks stay sorted by type whatever order the flags came in, and naming one twice emits it once. An unknown type is an error naming the closest match, not an empty array. |
docket version prints the binary's version and exits.
docket version- Recipes - the recipe file, plays, and
--play/--fail-fast - Task envelope -
tags,when,loop, and the rest - JSON output - the
--jsonevent schema - Task catalog - the
docket schemadocument - Remote execution - running against a remote server over SSH