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"
shell = "zsh" # Optional: override shell wrapper (default "sh") to load correct profile/env
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)
This installs nudeploy to ~/.local/bin.
curl -fsSL https://raw.githubusercontent.com/longcipher/nudeploy/main/install.sh | bashEnsure ~/.local/bin is in your $PATH.
- 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 deploy --dry-run --group prod
# Single service
nudeploy deploy --dry-run --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 exec "uname -a" --group prod- Run a playbook (script execution, stop on error):
# Run silently (shows only last command output)
nudeploy exec playbooks/arch.sh --group prod
# Run verbosely (shows every command and its output)
nudeploy exec playbooks/arch.sh --group prod -v- 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 deploy/status/restart). If omitted, acts on all services with
enable = true. - --group: Hosts group
- --hosts: Comma-separated hostnames (SSH Host aliases)
- --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) - --dry-run: For
deploy, show what would change without applying (formerlyplan)
- 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 shell scripts (Bash syntax) that nudeploy executes remotely over a single SSH session.
- Single Session: Context is preserved between lines (e.g.,
cd /tmpaffects subsequent commands). - Automatic Error Handling:
set -eis automatically prepended, so execution stops immediately if any command fails. - Verbose Mode (
-v): Shows every executed command (prefixed with👉 [Line N]) and its output interleaved. - Quiet Mode (default): Shows only the output of the last executed command (useful for status checks).
Example playbooks/bootstrap.sh:
# Context is preserved
cd /tmp
curl -L -o app.tar.gz https://example.com/app.tar.gz
# Variables work
APP_DIR="/opt/app"
mkdir -p "$APP_DIR"
tar -xzf app.tar.gz -C "$APP_DIR"
# Logic works
if ! id -u deploy >/dev/null 2>&1; then
useradd -m -s /bin/bash deploy
fi- 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. - PATH issues in
exec: If commands likemiseare missing, ensure they are in~/.profile(for sh/bash) or~/.zprofile(for zsh). You can also setshell = "zsh"in[[hosts]]config to use zsh as the wrapper shell.
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