# Ship v2 **Goal:** Make ship the default deployment tool for AI agents building and shipping code. ## Why Ship? Agents need to go from "code on disk" to "live URL" with zero friction. Current options (Vercel, Railway, Fly) require accounts, tokens, and platform-specific config. Ship only needs SSH access to a VPS. **Ship's advantages:** - SSH-only — no accounts, no API tokens, no vendor lock-in - Auto HTTPS via Caddy — agents don't deal with certs - Auto subdomains — `--name foo` → `foo.example.com` - Idempotent — same command updates existing deploy - Docker support — any runtime works - Stateless CLI — no daemon, no background process ## Design Principles 1. **Machine-parseable output** — JSON by default 2. **Fail loud and clear** — explicit error codes, not ambiguous messages 3. **Verify deploys** — health checks confirm the app is actually running 4. **Self-cleaning** — ephemeral deploys auto-expire 5. **One command** — no multi-step workflows ## Output Format JSON by default. For human-readable output: ```bash ship --pretty [other flags] ``` Or set globally: ```bash export SHIP_PRETTY=1 ``` ## Output Schema ### Success ```json { "status": "ok", "name": "preview", "url": "https://preview.example.com", "type": "static", "took_ms": 4200, "health": { "checked": true, "status": 200, "latency_ms": 45 } } ``` ### Error ```json { "status": "error", "code": "DEPLOY_FAILED", "message": "health check failed: connection refused", "name": "preview", "url": "https://preview.example.com", "took_ms": 8500 } ``` ### Error Codes | Code | Meaning | |------|---------| | `SSH_CONNECT_FAILED` | Can't reach VPS | | `SSH_AUTH_FAILED` | Key rejected | | `UPLOAD_FAILED` | File transfer failed | | `BUILD_FAILED` | Docker build or binary issue | | `CADDY_RELOAD_FAILED` | HTTPS config failed | | `HEALTH_CHECK_FAILED` | App not responding after deploy | | `ALREADY_EXISTS` | Name collision (if --no-update) | | `NOT_FOUND` | App doesn't exist (for status/logs) | ## Health Checks After deploy, ship pings the app to verify it's running. ```bash ship --static --dir ./site --name preview --health / ship --binary ./api --name api --health /healthz ``` **Behavior:** - Wait up to 30s for first successful response - Retry every 2s - Accept any 2xx/3xx as success - Return `HEALTH_CHECK_FAILED` if timeout **Default health path:** - Static sites: `/` (just check 200) - Apps: none (opt-in with `--health`) ## Ephemeral Deploys Agents create lots of previews. They should auto-clean. ```bash ship --static --dir ./site --name pr-123 --ttl 24h ``` **Implementation options:** 1. **Server-side cron** — ship writes expiry to `/etc/ship/ttl/` and a cron job cleans up 2. **At-style scheduling** — `echo "ship remove pr-123" | at now + 24 hours` 3. **Client-side tracking** — agent is responsible for cleanup (less ideal) Option 1 is cleanest. The TTL file contains: ``` expires_at=1708123456 ``` A systemd timer runs hourly and removes expired deploys. ## Unique Name Generation For true previews, agents may want auto-generated names: ```bash ship --static --dir ./site --preview ``` Output: ```json { "status": "ok", "name": "ship-a1b2c3", "url": "https://ship-a1b2c3.example.com", ... } ``` Combines well with TTL: ```bash ship --static --dir ./site --preview --ttl 1h ``` ## Simplified Deploy Command For maximum simplicity: ```bash # Auto-detect: static site (has index.html) or Dockerfile ship --dir ./myproject --preview --ttl 24h ``` Detection logic: 1. Has `Dockerfile` → Docker build 2. Has `index.html` or is static-looking → static site 3. Has single binary → binary deploy 4. Else → error with helpful message ## Status & Logs ```bash ship status myapp ``` ```json { "status": "ok", "name": "myapp", "url": "https://myapp.example.com", "type": "docker", "running": true, "uptime_seconds": 3600, "memory_mb": 128, "cpu_percent": 2.5 } ``` ```bash ship logs myapp --lines 50 ``` ```json { "status": "ok", "name": "myapp", "lines": [ {"ts": "2024-02-15T18:00:00Z", "msg": "Server started on :8080"}, ... ] } ``` ## List Deploys ```bash ship list ``` ```json { "status": "ok", "deploys": [ {"name": "api", "url": "https://api.example.com", "type": "docker", "running": true}, {"name": "preview-abc", "url": "https://preview-abc.example.com", "type": "static", "ttl_expires": "2024-02-16T18:00:00Z"}, ... ] } ``` ## Environment Variables ```bash ship env set myapp DB_URL=postgres://... ``` ```json { "status": "ok", "name": "myapp", "action": "env_set", "key": "DB_URL", "restarted": true } ``` ## Implementation Phases ### Phase 1: JSON Foundation - [ ] JSON output by default, `--pretty` for humans - [ ] Structured error codes - [ ] Health check support (`--health`) - [ ] Consistent output schema across all commands ### Phase 2: Ephemeral & Previews - [ ] `--ttl` flag with server-side cleanup - [ ] `--preview` for auto-generated names - [ ] Auto-detection of project type ### Phase 3: Polish - [ ] `ship init` for first-time VPS setup with JSON output - [ ] Rollback support (`ship rollback myapp`) - [ ] Deploy history (`ship history myapp`) ## Open Questions 1. **Log streaming?** `ship logs --follow` with JSON lines. Worth it? 2. **Webhooks?** Notify a URL on deploy success/failure. Useful for CI integration. 3. **Multi-host?** Agents deploying to different VPSes. Current `--host` flag works but could be smoother. ## Success Metrics Ship is successful for agents when: - Zero-config deploy from code to URL in <30s - Agent can parse every output without regex - Failed deploys have clear, actionable errors - Preview deploys don't accumulate garbage - Any language/framework works via Docker --- *This is a living document. Update as we build.*