Documentation

Configuration

Delivery Boy keeps the release contract in .deliver.yml at the root of your repository.

Treat this file as executable release code. Review changes to it with the same care as a shell script: deployers can run local commands, upload files, and run commands on remote hosts.

A small Hugo site

version: 1
app: example-site

defaults:
  target: production

targets:
  production:
    hosts: [example.com]
    dir: /var/www/example
    ssh:
      user: deploy
      key: ~/.ssh/example.pem

services:
  web:
    deployer: hugo
    config:
      source: .
      minify: true
      remote_subdir: web
      owner: www-data:www-data
    verify:
      - http:
          url: https://example.com/
          expect_status: 200

Targets

A target describes where a release goes. A target can name more than one host. SSH defaults to port 22 and supports a key or the running SSH agent.

targets:
  production:
    hosts: [app-1.example.com, app-2.example.com]
    dir: /var/www/example
    ssh:
      user: deploy
      port: 22
      key: ~/.ssh/example.pem

Services and order

Each service uses one deployer. Use needs to order related services:

services:
  web:
    deployer: hugo
    config: {source: apps/site, remote_subdir: web}

  nginx:
    deployer: nginx-vhost
    needs: [web]
    config:
      conf: nginx/example.conf

Run only part of a release with --service:

deliver plan --service web
deliver deploy --service web

Secrets

Declare names in the config and resolve values from the environment, a gitignored file, the macOS Keychain, 1Password, or SOPS. Plans show names and hidden placeholders, never values.

secrets:
  providers:
    - env
    - file: .env.deploy
    - keychain: {prefix: "example-"}
  define:
    DATABASE_PASSWORD: {}
    OPTIONAL_TOKEN: {required: false}

Check them without deploying:

deliver secrets

Keep secret values out of .deliver.yml. Use a provider and commit only the secret names. deliver plan --json also hides resolved values.

Checks

Checks run after activation. A failed check fails the release and rolls back reversible steps.

verify:
  - remote_file: web/index.html
  - contains:
      remote_file: web/index.html
      text: Example
  - http:
      url: https://example.com/health
      expect_status: 200
      retries: 5
      interval: 5

A check is the only thing that can roll a bad release back on its own, so deliver init scaffolds one for every deployer it writes, built from what it was actually told: the site root on the --host it was given for hugo and files, the appcast URL it just wrote for macos-app, nginx -t for nginx-vhost, and for docker-compose a container-up probe using the same -f files and -p project the deploy will bring the project up with. What it still cannot know โ€” whether the site is served from /, whether the deploy user can reach docker without sudo โ€” is printed as a note under the finding rather than guessed at inside the check, because a check that fails on a good release is worse than no check at all.

Services that end up with none are named by deliver plan, so shipping blind is a decision rather than an oversight:

โ–ธ Compiling plan
    2 service(s), 7 step(s): migrate โ†’ web
  no verification step: migrate โ€” a failed release there cannot roll itself back.

Where the logs are

deliver logs tails a docker-compose service with the deploy’s own compose invocation. Any other service says where to look, with exactly one of unit:, files: or command::

services:
  api:
    deployer: commands
    logs:
      unit: example-api        # journalctl -u example-api
  site:
    deployer: hugo
    logs:
      files:                   # tail -F
        - /var/log/nginx/example.com.access.log
        - /var/log/nginx/example.com.error.log
  worker:
    deployer: commands
    logs:
      command: "docker logs {follow} --tail {tail} worker"

command: runs verbatim on the target: {tail} becomes the line count and {follow} becomes -f when --follow is given. unit: and files: are read with the target’s own sudo, because that is how the deploy wrote them; command: is not, on the grounds that whoever wrote the command wrote all of it.

A logs: block wins over what the deployer declares, so a Compose project fronted by nginx can be pointed at the access log instead.

Version rules

Projects can refuse releases from the wrong branch, a dirty tree, or a commit that does not match its upstream:

versioning:
  require_clean: true
  require_pushed: true
  branch: main
  tag:
    enabled: true
    name: "v{version}"
    push: true
  after_tag:
    - command: ./scripts/publish-release.sh {version} {work}

Tags are created only after the release and its checks succeed. after_tag commands run only after the tag exists and a configured tag push succeeds. They can publish artifacts that earlier steps placed in {work}. They run only for a full deploy, not one selected with --service. Make these commands safe to retry: the tag and any earlier public step may already exist.

Use versioning.require_clean and versioning.require_pushed for production releases. They keep a local edit or an unpushed commit from becoming a release no one else can reproduce.