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.
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 listshows the built-in plugins and the project's, with their inputs.dockflow config showlists 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:
plugins:
- use: nginx
with:
domain: [shop.example.com, www.shop.example.com]
tls: true # certbot's certificate of the first domain-
It renders a vhost for the domains, proxying to the switch at
127.0.0.1:8080, with theHost,X-Forwarded-ForandX-Forwarded-Protoheaders and WebSockets. -
It uploads it to
/etc/nginx/sites-enabled/<app>-<env>.conf. -
After the deploy, when the vhost changed, it runs
sudo nginx -t. It then reloads nginx withsudo systemctl reload nginx, or withsudo nginx -s reloadwhere the first is not allowed.A vhost that
nginx -trefuses 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.
| Input | Default | Description |
|---|---|---|
domain | required without template | The domains the vhost serves: a list, or one domain. A template names its own, and needs none. |
upstream | 127.0.0.1:8080 | Where nginx sends requests: the switch's --switch-listen. |
port | none | With the none edge, a port published on the server's loopback, instead of upstream. |
tls | false | Listen 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. |
cloudflare | false | Behind Cloudflare: the client's address from CF-Connecting-IP, trusted from Cloudflare's ranges only, and the scheme from Cloudflare's X-Forwarded-Proto. |
ipv6 | true | Listen 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_size | 25m | nginx's client_max_body_size. |
dir | /etc/nginx/sites-enabled | The directory nginx includes vhosts from; /etc/nginx/conf.d on Red Hat systems. |
name | <app>-<env>.conf, with -<id> for an id other than nginx | The vhost's file name. |
template | none | The 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 nginxdockflow 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:
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, thenenableandrestartof the unit. - A failure is a warning: the release stays.
- Inputs:
unit(required, a file of the project) andname(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 withsudo. - The SSH user needs
sudowithout a password forsystemctl.
Writing a plugin
A plugin is a directory of your project with a plugin.yml.
- Every other file of the directory (except a
.gitdirectory) 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 inplugin.ymlis an error.
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"[Service]
ExecStart=/usr/bin/rclone mount {{ remote }} {{ mount }}{% if read_only %} --read-only{% endif %}| Key | Description |
|---|---|
name | The 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}$. |
description | Shown by plugins list. |
inputs | Each 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. |
templates | Files of the plugin rendered with Jinja: a path, rendered in place, or src and dest. |
uploads | As in dockflow.yml, with src a file or directory of the plugin. |
hooks | As 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:
| Type | Value |
|---|---|
string (default) | A string; a number or a boolean is turned into its text. Empty when not given. |
boolean | true or false, also as a string from a variable. false when not given. |
list | A list of strings; a single value is a list of one. Empty when not given. |
file | A 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,uploadsandhooksinplugin.ymlare Jinja templates too. They are rendered with the inputs,id, the built-in variables and the environment'svars, never the secrets. Mapping keys are not rendered. A value that renders totrueorfalseis 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 %}needsxdeclared 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 fromuploads:does.