Hooks
Run commands around builds and deploys, on your machine, on the server, or inside a running container.
Hooks run your own commands at fixed points of a deploy. For example: generate a key before the build, reload a systemd unit on the server, or tell a game server to save its world before its container is replaced.
Declare them in dockflow.yml under hooks:, one list per phase.
A first example
version: 1
app: shop
hooks:
before_build:
- name: generate keys
script: scripts/generate-keys.sh
secrets: [JWT_PRIVATE_KEY]
before_deploy:
- name: back up the world
service: game
exec: ["rcon-cli", "save-all"]
timeout: 10m
fatal: false
- host: sudo systemctl daemon-reload
after_deploy:
- name: tell the chat
run: 'curl -fsS -X POST -d "deployed $DOCKFLOW_VERSION" "$CHAT_WEBHOOK"'
secrets: [CHAT_WEBHOOK]
after_failure:
- run: echo "deploy of $DOCKFLOW_VERSION failed" >&2
environments:
production: {}Each entry has exactly one kind (run, script, host or exec) and runs in order within its phase. A phase stops at the first fatal hook that fails.
Phases
| Phase | When |
|---|---|
before_build | Before images are built |
after_build | After images are built and pushed |
before_deploy | Before any service changes |
after_deploy | After every service is healthy and routes are switched |
after_failure | After a failed run, once the rollback is done |
hooks: can also be set inside an environment. There, it replaces the top-level hooks: for that environment.
Kinds
| Kind | Where it runs | Typical use |
|---|---|---|
run: <command> | On the machine running the CLI, with sh -c, in the project directory | Generating files, calling an API |
script: <file> | On the machine running the CLI, in the project directory. The path is relative to the project; an executable file is run directly, any other file with sh | A longer local script |
host: <command> | On the server, over SSH, as the environment's SSH user | sudo systemctl …, host maintenance |
exec: [<argv>] with service: <name> | Inside every running container of that service, through the Docker Engine API, as docker exec would | Flushing a cache, warning players, an application-level backup |
exectakes an argument list, not a shell string. Writeexec: ["sh", "-c", "…"]when you need a shell inside the container.exechooks can only be used frombefore_deployon. Before that, nothing of the new release runs.- A
hosthook's command is sent tosh -son the server's standard input, with its environment exported at the top of that script. No value appears in a command line on the server. - A
hosthook runs without a terminal: asudocall needs a rule that allows it without a password.
Options
| Key | Type | Default | Description |
|---|---|---|---|
name | string | the command, cut to 40 characters | Shown in the output and in hook.* events |
run | string | A shell command, run locally | |
script | string | A script file of the project, run locally | |
host | string | A shell command, run on the server | |
exec | list of strings | A command run in the containers of service | |
service | string | The service an exec hook runs in; only with exec | |
secrets | list of keys | [] | Secrets passed to the hook as environment variables |
timeout | duration | 5m | A hook still running after this is stopped and counts as failed |
fatal | boolean | true for before_build and before_deploy, false for the others | Whether a failure stops the run |
dockflow validate checks the shape of each entry. It does not check that a script file exists, that an exec hook's service is in the Compose file, or that listed secrets have values. Those errors appear when the hook is about to run. A missing secret stops the run with exit code 3, before anything happens.
When each kind runs
exec hooks run inside the agent, on the server. Local and host hooks run in the CLI, before the agent starts or after it finishes, never in the middle of the agent's run. This way the agent can finish a deploy even if the CLI's connection drops. Why: see ADR-0022.
before_buildhooks run (local andhost).- Images are built and pushed.
after_buildhooks run.before_deploylocal andhosthooks run, before the agent starts, so before thebeforejobs.- The agent starts. After the
beforejobs and right before the first service changes, it runs thebefore_deployexechooks. - Services roll out. After the public health checks pass and before the previous containers are drained, the agent runs the
after_deployexechooks. - The release is committed. The CLI then runs the
after_deploylocal andhosthooks.
If the agent's run fails, the agent rolls back, runs the after_failure exec hooks, and reports the failure. The CLI then runs the after_failure local and host hooks.
after_failure does not run:
- when the run stops on the CLI's side before the agent starts (a failed build, a fatal
before_*local orhosthook, a missing secret); - when another run holds the lock.
Commands that run hooks
dockflow deployruns every phase.dockflow build --push, anddockflow buildwithbuild.builder: remote, run thebefore_buildandafter_buildhooks.--skip-hooksturns them off.- A local
dockflow buildwithout--pushruns no hooks. Neither dodockflow plan,dockflow rollbackand the day-2 commands.
Hooks inside containers
- On the first deploy of a service, a
before_deployexechook has nothing to run in: it is skipped, with a notice. In any other phase, anexechook whose service has no running container fails. - During
after_deploy, the hook runs in the containers of the new release only, not in the previous ones waiting to drain. exechooks run in each container in turn. The first container where the command exits with a non-zero code stops the hook.
Lost connection
Local and host hooks of after_deploy and after_failure run in the CLI once the agent has finished. If the CLI is stopped or loses its connection during the run, the deploy still completes on the server, but those hooks do not run.
Environment variables
Every hook gets these environment variables:
- the built-in variables:
DOCKFLOW_APP,DOCKFLOW_ENV,DOCKFLOW_STACK,DOCKFLOW_VERSION,DOCKFLOW_GIT_SHA,DOCKFLOW_GIT_BRANCH,DOCKFLOW_SERVER(see environment variables); - the environment's
vars; - the secrets listed in the hook's
secrets:, by their unprefixed name (JWT_PRIVATE_KEY, read fromPRODUCTION_JWT_PRIVATE_KEYforproduction). Other secrets are not passed.
Local hooks also inherit part of the CLI's environment, what a shell and the usual tools need:
PATH,HOME,USER,LOGNAME,SHELL,TERM,COLORTERM,NO_COLOR,TZ,LANG,LANGUAGEandLC_*,TMPDIR,TMP,TEMP, theXDG_*_HOMEandXDG_RUNTIME_DIRdirectories;SSH_AUTH_SOCK, the proxies (HTTP_PROXY,HTTPS_PROXY,NO_PROXY,ALL_PROXY, in either case), and the local Docker'sDOCKER_HOST,DOCKER_CONTEXT,DOCKER_CONFIG,DOCKER_CERT_PATH,DOCKER_TLS_VERIFY;- on Windows, the system's own (
SYSTEMROOT,COMSPEC,PATHEXT,USERPROFILE,APPDATA, …); - the CI's description of the run:
CI,GITHUB_*,RUNNER_*,CI_*,GITLAB_*,BUILDKITE_*,BITBUCKET_*, and Jenkins'BUILD_ID,BUILD_NUMBER,BUILD_URL,JOB_NAME,BRANCH_NAME,TAG_NAME,GIT_COMMIT,GIT_BRANCH,WORKSPACE,JENKINS_URL. A name holdingTOKEN,SECRET,PASSWORD,PASSWD,JWT,KEY,CREDENTIALorAUTHis left out (GITHUB_TOKEN,CI_JOB_TOKEN), and so isCI_REPOSITORY_URL, which holds GitLab's job token.
Nothing else is inherited: not the variables of any environment's secrets (PRODUCTION_*, STAGING_*, even when one of them would match a family above), nor DOCKFLOW_SECRETS, DOCKFLOW_SSH_KEY*, SOPS_AGE_KEY*, or a token your CI sets (NPM_TOKEN, AWS_*). A hook that needs one of these values lists it under secrets:, which reads it from PRODUCTION_NPM_TOKEN, DOCKFLOW_SECRETS or .env.dockflow, as any secret.
host hooks get the variables above on top of the SSH session's own environment, and exec hooks on top of the container's.
$ in hook commands
Hook commands (run, host, exec) are neither interpolated nor rendered as templates. $NAME reaches the shell as you wrote it, and the shell expands it from the environment the hook gets.
A script: that needs a real template must be listed under templates:. It then runs rendered, from a temporary copy made when the hook starts, so it sees what earlier hooks wrote. Your project's file is not modified.
# dockflow.yml (fragment)
templates:
- scripts/notify.sh
hooks:
after_deploy:
- script: scripts/notify.sh # runs with {{ DOCKFLOW_VERSION }} and the vars rendered# dockflow.yml (fragment)
hooks:
after_deploy:
- run: echo "$DOCKFLOW_ENV is now on $DOCKFLOW_VERSION" # the shell expands theseOutput and masking
Each line a hook prints becomes a hook.log event, between hook.started and hook.finished (with the exit code). The terminal shows these lines prefixed with the hook's name; --json prints them as events.
Before any line is shown or stored, every value of the environment's secrets is replaced with ***, whether the hook lists that secret or not. The masking works on exact values: a secret that a hook transforms (base64-encodes, splits) is not recognised.
Failures and exit codes
A hook fails when it exits with a non-zero code, cannot start, or runs past its timeout. A non-fatal failure is shown as a warning, and the run goes on. A fatal failure does this:
| Phase | Kind | What happens | Exit code |
|---|---|---|---|
before_build | local, host | The run stops; nothing is built or changed | 11 |
after_build | local, host | The run stops before the agent starts; nothing changes on the server | 11 |
before_deploy | local, host | The run stops before the agent starts; nothing changes on the server | 11 |
before_deploy | exec | The agent stops before any service changes and restores the uploads the run applied; what the before jobs did stays done | 11 |
after_deploy | exec | The agent rolls back, as for a failed health check | 8 |
after_deploy | local, host | The release was committed: the CLI starts a rollback run to the previous release | 8 |
after_failure | any | A warning only: the run has already failed | the run's own |
after_build hooks are not fatal unless you set fatal: true. See exit codes for the full list.