Dockflow

Templates

Render configuration files and scripts with Jinja before they are built into images or uploaded to the server.

Some files need values that depend on the environment: an nginx vhost with the environment's domain, an entrypoint script with a worker count. List them under templates:, and Dockflow renders them with Jinja before it builds images and uploads files.

The rendering happens in a temporary copy. Your project is never modified.

Example

dockflow.yml
compose.yml
Dockerfile
site.conf.j2
Dockerfile
entrypoint.sh
dockflow.yml
version: 1
app: shop
templates:
  - src: nginx/site.conf.j2
    dest: nginx/site.conf
  - app/entrypoint.sh
environments:
  production:
    vars:
      DOMAIN: shop.example.com
      WORKERS: "4"
  staging:
    vars:
      DOMAIN: staging.shop.example.com
      WORKERS: "2"
compose.yml
services:
  proxy:
    build: ./nginx
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://localhost/healthz"]
    x-dockflow:
      routes:
        - domains: ["${DOMAIN}"]
          port: 80
  app:
    build: ./app

The template uses Jinja's {{ }} and {% %}, so nginx's own $host stays as it is:

nginx/site.conf.j2
server {
    listen 80;
    server_name {{ DOMAIN }};

    location /healthz {
        return 200;
    }

    location / {
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://app:8080;
{%- if DOCKFLOW_ENV != "production" %}
        add_header X-Robots-Tag "noindex";
{%- endif %}
    }
}
nginx/Dockerfile
FROM nginx:1.27-alpine
COPY site.conf /etc/nginx/conf.d/default.conf

nginx/site.conf does not exist in the project. It exists only in the copy the build reads, with the values of the environment being deployed.

Entries

Each entry of templates: is one of:

FormMeaning
src: <path> and dest: <path>Renders src and writes the result at dest in the temporary copy
<path>Renders the file in place: the copy holds the rendered file at the same path

Both paths are relative to the project directory and must stay inside it. Only the files listed are rendered: nothing else is ever treated as a template.

templates: can also be set inside an environment. There, it replaces the top-level list for that environment.

When templates are rendered

The commands that read build contexts or upload sources render templates, once per command:

  • dockflow deploy and dockflow plan, after the before_build hooks, and before the images are built and the uploads are described;
  • dockflow build.

dockflow validate does not render templates. To catch a template error before a deploy, run dockflow build (when the app builds images) or dockflow plan.

Only builds and uploads read rendered files. Hooks run from the project directory and see the source file, not the rendered one. The exception is a hook script: that is itself listed under templates:: it runs rendered (see Hooks).

Variables

A template sees the same values as the rest of the configuration, as top-level variables:

VariablesExamples
Built-in variables{{ DOCKFLOW_APP }}, {{ DOCKFLOW_ENV }}, {{ DOCKFLOW_STACK }}, {{ DOCKFLOW_VERSION }}, {{ DOCKFLOW_GIT_SHA }}, {{ DOCKFLOW_GIT_BRANCH }}, {{ DOCKFLOW_SERVER }}
The environment's secrets, by their unprefixed name{{ API_KEY }} for PRODUCTION_API_KEY
The environment's vars{{ DOMAIN }}

When a name exists in several places, a built-in variable wins over a secret, and a secret over vars. See environment variables for where secrets come from.

Every value is a string. Compare with strings (WORKERS == "4"), or convert first (WORKERS | int).

Secrets in rendered files

A secret rendered into a file ends up in that file: inside the image when the file is part of a build context, or on the server when it is uploaded. Render secrets only into files meant to hold them.

Undefined variables are errors

A variable with no value stops the command with exit code 3, naming the template:

error: templates: nginx/site.conf.j2 cannot be rendered: …
hint: every variable must be defined: built-in, secret or vars of the environment

Dockflow never silently substitutes an empty value. For an optional value, give a default:

worker_processes {{ WORKERS | default("2") }};

A file listed under templates: that does not exist is also an error.

Jinja basics

The engine is gonja, a Go implementation of Jinja.

SyntaxUse
{{ NAME }}Insert a value
{{ NAME | default("x") }}, | upper, | lower, | int, | replace("a", "b")Filters
{% if DOCKFLOW_ENV == "production" %}…{% endif %}Conditions
{% for host in ["a", "b"] %}…{% endfor %}Loops
{%- … %}, {{- … }}Remove the whitespace before the tag (or after it, with -%})
{% raw %}…{% endraw %}Leave the content as written

{% raw %} protects text that uses {{ }} for another tool, such as a Go template in an alerting rule:

# prometheus/rules.yml (listed under templates:)
groups:
  - name: {{ DOCKFLOW_STACK }}
    rules:
      - alert: InstanceDown
        expr: up == 0
        annotations:
{% raw %}          summary: "{{ $labels.instance }} is down"{% endraw %}

How the copy is made

Dockflow renders every template first, then builds a temporary directory:

  • Each build context and each upload source that holds a rendered file is copied into it, with hard links where the filesystem allows. The copy leaves out .git and what the context's .dockerignore excludes, since the build would not read them.
  • The rendered files are written into the copy and replace the hard links, so the source files are never written to.
  • Build contexts and upload sources without a rendered file are read in place, without a copy.

The build and the uploads read from the copy. It is deleted when the command ends.

File modes

A file rendered in place keeps its mode, so an executable entrypoint.sh stays executable.

A file rendered to a new dest takes the mode of the file at dest if the project has one, and 644 otherwise. For a script that must stay executable, render it in place instead, or set the mode in the Dockerfile (COPY --chmod=755).

Edit on GitHub

On this page