Deploys without holding
your breath.
Shipyard is an atomic-release deploy CLI: zero-downtime SSH/rsync deploys with health-gated promotion and automatic rollback. One static Go binary, one YAML config, no agent on the server.
The wrong shape
Most "deploy scripts" you'll inherit look like this:
ssh prod 'cd app && git pull && pm2 restart app'Between git pull and the restart, Apache is serving half-old, half-new files. Composer autoload sees a class the app doesn't yet have on disk. If the new code breaks at boot, pm2 restart still exits 0 — it returned the moment the process was alive, not when it was serving 200s. There's no rollback.
The right shape
Immutable release directories, atomic symlink flip, health-gated promotion, automatic rollback. This is what Capistrano has done since 2010 and what most "deploy scripts" in the wild still don't do.
# Pipeline shipyard runs for you:
pre_upload hooks (local) composer install --no-dev
npm run build
SSH connect + lockfile
SFTP upload artifact _incoming/20260610190921.tar.gz
extract releases/20260610190921/
symlink shared/ .env, storage/, bootstrap/cache/
post_extract hooks (remote) php artisan migrate --force
atomic flip ln -s + mv -Tf
post_flip hooks (remote) sudo systemctl reload apache2
health probe GET /healthz → expect "healthy"
FAIL ──→ rollback symlink, run on_rollback hooks, exit 4
PASS ──→ continue
auto-prune releases.keep = 5
release lockWhat you write
One shipyard.yaml:
app: webhook-relay
host:
ssh: ubuntu@1.2.3.4
identity_file: ~/.ssh/id_ed25519
release_root: /var/www/webhook-relay
artifact:
source: ./deploy-staging/release.tar.gz
format: tar.gz
shared:
files: [.env]
dirs: [storage, bootstrap/cache]
health_check:
url: https://api.webhook-relay.example.com/healthz
expect: "healthy"
retries: 10
delay: 3s
hooks:
pre_upload: ["composer install --no-dev", "npm run build"]
post_extract: ["php artisan migrate --force"]
post_flip: ["sudo systemctl reload apache2"]
on_rollback: ["sudo systemctl reload apache2"]Then:
$ shipyard deploy
[connect] ▸ dialing target=ubuntu@1.2.3.4:22
[connect] ▸ connected
[lock] ▸ acquired
[upload] ▸ uploading to=_incoming/20260610190921.tar.gz
[extract] ▸ extracting
[post-extract] ▸ php artisan migrate --force
[flip] ▸ flipped from=20260610185512 to=20260610190921
[post-flip] ▸ sudo systemctl reload apache2
[health] ▸ attempt n=1 status=200
[health] ▸ passed attempts=1 elapsed=147ms
[prune] ▸ deleted release=20260610150318
[done] ▸ deploy complete release=20260610190921Why not Capistrano / Deployer / Kamal
| Tool | Strength | Where it doesn't fit |
|---|---|---|
| Capistrano | Battle-tested Ruby ecosystem | Ruby-shaped — extension surface assumes Rails idioms |
| Deployer | PHP-native; Composer-aware | PHP-shaped — awkward for Node/Go/Python stacks |
| Kamal | Docker-first, Rails-team-shaped | If you don't run containers, mostly overhead |
| GitHub Actions deploy | CI-shaped; no extra tool | Build artifacts have to be produced wherever Composer lives — usually not on the server |
Shipyard's slot: language-agnostic, transport-agnostic, opinionated about safety, unopinionated about your stack. A single static binary you scp anywhere.
This page is deployed by Shipyard. The docs site you’re reading lives at /var/www/shipyard-web/current, which is a symlink Shipyard flipped after a health-gated promotion. Every push to main reaches you the same way. webhook-relay’s deploy migrates onto Shipyard 30 days after v0.1.0 once the failure modes shake out.