Fleet

Fleet

Fleet: VMs

# Register an existing VM (e.g. the Oracle box)
baryovm vm add oracle --host <your-vm-ip> --user opc --key ~/.ssh/id_ed25519

baryovm vm list
baryovm vm ping oracle           # SSH in, report host + Docker version
baryovm vm exec oracle -- uname -sr
baryovm vm exec oracle -- df -h /var/www
baryovm vm exec oracle -o json -- cat /etc/os-release
baryovm vm bootstrap oracle      # install Docker if missing
baryovm vm runner oracle --repo acme/app   # GitHub Actions self-hosted runner
baryovm vm runner remove oracle --repo acme/app
baryovm vm remove oracle         # forget it (does not destroy the machine)
baryovm vm forget-key oracle     # drop its recorded SSH host key

Host keys

The first connection to a machine records its SSH host key in ~/.baryovm/known_hosts and prints the fingerprint. Every connection after that must match, and one that does not stops the command:

host key for 203.0.113.10:22 changed
  stored   SHA256:aWxF...
  offered  SHA256:9KpQ...

A rebuilt VM is the ordinary reason, and so is someone answering on that address who should not be. BaryoVM cannot tell them apart, so it stops. If you rebuilt the machine, baryovm vm forget-key <name-or-host> drops the old key and the next connection learns the new one. There is no flag that turns the check off for every host.

Host keys in CI

Trust on first use assumes there is a first use. A CI runner has no memory between runs, so every run is the first one, and learning a key on each run means trusting whatever answers on that address, every time, with a key that can dump your database.

Strict mode refuses to learn. An unknown host is an error:

export BARYOVM_STRICT_HOST_KEYS=1     # or --strict-host-keys

So a CI job records the key first, and fails rather than trusting a new one:

- name: Pin the host key
  run: |
    mkdir -p ~/.baryovm
    ssh-keyscan -t ecdsa,ed25519 "$VM_HOST" >> ~/.baryovm/known_hosts

- name: Deploy
  env:
    BARYOVM_STRICT_HOST_KEYS: "1"
  run: baryovm stack release myapp

Better still, commit the expected known_hosts or keep it in a secret, so the pin is reviewed rather than fetched from the network on every run. ssh-keyscan at deploy time trusts whatever answers; a committed file does not.

The flag beats the environment, so --strict-host-keys=false is how a human turns it off for one command on a machine where the workflow sets it globally.

vm exec runs an arbitrary command on one named VM with the SSH key already in fleet.json. Everything after -- is the remote command, so its own flags reach the VM. Put sudo -n in the remote command when you need root; there is no --sudo flag. A non-zero remote exit becomes a non-zero CLI exit. Under -o json the envelope keeps stdout, stderr and exitCode as separate fields.

vm add connects once, over SSH, to detect the host's OS and records it in fleet.json; give --os linux|windows and it does not connect at all. Any host with a POSIX shell (Linux, macOS, FreeBSD) is recorded as linux. If the host cannot be reached (refused, timed out) or does not answer the probe, vm add still saves it, with no OS, which reads as Linux, and warns; run vm add again when the host is up, or pass --os windows. A host key that does not match the one on record is not "unreachable": vm add fails and saves nothing. Entries from before this existed are read as Linux, and an os that is neither linux nor windows is refused by every command that uses it.

On a Windows VM (OpenSSH Server, administrator account) only vm exec works so far. The words after -- run as one PowerShell command. Pass the command as one quoted argument, so your own shell leaves its spaces, quotes and pipes alone:

baryovm vm exec win -- 'Get-ChildItem "C:\Program Files" | Select-Object -First 3'

The exit code, and errors as plain text, come back under either OpenSSH default shell (cmd or powershell). vm add detects which one and records it as shell, so re-add a VM if you change its default shell. With --os windows nothing is detected and the shell is recorded as cmd; add --shell powershell when the host's default is powershell.exe. --shell is only valid with --os windows. A script is sent encoded, which makes it about three times longer, so a very long one is refused before connecting (cmd.exe takes a command line of 8191 characters, powershell.exe 32767). Every other command stops with a message naming it and the VM.

GitHub Actions runner

baryovm vm runner oracle --repo acme/app
baryovm vm runner oracle --repo acme/app --labels docker,gpu --memory-max 6G --cpu-quota 300%
baryovm vm runner remove oracle --repo acme/app

vm runner installs a self-hosted runner for one repo on a registered VM. It needs the gh CLI logged in with admin rights on the repo, and Docker on the VM (vm bootstrap). It creates a github-runner user in the docker group, unpacks the runner under /opt/github-runner/<owner>__<repo> (lowercase), registers it, installs it as a systemd service and checks that the service is active.

Flag Default
--repo required owner/name
--name <vm>-<repo> runner name; reruns use --replace, so one registration stays
--labels none extra labels on top of self-hosted, Linux and the arch
--version latest release pin a runner version such as 2.320.0
--memory-max 4G systemd MemoryMax for the service
--cpu-quota 200% systemd CPUQuota for the service

It is safe to rerun. A configured, running runner is reported as alreadyReady and nothing changes. A stopped one is started, and one with no service gets the service installed. A rerun after a crash continues from the step that failed. If you pass --memory-max or --cpu-quota explicitly, they are applied even to a runner that is otherwise ready: the drop-in is rewritten and the service restarted, and the result reports limitsApplied. Leave them out and a ready runner is not touched.

  • A repo whose visibility is not private is refused before BaryoVM connects to the VM, because a fork pull request could run its code there. That includes enterprise internal repos, which every enterprise member can see and fork.
  • Also turn on "Require approval for all outside collaborators" for fork pull request workflows in the repo's Settings, Actions, General page.
  • The runner tarball is checked against the sha256 digest GitHub publishes for the release asset before it is extracted. A release with no digest is refused.
  • The memory and CPU limits cover the runner and what it runs directly. Containers a job starts through Docker (Testcontainers, docker run) run under dockerd and are not bounded by them.
  • vm runner remove stops and uninstalls the service, unregisters the runner and deletes its directory. If no remove token can be fetched, or GitHub rejects it, it removes the local runner anyway and says so, and you have to delete the entry on the repo's Settings, Actions, Runners page by hand. If that local removal fails too, the command fails and leaves the directory in place for another try.

Provision + deploy in one (untested, billable)

# Preview only, no AWS calls:
baryovm vm provision web1 --provider lightsail --key ~/.ssh/id_ed25519 --dry-run

# The one button: provision (if new) -> install Docker -> deploy
baryovm up web1 --provider lightsail --key ~/.ssh/id_ed25519 \
  --image nginx:alpine --container site -p 80:80

# On an already-registered VM, up just bootstraps + deploys:
baryovm up oracle --image app:latest --container app -p 80:8080