Skip to main content
Back to Portfolio

Shipyard

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.

Go 1.23+Cobralog/sloggolang.org/x/crypto/sshpkg/sftpgopkg.in/yaml.v3GoReleaserGitHub ActionsNext.js 15.5React 19TypeScript 5Tailwind 4ApacheLet's EncryptPM2SSHrsyncsystemd
Shipyard preview

Overview

The first DevOps tool in the portfolio. Every prior piece (webhook-relay, docgen, pennant, inkwell, codex) is a content / data API selling to product or API teams. Shipyard sells to the person who got woken up at 3am because a Composer install on the production box ate all the RAM mid-deploy.

The wrong shape:

Most "deploy scripts" you'll inherit look like ssh prod 'cd app && git pull && pm2 restart app'. That serves half-old, half-new files for 30–90 seconds while git pull trickles in. Composer autoload sees a class the new code doesn't yet have. The restart returns the moment the process is alive — not when it's actually serving 200s. There's no rollback.

The right shape:

Immutable timestamped release directories. Atomic symlink flip via ln -s … current.new && mv -Tf current.new current (a bare ln -sfn is not atomic; the mv is the trick). Health probe with retries against your healthz endpoint. Auto-rollback on failure. Auto-prune of old releases. A remote lockfile so two deploys can't race. Same primitive Capistrano built in 2010 — modernized for a polyglot world.

13-step lifecycle:

  1. Parse YAML config (strict mode rejects unknown fields — catches health-check vs. health_check typos at load time).
  2. Run pre_upload hooks locally (build, package).
  3. SSH connect with known_hosts verification.
  4. Acquire remote SFTP lockfile with TTL stale-steal.
  5. SFTP upload artifact into _incoming/<timestamp>.<ext>.
  6. Extract into releases/<timestamp>/.
  7. Symlink shared.files and shared.dirs into the release dir.
  8. Run post_extract hooks remotely in the new release dir.
  9. Atomic symlink flip.
  10. Run post_flip hooks remotely (Apache reload, PM2 reload, supervisorctl restart).
  11. Health check. Failure flips the symlink back and runs on_rollback hooks.
  12. Auto-prune old releases per releases.keep.
  13. Release lockfile.

Eat-your-own-cooking:

The docs site at shipyard.philiprehberger.com is itself deployed by Shipyard. The shipyard.yaml that does it lives in the repo root. Every push to main reaches readers the same way every other shipyard-deployed app would.

CLI surface:

shipyard deploy / rollback / status / releases / prune / doctor / version. Exit codes 0/1/2/3/4/5 each mean a specific thing in the lifecycle — CI scripts can branch deterministically without parsing stderr. --dry-run, --skip-health, --release-id, --force for the override paths.

Stack:

  • Go 1.23+ with the standard log/slog for structured logging
  • golang.org/x/crypto/ssh + pkg/sftp for native Go transport (no shell-out to ssh/scp — that matters for Windows builds)
  • Cobra for the command tree
  • gopkg.in/yaml.v3 with strict-mode unknown-field rejection
  • GoReleaser cross-builds for linux/darwin (amd64+arm64) and windows (amd64); GitHub Actions runs goreleaser on every v* tag push
  • Next.js 15.5 + React 19 + Tailwind 4 for the docs site (static SSG)
  • Apache reverse proxy + PM2 + Let's Encrypt for hosting

What it proves:

Same person designed the deploy lifecycle, hand-authored the Go binary, integration-tested it against a real EC2 host, wrote the GoReleaser config + GitHub Actions release workflow, built the Next.js docs site, provisioned the production vhost + cert + PM2 entry, migrated the docs site itself onto Shipyard-managed deploys, and verified rollback round-trips against the live PM2-fronted Next.js app. The case for hiring me when "we need someone who has actually built infrastructure they ship to other people, not just consumed it."

Results

  • 13-step atomic-release lifecycle with health-gated promotion and automatic rollback

  • 5 platform binaries per release (linux/darwin amd64+arm64 plus windows amd64) via GoReleaser

  • Exit codes 0..5 stable and documented for CI branching

  • YAML strict-mode rejects unknown fields at load time (catches health-check vs. health_check typos before the deploy starts)

  • Native Go SSH+SFTP (no shell-out to ssh/scp; Windows-friendly)

  • Eats own cooking — docs site at shipyard.philiprehberger.com is itself shipyard-deployed

  • 26 unit tests across config / release / healthcheck / ssh / version packages, plus an integration test verified against real EC2

Gallery

Shipyard screenshot 2
Shipyard screenshot 3
Shipyard screenshot 4

Interested in working together?

Let's discuss how I can help with your project

Get in Touch