A tiny, stable, idempotent deployment helper to roll out a systemd service to multiple remote hosts using:
- Nushell for orchestration
- SSH/SCP for remote execution and file transfer
It reuses your existing SSH config at ~/.ssh/config. No extra SSH config in this repo.
Define hosts and services in a single TOML config (defaults to ./nudeploy.toml). Example:
[[hosts]]
name = "localhost"
ip = "127.0.0.1"
port = 22
user = "akagi201"
enable = true
group = "prod"
[[services]]
name = "axon"
src_dir = "./axon"
dst_dir = "/home/akagi201/axon"
unit_file = "axon.service" # relative to src_dir or absolute
sync_files = [
{ from = "foo.conf", to = "bar.conf" },
# Support downloading on remote directly:
# { from = "https://example.com/file.conf", to = "bar.conf" }
# Optional per-file chmod (applied with sudo after sync); defaults to "0644"
# { from = "bin/myapp", to = "bin/myapp", chmod = "0755" },
]
restart = true # restart service when files changed
enable = true # enable service (default true)You can target hosts by group or explicitly via --hosts hostA,hostB.
- macOS or Linux
- Nushell v0.90+ (newer preferred)
- bash (for the CLI wrapper)
- ssh, scp
- Remote machines run systemd and have sudo available if you need to install into /etc
- Remote tools: one of sha256sum | shasum | openssl must be available (most distros have at least one)
- Ensure Nushell is installed:
- brew install nushell
- Make the wrapper executable:
- chmod +x nudeploy/nudeploy.sh
The repo includes nupm.nuon. You can install via nupm and get a nudeploy bin on PATH (points to nudeploy.nu).
# Local install from the current directory (install nupm first per its docs)
nu -c 'nupm install --path .'
# Or install from Git (example)
nu -c 'nupm install --git https://github.com/longcipher/nudeploy'
# Now you can run it directly (nupm exposes `nudeploy` on PATH)
nudeploy --helpNote: After nupm install, nudeploy runs nudeploy.nu. The Bash wrapper nudeploy.sh still works standalone.
- Plan (no changes; shows what would change):
# All enabled services
nudeploy plan --group prod
# Single service
nudeploy plan --service axon --group prod- Deploy to a group (idempotent):
# All enabled services
nudeploy deploy --group prod --sudo
# Single service
nudeploy deploy --service axon --group prod --sudo- Deploy to specific hosts:
nudeploy deploy --service axon --hosts host1,host2 --sudo- Check status:
# All enabled services
nudeploy status --group prod
# Single service
nudeploy status --service axon --group prod- Restart without redeploying files:
# All enabled services
nudeploy restart --hosts host1
# Single service
nudeploy restart --service axon --hosts host1- List hosts (enabled by default):
nudeploy hosts --group prod- Run a shell command on targets:
nudeploy shell --group prod --cmd 'uname -a'- Run a playbook (line-by-line, stop on error):
nudeploy play --group prod --file playbooks/arch.nu- Download artifacts locally (curl + extract):
# All enabled downloads in config
nudeploy download
# Only selected names
nudeploy download --name openobserve
# Alternate config file
nudeploy download --config ./nudeploy.toml- --config: Path to config TOML (default: ./nudeploy.toml)
- --service: Service name from config (optional for plan/deploy/status/restart). If omitted, acts on all services with
enable = true. - --group: Hosts group
- --hosts: Comma-separated hostnames (SSH Host aliases)
- --cmd: Command to run for shell
- --file: Path to playbook file for
play(one command per line;#comments and blank lines ignored) - --sudo: Use sudo for systemd actions (daemon-reload/enable/start/restart) and installing unit files into /etc. All other file and directory operations run as the SSH user.
- --json: Emit JSON output suitable for CI
- --name: For
download, comma-separated artifact names to fetch (defaults to all enabled)
- Files are uploaded only if remote checksum differs
- Optional chmod is enforced after sync as the SSH user when
chmodis set on an item - Systemd daemon-reload runs only when unit changed
- Service is enabled once if not enabled
- Service is restarted only when changes detected (or restart mode forces it)
- nudeploy does not install software on remote machines; it only pushes your service unit/config and manages systemd
- Per-file permissions: set
chmod = "0755"on binaries you need to execute; default mode is0644. - Local
downloadsubcommand readsdownload_dirand[[downloads]]from your config, fetches with curl, extracts by suffix (tar.gz/tgz, tar.xz, zip, tar, gz, xz), and removes archives after extraction. - For sudo prompts, passwordless sudo is recommended for automation
Playbooks are simple text files that nudeploy executes remotely, one command per line. Ensure each line is idempotent. On the first failure (non-zero exit code), execution stops for that host and the failing line and command are reported. Use --sudo when commands require privileges.
Example playbooks/bootstrap.sh:
# Ensure curl exists
which curl >/dev/null 2>&1 || (apt-get update -y && apt-get install -y curl)
# Create user if missing
id -u deploy >/dev/null 2>&1 || useradd -m -s /bin/bash deploy
# Ensure directory and ownership
mkdir -p /opt/myapp && chown -R deploy:deploy /opt/myapp- The Bash wrapper only parses CLI; all orchestration lives in Nushell
- Install prerequisites (macOS):
brew install nushell- Ensure the CLI is executable and callable:
chmod +x nudeploy/nudeploy.sh
./nudeploy/nudeploy.sh --help- Define your config at
./nudeploy.tomlwith [[hosts]] and [[services]]. Host names are arbitrary labels; you can also set ip/user/port.
./nudeploy/nudeploy.sh plan \
--service example-service \
--group all \
--sudoWhen you’re ready:
./nudeploy/nudeploy.sh deploy \
--service example-service \
--group all \
--sudoTip: If Nushell is not on PATH or is named differently, set NU=/path/to/nu before running.
- Unit file is copied to
/etc/systemd/system/<service>.service. - sync_files entries copy local files (hash-compared) or download URLs on the remote; only changed files are installed.
- Idempotent: files only update on hash change;
daemon-reloadonly when unit changes; enable once; restart on change whenrestart=true.
Use the included example config/service to get a feel for the workflow:
# Plan the changes (no writes)
./nudeploy/nudeploy.sh plan \
--service axon \
--group prod \
--sudo
# Apply changes idempotently
./nudeploy/nudeploy.sh deploy \
--service axon \
--group prod \
--sudo
# Check status
./nudeploy/nudeploy.sh status \
--service example-service \
--group allOutputs:
- Plan shows which items would be uploaded per host.
- Deploy uploads/downloads only when checksums differ, reloads systemd if unit changed, enables once, and restarts only when needed.
- Status reports enabled/active states, plus a few systemctl properties in JSON mode.
Use --json to emit structured records per host that you can pipe to jq or parse in CI:
./nudeploy/nudeploy.sh deploy \
--service axon \
--group prod \
--sudo \
--jsonYou can fail a CI job if any host failed or if changes are found (policy dependent). Example:
./nudeploy/nudeploy.sh deploy --service foo --group all --sudo --json \
| jq -e 'all(.[]; .ok? // true)'Plan prints a detailed summary plus per-file actions by default. Use --json for structured data.
Note: The main entry is nudeploy.sh. You can symlink it to nudeploy to match the examples above.
- First SSH to a host prompts for key: we use
StrictHostKeyChecking=accept-newwhich will trust new hosts on first connect. - Permission denied writing files: destinations must be writable by the SSH user. Pre-create directories/files with proper ownership if needed.
- Permission denied (systemctl): configure passwordless sudo for systemctl for your deployment user, or run with a TTY if prompts are needed.
- Remote host missing hasher: needs one of
sha256sum,shasum, oropenssl. Installcoreutilsorperlpackages accordingly. - Remote not systemd: this tool targets systemd-based Linux. Non-systemd hosts aren’t supported.
- Unit not restarting: with
restart = true, restarts occur when files changed; otherwise not. - File destinations: ensure the destination parent directory exists or is creatable; we auto-create with
mkdir -pwhen needed.
NU: path to Nushell executable. Defaults tonuon PATH.
The project includes nupm.nuon:
{
name: "nudeploy",
version: "0.1.0",
description: "Idempotent systemd deploy helper over SSH using Nushell",
license: "MIT",
bins: { nudeploy: "nudeploy.nu" },
modules: ["lib.nu"],
}Example release flow (using nupm install from Git tags):
# Tag a release (version must match `nupm.nuon`)
git tag v0.1.0 && git push origin v0.1.0
# Verify install from the Git tag
nu -c 'nupm install --git https://github.com/longcipher/nudeploy --tag v0.1.0'
# When bumping versions:
# 1) Update `version` in nupm.nuon
# 2) Update README examples
# 3) Re-tag and push