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.