Fast parallel file deployment to multiple hosts over SSH.
$ ship ./myapp:/usr/local/bin/myapp host1 host2 host3 host4
host1 host2 host3 host4
OK 85% 24M OK INS
- Parallel uploads - configurable concurrency with
-j - MD5 skip - skip upload if remote file matches
- Gzip compression - auto-enabled for large files
- Verified install - MD5-check staged bytes before replacing destination
- Atomic install - upload to temp, then mv into place
- Sudo support - upload as user, install as root
- Progress display - per-host status with transfer speed
- Stall detection - fail fast if transfer hangs
- Restart hook - run command after successful deploy
curl -fsSL https://github.com/piranha/ship/releases/latest/download/ship-$(uname -s)-$(uname -m) \
-o ~/bin/ship && chmod +x ~/bin/shipOr grab a binary from releases.
Note
macOS users: if you get a quarantine warning, run xattr -d com.apple.quarantine ~/bin/ship
ship [options] <local_path:remote_dest> <host...>
host # use ssh config defaults
user@host # specify user
host:/other/path # override dest for this host
user@host:/path # override both
# Deploy binary to 3 hosts
ship ./myapp:/usr/local/bin/myapp web1 web2 web3
# With sudo (upload as user, mv as root)
ship --sudo ./nginx.conf:/etc/nginx/nginx.conf web1 web2
# Restart service after deploy
ship --sudo --restart "systemctl restart myapp" ./myapp:/usr/local/bin/myapp web1 web2
# Override dest per host
ship ./config:/etc/myapp/config web1:/etc/myapp/config.web1 web2
# Skip MD5 check, force upload
ship --skip-md5 ./myapp:/usr/local/bin/myapp web1
# Custom SSH options
ship --ssh-opts "-i ~/.ssh/deploy_key -o ConnectTimeout=10" ./app:/opt/app host1| Option | Default | Description |
|---|---|---|
-j, --jobs <N> |
min(hosts, 8) | Max parallel uploads |
--ssh <path> |
ssh | SSH binary |
--ssh-opts <str> |
-oBatchMode=yes -oConnectTimeout=5 | SSH options |
--port <port> |
- | Default SSH port |
--user <user> |
- | Default SSH user |
--skip-md5 |
false | Skip pre-upload MD5 check (staged bytes are still verified) |
--compress |
auto | Force gzip compression |
--no-compress |
- | Disable compression |
--compress-level <1-9> |
1 | Gzip level |
--chmod <mode> |
0755 | File mode (octal) |
--no-chmod |
- | Skip chmod |
--sudo |
false | Use sudo for install |
--sudo-cmd <cmd> |
sudo -n | Sudo command |
--install-owner <u:g> |
- | Set owner:group via sudo |
--timeout <sec> |
30 | SSH timeout |
--stall-timeout <sec> |
10 | Fail if no progress for N sec |
--restart <cmd> |
- | Run after successful install |
--quiet |
false | No progress output |
--keep-tmp-on-fail |
false | Keep temp file on failure |
host1 host2 host3 +2 more
OK 85% 24M INS +1✓ 0✗ 1↻
- Percentage + speed during upload (e.g.,
85% 24M= 85% done at 24 MB/s) - Status codes:
OKdone,SKIPmd5 matched,ERRfailed,INSinstalling,RSTrestarting,STALLtransfer hung - Summary for overflow hosts: ✓ done/skipped, ✗ failed, ↻ running
Display auto-fits terminal width; excess hosts shown in summary.
When stdout isn’t a TTY, ship prints one line per host on completion (e.g. host1 OK). Failures are listed with messages at the end.
- MD5 check - compare local MD5 with remote (skip if match)
- Upload - stream file to temp path via SSH (optionally gzipped)
- Verify - require the staged file's MD5 to match before changing destination
- Install - chmod + atomic mv to final dest (via sudo if enabled)
- Restart - run restart command if specified
Temp file pattern: /tmp/ship.{basename}.{pid}.new
- Remote: any POSIX system with
cat,mv,chmod,md5sum(or busybox equivalents) - Local: Zig 0.15+ to build
0- all hosts succeeded1- one or more hosts failed2- usage error