Releases
Release: sync source, build, deploy (config-driven)
stack release is the full deploy in one command: back up → sync source → build
images on the VM → compose up. It's driven by a JSON manifest in your project, so the
pipeline is generic (no app-specific logic). The compose dir (with its .env) is never in
the sync list, so secrets can't be wiped by --delete.
baryovm.release.json:
{
"localRoot": "~/repos/BaryoClub",
"remoteRoot": "/home/deploy/baryoclub-src/BaryoClub",
"sync": ["api", "web"],
"exclude": ["bin", "obj", "node_modules", ".next", ".git"],
"builds": [
{ "image": "baryoclub-api:latest", "dockerfile": "deploy/Dockerfile.api", "context": "api" },
{ "image": "baryoclub-web:latest", "dockerfile": "deploy/Dockerfile.web", "context": "web",
"args": { "NEXT_PUBLIC_API_URL": "" } }
]
}
baryovm stack add baryoclub --vm oracle --path …/deploy \
--release-file ~/repos/BaryoClub/baryovm.release.json \
--db-container deploy-postgres-1 --db-name baryoclub --env-file .env
baryovm stack release baryoclub # backup → sync → build → deploy
baryovm stack release baryoclub --no-backup # skip the pre-release backup
baryovm stack release baryoclub --no-build # just sync + compose up
baryovm stack release baryoclub --config ./other.release.json
Per-build options: image, dockerfile, context (both relative to remoteRoot), args
(build args), noCache.
Manifest options:
| key | default | what it does |
|---|---|---|
localRoot |
required | local source root, ~ allowed |
remoteRoot |
required | absolute destination on the VM |
sync |
required | paths under localRoot to rsync. A trailing slash follows rsync's own rule |
exclude |
bin, obj, node_modules, .next, .git |
rsync-style excludes, honoured by both transports (see below) |
builds |
none | images to build on the VM after syncing |
sudo |
false |
run the release's remote steps as root: the far side of the rsync, the image builds, preDeploy, postDeploy and the closing compose up. A stack registered --sudo turns this on for its own release and a manifest cannot turn it back off |
noCompose |
false |
skip docker compose up at the end |
postDeploy |
none | commands to run on the VM after syncing and building |
verify |
none | commands run on the VM after postDeploy that decide whether the release worked. Every one must exit zero |
verifyAttempts |
5 |
maximum number of times the whole verify plan may run, not retries after a first run |
verifyDelaySeconds |
3 |
wait between attempts |
Moving a hand-run container into a stack
A VM you already have is usually running containers started with docker run. If your compose file
gives a service the same container_name, compose cannot create it while the old container exists.
stack release, stack deploy and stack update check this before they back up, sync, build or
pull, and stop with the container's name and the command that frees it:
container name "umbraco-pwa-demo" is held by a container compose does not manage, probably started with docker run: check it is safe to remove, then run `docker rm -f 'umbraco-pwa-demo'` on the VM and try again
BaryoVM never removes that container itself. Check first that its data lives in a volume, not in the
container. If the old container used a named volume, declare it external: true in the compose file.
Otherwise compose creates a new, empty, project-prefixed volume and the app starts with no data.
volumes:
umbraco-pwa-demo-data:
external: true
Verifying a release
A release that reports success without asking the running site anything cannot tell a working deploy from one that landed and serves the wrong thing. That is not hypothetical: a demo site served its framework's "welcome, now install me" screen through two releases, a perfectly valid 200 at the exact URL every listing pointed at, and every command in the deploy had succeeded.
So after postDeploy, stack release runs a verification plan and fails the release if it does
not pass:
- If the manifest sets
verify, those commands run. Point this at a check script the repository already has rather than reimplementing one here. - Otherwise, if the stack has a
healthUrl, it becomes acurl --failprobe. - If neither is set, the release warns that it verified nothing and still succeeds. An interactive release is not an unattended update, so this is a warning rather than a refusal.
The probe runs on the VM, not locally. A healthUrl is usually loopback, which from your
laptop either fails or, worse, reaches something local and passes.
One rule worth carrying into any check script: a "must not contain" assertion only means something after a real body has arrived. An empty response, a 502 and a DNS failure all satisfy a negative grep, so assert the status and a minimum size first.
Sync transports and excludes
With local rsync on your PATH, sync entries go through rsync. Without it they go as a tar
stream over the SSH session (Linux hosts), and baryovm doctor names which one a release will
use. The stream is written with Go's archive/tar straight into tar -x on the host, never held
in memory. Once the extract has exited cleanly, files that are on the host and not local are
removed, so the result matches rsync --delete. Two differences: there is no delta transfer, and
files arrive owned by the SSH user (root under sudo) rather than with your local owner.
Setuid and setgid bits are dropped.
exclude follows rsync's matching under both transports:
- A pattern with no
/matches a name at any depth:.env,*.map. - A pattern with a
/matches the end of the path at any depth:secrets/.envalso excludesa/secrets/.env. - A leading
/anchors to the transfer root:/secrets/.envmatches only the top level. With asyncentry like"out"(no trailing slash) rsync's paths start without/, so anchor as/out/secrets/.env. *and?stop at/,**crosses it. A leading**/also matches zero directories, so**/.envexcludes the top-level.envtoo. A trailing/matches directories only.- Excluded paths are not sent, are not deleted from the host, and a host directory that holds one is not deleted either.
The tar transport supports only that subset. A pattern using character classes ([ab]), a
backslash, ***, a leading // or a filter prefix (!, + , - ) is refused with an error before anything
touches the host; simplify it or install rsync.
Pruning is confined to the entry's destination directory. It is refused when that directory, or
one between remoteRoot and it, is a symlink on the host. Each removal re-checks, on the host
and just before the delete, that the path's parent still resolves inside the release dir. A
window of an instant remains between that check and the delete, which POSIX tools cannot close,
so do not release into a directory that other users can write to. A symlink to a single file
cannot be a sync entry under tar.
The trailing slash on sync entries
This is rsync's rule, kept rather than invented, and it decides where files land:
"out"copies the directory, givingremoteRoot/out/..."out/"copies its contents, givingremoteRoot/...
Get it wrong on a webroot and --delete empties the directory the site is served from while
putting the site one level down. Check with a dry run before the first release of a new stack:
rsync -az --delete --dry-run --itemize-changes --rsync-path="sudo -n rsync" \
-e "ssh -i ~/.ssh/your_key" ~/repos/site/out/ user@host:/var/www/site/
When a release refuses to sync the compose directory
The compose directory is the stack's --path, and it holds the .env. rsync --delete removes any
file on the VM that is not in the local source, so a sync that reaches that directory would delete
the .env. stack release checks for this after the pre-release backup and before the first sync
entry runs. It stops if any entry lands on the compose directory or on a directory above it.
Where an entry lands follows the trailing slash rule above. With "remoteRoot": "/opt/app" and a
stack registered with --path /opt/app/deploy:
"deploy"lands on/opt/app/deployand is refused."out/"lands on/opt/app, which containsdeploy, and is refused."api","web"and"deploy-assets"are allowed. A name that only starts withdeployis not inside it.
The first case fails with:
sync entry "deploy" lands on /opt/app/deploy, which contains the compose directory /opt/app/deploy: rsync --delete would remove its .env. Sync the application directories individually instead of the root that holds them
Under -o json the same text is the envelope's error, with "action": "stack release".
To fix it, list the directories the release needs one by one, leaving the compose directory out:
"sync": ["api", "web"] rather than "sync": ["out/"]. A remoteRoot that does not contain the
compose directory passes too.
What the check does not do:
- It skips a manifest with
"noCompose": true. A static site's stack directory is its webroot, so it is the same path asremoteRooton purpose and there is no.envto protect. - It compares the paths as written, after trimming spaces and one trailing slash. It does not
resolve symlinks or
.., and it does not look at the VM, so a destination that reaches the compose directory through a symlink is not caught. - It is the same with or without
--sudo.