Dockflow

Plugins

Reuse bundles of uploads, templates and hooks, such as the built-in nginx vhost and systemd unit, or your own.

A plugin is a bundle of uploads, templates and hooks, filled in with the inputs your project gives it. Dockflow ships two, nginx and systemd, and a project can write its own.

A plugin behaves exactly like the uploads and hooks you write yourself. What it uploads is replaced atomically and put back by a rollback. Its hooks run, fail and roll back like yours.

dockflow.yml
plugins:
  - use: nginx                      # built-in
    with:
      domain: [shop.example.com, www.shop.example.com]
  - use: ./deploy/plugins/rclone    # the project's own
    id: archives
    with: { remote: "s3:archives" }
  • dockflow plugins list shows the built-in plugins and the project's, with their inputs.
  • dockflow config show lists the uploads and hooks they add.
  • The plan names the plugin of each upload.

The nginx plugin

The nginx plugin writes the nginx vhost that sends your domains to the switch, from your repository. Use it with the external edge, which puts the switch behind the host's nginx:

dockflow.yml
plugins:
  - use: nginx
    with:
      domain: [shop.example.com, www.shop.example.com]
      tls: true                     # certbot's certificate of the first domain
  1. It renders a vhost for the domains, proxying to the switch at 127.0.0.1:8080, with the Host, X-Forwarded-For and X-Forwarded-Proto headers and WebSockets.

  2. It uploads it to /etc/nginx/sites-enabled/<app>-<env>.conf.

  3. After the deploy, when the vhost changed, it runs sudo nginx -t. It then reloads nginx with sudo systemctl reload nginx, or with sudo nginx -s reload where the first is not allowed.

    A vhost that nginx -t refuses rolls the deploy back, which puts the previous vhost back. nginx was never reloaded, so it kept serving the previous configuration.

    A deploy that leaves the vhost unchanged does not touch nginx.

InputDefaultDescription
domainrequired without templateThe domains the vhost serves: a list, or one domain. A template names its own, and needs none.
upstream127.0.0.1:8080Where nginx sends requests: the switch's --switch-listen.
portnoneWith the none edge, a port published on the server's loopback, instead of upstream.
tlsfalseListen on 443 with the certificate in certificate_dir, and redirect 80 to 443. Without it, the vhost listens on 80 only, as behind Cloudflare's flexible mode.
certificate_dir/etc/letsencrypt/live/<first domain>The directory of fullchain.pem and privkey.pem.
cloudflarefalseBehind Cloudflare: the client's address from CF-Connecting-IP, trusted from Cloudflare's ranges only, and the scheme from Cloudflare's X-Forwarded-Proto.
ipv6trueListen on IPv6 too. Turn it off on a server whose kernel has no IPv6 (booted with ipv6.disable=1), where nginx refuses listen [::].
max_body_size25mnginx's client_max_body_size.
dir/etc/nginx/sites-enabledThe directory nginx includes vhosts from; /etc/nginx/conf.d on Red Hat systems.
name<app>-<env>.conf, with -<id> for an id other than nginxThe vhost's file name.
templatenoneThe project's own vhost, a Jinja template that sees these inputs, instead of the plugin's.

Each value goes into nginx's configuration as it is, so each is checked first: domains are host names or *. wildcards, upstream is host:port, port a number, max_body_size a size such as 25m, dir and certificate_dir absolute paths, name a file name. Anything else, such as a ; or a space, is refused with config.invalid.

The hook runs as the environment's SSH user, without a terminal. So sudo must not ask for a password for nginx -t and one of the two reloads.

server init --nginx installs nginx and this rule. On a server prepared otherwise, add the rule with sudo visudo -f /etc/sudoers.d/dockflow-nginx, with your SSH user and the paths that command -v nginx systemctl prints:

dockflow ALL=(root) NOPASSWD: /usr/sbin/nginx -t, /usr/bin/systemctl reload nginx

dockflow ALL=(root) NOPASSWD: /usr/sbin/nginx -t, /usr/sbin/nginx -s reload works too.

To get the certificate first, without letting certbot edit the vhost: sudo certbot certonly --nginx -d shop.example.com -d www.shop.example.com.

