Dockflow

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

dockflow.yml
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

PhaseWhen
before_buildBefore images are built
after_buildAfter images are built and pushed
before_deployBefore any service changes
after_deployAfter every service is healthy and routes are switched
after_failureAfter 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

KindWhere it runsTypical use
run: <command>On the machine running the CLI, with sh -c, in the project directoryGenerating 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 shA longer local script
host: <command>On the server, over SSH, as the environment's SSH usersudo systemctl …, host maintenance
exec: [<argv>] with service: <name>Inside every running container of that service, through the Docker Engine API, as docker exec wouldFlushing a cache, warning players, an application-level backup
  • exec takes an argument list, not a shell string. Write exec: ["sh", "-c", "…"] when you need a shell inside the container.
  • exec hooks can only be used from before_deploy on. Before that, nothing of the new release runs.
  • A host hook's command is sent to sh -s on 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 host hook runs without a terminal: a sudo call needs a rule that allows it without a password.

Options

KeyTypeDefaultDescription
namestringthe command, cut to 40 charactersShown in the output and in hook.* events
runstringA shell command, run locally
scriptstringA script file of the project, run locally
hoststringA shell command, run on the server
execlist of stringsA command run in the containers of service
servicestringThe service an exec hook runs in; only with exec
secretslist of keys[]Secrets passed to the hook as environment variables
timeoutduration5mA hook still running after this is stopped and counts as failed
fatalbooleantrue for before_build and before_deploy, false for the othersWhether 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.

  1. before_build hooks run (local and host).
  2. Images are built and pushed.
  3. after_build hooks run.
  4. before_deploy local and host hooks run, before the agent starts, so before the before jobs.
  5. The agent starts. After the before jobs and right before the first service changes, it runs the before_deploy exec hooks.
  6. Services roll out. After the public health checks pass and before the previous containers are drained, the agent runs the after_deploy exec hooks.
  7. The release is committed. The CLI then runs the after_deploy local and host hooks.

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 or host hook, a missing secret);
  • when another run holds the lock.

Commands that run hooks

  • dockflow deploy runs every phase.
  • dockflow build --push, and dockflow build with build.builder: remote, run the before_build and after_build hooks. --skip-hooks turns them off.
  • A local dockflow build without --push runs no hooks. Neither do dockflow plan, dockflow rollback and the day-2 commands.

Hooks inside containers

  • On the first deploy of a service, a before_deploy exec hook has nothing to run in: it is skipped, with a notice. In any other phase, an exec hook 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.
  • exec hooks 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 from PRODUCTION_JWT_PRIVATE_KEY for production). 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, LANGUAGE and LC_*, TMPDIR, TMP, TEMP, the XDG_*_HOME and XDG_RUNTIME_DIR directories;
  • SSH_AUTH_SOCK, the proxies (HTTP_PROXY, HTTPS_PROXY, NO_PROXY, ALL_PROXY, in either case), and the local Docker's DOCKER_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 holding TOKEN, SECRET, PASSWORD, PASSWD, JWT, KEY, CREDENTIAL or AUTH is left out (GITHUB_TOKEN, CI_JOB_TOKEN), and so is CI_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 these

Output 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:

PhaseKindWhat happensExit code
before_buildlocal, hostThe run stops; nothing is built or changed11
after_buildlocal, hostThe run stops before the agent starts; nothing changes on the server11
before_deploylocal, hostThe run stops before the agent starts; nothing changes on the server11
before_deployexecThe agent stops before any service changes and restores the uploads the run applied; what the before jobs did stays done11
after_deployexecThe agent rolls back, as for a failed health check8
after_deploylocal, hostThe release was committed: the CLI starts a rollback run to the previous release8
after_failureanyA warning only: the run has already failedthe run's own

after_build hooks are not fatal unless you set fatal: true. See exit codes for the full list.

Edit on GitHub

On this page