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 YAML, JSON5 and HCL recipes. When --tasks is
omitted they probe tasks.yml, then tasks.yaml, then tasks.json, then tasks.hcl, 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 -
docket export --output - --format hcl | docket apply --tasks-format hcl -The format normally comes from the file extension, and for stdin from the first significant token
([, {, //, or /* means JSON5; an identifier followed by =, {, or a quoted label means
HCL; anything else means YAML). --tasks-format yaml|json5|hcl overrides both. Reach for it when
the extension is absent or misleading (--tasks recipe.txt, or a URL whose path carries no
extension), when a YAML recipe written in flow style would be sniffed as JSON5 because it opens with
[, or when a hand-written HCL recipe opens with a // comment, which JSON5 claims first.
--tasks-format is the reading side. On the writing side, init, export, and fmt take
--format yaml|json5|hcl to state the format of what they emit. Without it, init and export can
only ever write YAML to stdout, since there is no extension to infer from. On fmt the two flags
compose: --tasks-format says what the recipe already is, --format says what to write it as, and
naming a different one converts the recipe between them.
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, .hcl writes an HCL one, anything else
writes YAML. Streaming to stdout (--output -) has no extension to read, so it writes YAML unless
--format says otherwise. Passing --format without an --output moves the default name to match:
--format json5 writes ./tasks.json and --format hcl writes ./tasks.hcl, rather than either
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 json5
# Same scaffold in HCL, written to ./tasks.hcl.
docket init --format hclinit 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, tasks.hcl from --format hcl, 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); each input
declares a type: docket implements and a default: readable as that type (invalid_input_type and
invalid_input_default - an omitted default is the zero value and is fine); 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 file in any recipe format, chosen by its extension and defaulting to YAML (repeatable). Values here count as overrides for --strict. A file that supplies a sensitive: true input and is readable by other users is warned about on stderr. 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 YAML, JSON5 and HCL, detected per file from
the extension, and all three share the same canonical key order so a YAML recipe and its JSON5 or
HCL twin lay out identically. Comments are preserved in every format. An HCL recipe also has its
= signs aligned, by hclwrite.Format, so docket's HCL and terraform fmt's agree about
whitespace.
A recipe it cannot parse is reported with the byte offset of the problem and left untouched. What
fmt accepts is what apply, plan, and validate accept, so a file that formats is a file that
loads. It refuses a little more than they do, which the next paragraphs cover.
What fmt objects to that the others do not comes from it seeing parts of the recipe the others
never look at. Two cases come from reading the recipe as written rather than rendering it first,
which is what lets fmt see the quote characters at all. An unquoted interpolation is the first:
{{ .app }} outside quotes is YAML flow syntax rather than template text, so fmt names the line
instead of writing YAML's reading of it back out. A rewrite that would change an interpolation's
quoting is the second, refused on a conversion, on a JSON5 recipe whose string is single-quoted, and
on an HCL recipe whose heredoc holds one, but never on a plain YAML format - see
Converting between formats. Both are about the same thing, and
Inputs covers it.
The third is comments, which fmt preserves and everything else discards before it reads a recipe
at all. A JSON5 comment holding a byte that is not valid UTF-8 is refused, naming the byte and its
offset, because a comment is carried through a conversion verbatim and there is no writing such a
byte into YAML. A YAML recipe needs no such rule; its parser will not read the byte in the first
place. Inside a string value an invalid byte is not refused but read as U+FFFD, which is what
apply reads it as too.
--format makes it a converter as well as a formatter. It states the format to write, so naming
one the recipe is not already in rewrites it into that format, comments and all - the thing a trip
out to another tool and back cannot do.
# 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 -
# Convert a recipe to JSON5 and write it to stdout, touching nothing on disk.
cat tasks.yml | docket fmt --format json5 -
# Convert a recipe to HCL.
docket fmt --format hcl --output tasks.hcl tasks.yml
# Convert to a new file, leaving the original alone. The extension alone would
# have been enough; --format is what a name like recipe.txt would need.
docket fmt --format json5 --output tasks.json5 tasks.yml
# Convert in place, keeping the name. See the warning this prints, below.
docket fmt --format json5 tasks.json| 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> |
Read the recipe as yaml, json5 or hcl instead of detecting it. |
--format <fmt> |
Write the recipe as yaml, json5 or hcl, converting it when that is not what it was read as. Cannot be combined with --check. |
--output <path> |
Write to this path instead of in place; - writes to stdout. Takes a single recipe, and cannot be combined with --check or --diff. |
--force |
Overwrite an existing --output file. |
- |
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.
Without --output, --format converts the file in place and keeps its name, which leaves a
tasks.yml holding JSON5. fmt says so once, on stderr, because nothing downstream can tell: a
later docket validate --tasks tasks.yml picks its parser from the extension and fails. Prefer
--output when the name should follow the format:
docket fmt --format json5 --output tasks.json5 tasks.yml && rm tasks.yml--output never overwrites an existing file without --force, and takes a single recipe, so it
cannot be combined with a glob. Writing back over the file that was read is the ordinary in-place
case and needs no --force.
--check cannot be combined with a --format that converts. It asks whether a recipe is already
canonical, and a conversion is never a no-op, so every file would be reported as unformatted. To
check a recipe whose extension is misleading, use --tasks-format. --diff does compose with a
converting --format, and previews the conversion without writing anything.
A conversion is not byte-reversible, and is not meant to be. Comments survive, and so does every value, but these things are normalised on the way:
- Comments change syntax. A
# notebecomes// noteand back. A JSON5 block comment/* note */comes back as// note, since a line comment cannot be terminated early by its own text. - YAML anchors, aliases, and merge keys are inlined. JSON5 has no way to write sharing down, so
<<: *baseis expanded into the keys it merges in, following YAML's own precedence. The recipe means the same thing; the fact that a block was written once does not survive. - Numbers are normalised to decimal. YAML accepts
0o17,0b1010, and1_000, none of which JSON5 has, so they are written as15,10, and1000. A JSON50x1Fbecomes31even though YAML would take the hex, because that is whatvalidatealready reads it as, andfmtmust not disagree withvalidateabout what a recipe says. - A leading
---is not restored. The marker describes the bytes that were read, and a recipe arriving from JSON5 had none. - A key that is not a plain ASCII identifier is written quoted. JSON5 leaves
appunquoted, butcaféandmy-keyare not identifiers to a JSON5 reader, so they are written"café"and"my-key". HCL's identifiers are wider and take both unquoted, but its attribute NAMES have to be identifiers, so a play or envelope key holding a space, a dot, or a leading digit is refused. - An unpaired surrogate escape becomes U+FFFD. A
\uD83Dwith no low surrogate after it is not a character, and the replacement is what every JSON5 reader makes of it, including the oneapplyuses. - Into HCL,
${and%{are doubled. They open a template in HCL and are ordinary text everywhere else, so the literal has to be written$${and%%{. See HCL recipes. - Into HCL, a multi-line string becomes a heredoc. A value carrying a newline is written as
<<EOT, which keeps a certificate or anapp.jsonreadable. A value carrying an interpolation never is, because a heredoc processes no backslash escapes. - An empty recipe has no HCL spelling. A list of no plays would be an empty HCL file, which reads back as no recipe at all, so the conversion is refused rather than written.
A YAML timestamp is the one value whose type changes: neither JSON5 nor HCL has a date literal, so
2015-01-01 becomes the string "2015-01-01". Nothing in a recipe reads it as anything else - every
task field holding one is a string already. Infinity and NaN go the other way: JSON5 spells them
and HCL has no literal for either, so a conversion into HCL is refused rather than widened.
Quoting is preserved where it carries meaning, and where it cannot be, fmt refuses rather than
change it. A recipe is rendered as text and only then parsed, so the quotes around an interpolation
decide how the substituted value is escaped - see Inputs. Canonical JSON5 and canonical
HCL both have only the double-quoted string, which is the one spelling that needs | dq, so every
other way of writing a YAML scalar loses something on the way across to either:
! tasks.yml: line 5: `{{ .app }}` is single-quoted, and rewriting it double-quoted would leave
a recipe that no longer tolerates a double quote in the value; write it as "{{ .app | dq }}"
The error names every line it objects to, so a recipe with several is one edit rather than several.
It covers a single-quoted scalar, a plain one, and a literal or folded block - all of them tolerate
a double quote in the value, and none of them survives being written as "...". The fix is always
the same, | dq inside a double-quoted scalar, which renders identically wherever the original
worked and keeps working where the original would have broken.
Two things are deliberately left alone. An interpolation that is already | dq escaped converts
untouched, in either direction. So does one that substitutes no value at all, such as
'web{{ if .debug }}-verbose{{ end }}', where only literal recipe text is ever inserted.
The same refusal applies with no conversion in sight: docket fmt tasks.json5 would fold a
single-quoted JSON5 string into the double-quoted canonical form, and docket fmt tasks.hcl would
fold a heredoc into it, so both are refused on exactly the same terms. A heredoc is refused a little
harder than a single-quoted string: it processes no backslash escapes at all, so | dq inside one
is as wrong as no escaping. Only a plain docket fmt of a YAML recipe is unaffected, since
YAML-to-YAML formatting leaves every scalar's quoting as it found it.
An interpolation containing a double quote of its own - {{ .app | default "" }}, say - has no
double-quoted spelling in any of the three formats, because the scalar would have to escape that
quote and the template engine reads the recipe before anything unescapes it. Such an action has to be rewritten to
drop the literal; a backquoted raw string is one way.
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). A file that supplies a sensitive: true input and is readable by other users is warned about on stderr. 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 |
Run dokku as root via sudo -n, remotely with --host and locally without. See remote execution. |
--accept-new-host-keys |
Trust an unknown SSH host key on first connect. See remote execution. |
--output <path> |
Save the plan to a file for docket apply --plan. Written 0600, and not written at all when the plan has errors. Not valid with - or --list-tasks. See saved plans. |
--force |
Overwrite an existing --output file. |
# CI gate: fail the job if any task would change the server.
docket plan --detailed-exitcode || exit $?docket plan --output plan.json saves the plan, and docket apply --plan plan.json applies it
later. This is the workflow for reviewing a change before it runs: plan in one step, have someone
approve what it printed, and apply that plan in another.
docket plan --output plan.json --vars-file prod.yml
# ...review the output...
docket apply --plan plan.jsonThe plan file freezes everything that decides what apply does:
- The recipe text, so a recipe read from a URL or stdin is not read again, and a recipe edited after the plan was saved does not change what is applied.
- Every input's resolved value, whether it came from a default, a flag, or a
--vars-file. --play,--tags, and--skip-tags.- The target:
--host,--sudo,--accept-new-host-keys, and theDOKKU_HOST,DOKKU_SUDO, andDOKKU_SSH_ACCEPT_NEW_HOST_KEYSthey fall back to. Under--planthose variables are ignored. A play's ownhost:still comes from the saved recipe. - The plan itself: every play and task, what each would change, and the commands it would run.
Because all of that comes from the file, apply --plan refuses the flags that would contradict it:
--tasks, a positional recipe, --tasks-format, --vars-file, input flags, --play, --tags,
--skip-tags, --host, --sudo, --accept-new-host-keys, --start-at-task, and
--list-tasks. --json, --verbose, --fail-fast, and --detailed-exitcode work as usual.
The saved plan is checked before anything runs. apply --plan first probes every task again,
exactly as plan does, and compares the result with what the file recorded. If anything differs -
the server changed, a probe failed, or a local file the recipe reads now says something else - it
refuses with saved plan is stale, lists what moved, and exits 1 without running any task:
saved plan is stale: plan.json no longer matches what docket would do; run docket plan again
tasks/ensure api: planned "+ app missing", now "ok"
This means every task is probed twice: once for the check, and once more as it runs. Terraform only notices a stale plan when its own state file has moved on. docket has no state file, so it asks the server directly, which also catches changes made outside docket.
After the check, apply behaves as it always does. Each task reads the server again right before
it acts, and decides what to do from what it finds at that moment. So a saved plan guarantees that
the server matched what was reviewed when apply started, and that apply then brought the server
to what the recipe describes. It does not guarantee that the exact commands in the plan ran:
- A change someone else makes after the check is absorbed rather than reported. If the app already
exists by the time its task runs, the task reports
[ok]; if a config key was changed, it is set back. - A change that breaks a later task - the app deleted halfway through - fails that task the normal
way, through
--fail-fast,rescue, andignore_errors. - As with every
apply, a change that lands between one task's read and its own write goes unseen.
A few more rules:
- The file holds secrets in the clear. The recipe and every input value are stored as-is,
sensitive: trueinputs included, so the plan can be applied without them. It is written0600, andapply --planwarns on stderr when the file is readable by other users. The outputplanprints stays masked as usual. - Files the recipe reads from disk are read again at apply time. Only the recipe itself is frozen. A change to such a file shows up as a stale plan only when it changes what a task would do.
- A plan is applied only by the docket version that wrote it. What a task reads and how it
applies can change between releases, so
apply --planrefuses a plan written by another version. Runplanagain after upgrading. - An existing file is kept unless
--forceis passed, and that check happens before anything is probed. - A plan with errors is not saved. When a probe fails,
planprintsplan has errors; not writing plan.jsonand exits1.
The file is JSON, described by schemas/plan-v1.schema.json.
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). A file that supplies a sensitive: true input and is readable by other users is warned about on stderr. 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 |
Run dokku as root via sudo -n, remotely with --host and locally without. See remote execution. |
--accept-new-host-keys |
Trust an unknown SSH host key on first connect. See remote execution. |
--plan <path> |
Apply a plan saved by docket plan --output, after checking the server still matches it. Takes the recipe, inputs, filters and target from the file. See saved plans. |
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.
Setting TERM=dumb disables them too, and output is plain automatically when piped to a non-TTY.
Ctrl-C aborts the whole run, not just the task in flight. The interrupt ends whatever dokku or
ssh command is executing, no further task is started - including the plays that would have
followed - and apply prints run cancelled and exits 1. That exit code is deliberate: an
interrupted run reports 1 rather than the 2 --detailed-exitcode uses for "completed, and
something changed", because it did not complete. A second Ctrl-C kills docket outright, in case
the first one left something wedged.
A dokku or ssh command killed by the interrupt is never read as the server's answer: the task in
flight reports [!] rather than a predicted change, and apply does not act on it. A command
killed by a signal outside an interrupt is reported as an error, killed by a signal, for the same
reason.
Cancellation reaches only the local process. Over SSH it ends the local ssh client; a dokku
command already running on the remote host keeps going, so re-run plan afterwards to see where
the server actually ended up.
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_app[app=api] [tags=core]
[1] dokku_service_property[service=redis,name=cache,property=shm-size] (never converges)
[2] dokku_git_from_image[app=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.
Sensitive values are masked in the listing exactly as they are in a run. A task name, play name,
tag, when: predicate, or loop item that interpolated a sensitive: true input or a task field
tagged sensitive:"true" renders as *** on both the human and the --json path, so the listing
is safe to paste into a ticket or a CI log. The hints an unmatched --play or --start-at-task
prints - each of which lists the names it could have matched - are masked the same way.
--start-at-task still matches on the real name, which means a name masked down to *** cannot be
copied out of the listing and used verbatim - write an explicit name: on any task you need to
resume at.
--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 -
# Export the local server as an HCL pair.
docket export --format hcl
# 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]).
The two halves are written with different modes, because they hold different things. The recipe
carries interpolations rather than values, so it lands at 0644 like every other file docket
writes. The vars-file holds the values themselves in the clear - every config value, the
scheduler-k3s cluster token, the dns-provider-* credentials, the logs vector sinks, the http-auth password hashes - so it
lands at 0600, readable only by the user who ran the export, which is the same user who reads it
back through --vars-file. That covers --redact too: a placeholder file is the one you then type
the real secrets into, and it covers a vars-file already on disk, whose mode is reset on every
write so a file an older docket left at 0644 is locked down the next time export writes it. On a
filesystem that cannot hold the bits at all, export says so and finishes rather than failing.
--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.
When every address either names its app or is global, export reads only the named apps and does not list the server's apps first. An app that does not exist is still reported as the address that named it.
| 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, json5 or hcl 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, and --format hcl writes ./tasks.hcl and ./tasks.vars.hcl. Required to stream anything but YAML 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). Written 0600 wherever it lands. 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, and the template is still written 0600 because filling it in is what puts the secrets there. |
--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 |
Run dokku as root via sudo -n, remotely with --host and locally without. |
--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, .hcl writes HCL, anything else YAML); the vars-file follows the recipe, or
its own --vars-output extension when --format is not given. A JSON5 vars-file is plain JSON,
which is also valid YAML, so one named .yml still loads; an HCL one does not, and export says so
rather than letting the next --vars-file find out. 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, as is a
resource that reads back fine but could not be applied back - a dokku_scheduler_k3s_profile
whose name dokku accepted but helm cannot turn into a release name, for example. Emitting one of
those would fail docket validate for the whole recipe rather than for the single task, so the
warning names the resource and what to do about it instead.
Those warnings, and the failure messages beside them, mask the secrets the export read off the
server - every config value, every field a task marks sensitive, and every property in a family
declared sensitive - so an export log is safe to paste into a ticket even when an exporter's error
quotes the value it choked on. The recipe and the vars-file are written straight to their file (or
to stdout) rather than through that masking, which is the point: the pair has to carry the real
values to be appliable. Two things are deliberately left readable: an --app name or --resource
address reported as not found on the server, and the output paths, both of which are your own
arguments echoed back at you - a name masked down to *** would hide the typo the message exists
to report. The one gap is a value export never managed to read: if an exporter fails while parsing
it, nothing ever learned it was a secret, and the error text is the only place it appears.
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