With cloudflare: true, Cloudflare's ranges are the ones Dockflow knew when it was built. They are written into the vhost's server block, so several apps behind Cloudflare can share one nginx. A deploy with a newer Dockflow writes the newer list.

The systemd plugin

The systemd plugin installs a unit from your repository, rendered as a Jinja template, and enables it:

dockflow.yml
plugins:
  - use: systemd
    with:
      unit: deploy/rclone-archives.service
  • The unit goes to /etc/systemd/system/rclone-archives.service.
  • When the unit changed, the plugin runs sudo systemctl daemon-reload, then enable and restart of the unit.
  • A failure is a warning: the release stays.
  • Inputs: unit (required, a file of the project) and name (default: the file's name). The unit's name, given or taken from the file, must match [A-Za-z0-9@._-]+: it goes into a command run with sudo.
  • The SSH user needs sudo without a password for systemctl.

Writing a plugin

A plugin is a directory of your project with a plugin.yml.

  • Every other file of the directory (except a .git directory) belongs to the plugin. Paths in the plugin are relative to the directory, and must stay inside it.
  • use: names the plugin by its path, which contains a /: ./deploy/plugins/rclone.
  • As in dockflow.yml, an unknown key in plugin.yml is an error.
deploy/plugins/rclone/plugin.yml
description: An rclone mount of a remote, as a systemd unit
inputs:
  - name: remote
    required: true
    description: the rclone remote, like s3:archives
  - name: mount
    default: /mnt/archives
  - name: read_only
    type: boolean
templates:
  - rclone.service                   # rendered in place
uploads:
  - src: rclone.service
    dest: "/etc/systemd/system/rclone-{{ id }}.service"
    mode: "644"
hooks:
  after_deploy:
    - name: restart the mount
      host: sudo systemctl daemon-reload && sudo systemctl restart rclone-{{ id }}
      if_changed: "/etc/systemd/system/rclone-{{ id }}.service"
deploy/plugins/rclone/rclone.service
[Service]
ExecStart=/usr/bin/rclone mount {{ remote }} {{ mount }}{% if read_only %} --read-only{% endif %}
KeyDescription
nameThe plugin's name, the default id of its uses. Default: the directory's name. An id, given or defaulted, must match ^[a-z][a-z0-9_-]{0,31}$.
descriptionShown by plugins list.
inputsEach with name, description, type (default string), required and default. required_unless: <input> makes it required when that other input is not given; it must name another input. pattern is a regular expression a given value must match whole: a string, each item of a list, a file's name; a value that ends up in a command or a configuration file should have one. A name matches ^[a-z][a-z0-9_]*$, is declared once, and cannot be id, which is reserved.
templatesFiles of the plugin rendered with Jinja: a path, rendered in place, or src and dest.
uploadsAs in dockflow.yml, with src a file or directory of the plugin.
hooksAs in dockflow.yml, without script: upload the script and run it with host:. An after_deploy hook can take if_changed: the destination of one of the plugin's uploads, so it runs only when the deploy changed it. if_changed in another phase, or naming no upload of the plugin, is an error. Write a value into a host command as {{ name | shquote }}, which makes it one shell word whatever it holds.

Input types:

TypeValue
string (default)A string; a number or a boolean is turned into its text. Empty when not given.
booleantrue or false, also as a string from a variable. false when not given.
listA list of strings; a single value is a list of one. Empty when not given.
fileA file of the project, copied into the plugin as inputs/<input>/<file name>: the input's value is that path, for templates or uploads to use.

What the plugin sees:

  • The string values of templates, uploads and hooks in plugin.yml are Jinja templates too. They are rendered with the inputs, id, the built-in variables and the environment's vars, never the secrets. Mapping keys are not rendered. A value that renders to true or false is a boolean again (fatal: "{{ strict }}").
  • Template files also see the secrets, as the project's own templates do.
  • An undefined variable is an error, so {% if x %} needs x declared as an input.

How plugins combine with the project:

  • The plugins' uploads and hooks come before the project's own, in the order of plugins:.
  • Each hook is named <id>: <name>.
  • Two uses of one plugin need different ids.
  • A plugin removed from plugins: leaves its uploaded files on the server, as an upload removed from uploads: does.
Edit on GitHub

On this page