diff options
| -rw-r--r-- | DESIGN_SPEC.md | 586 |
1 files changed, 0 insertions, 586 deletions
diff --git a/DESIGN_SPEC.md b/DESIGN_SPEC.md deleted file mode 100644 index 51342d4..0000000 --- a/DESIGN_SPEC.md +++ /dev/null | |||
| @@ -1,586 +0,0 @@ | |||
| 1 | # VPS Deployment CLI - Design Spec | ||
| 2 | |||
| 3 | ## Overview | ||
| 4 | Standalone CLI tool for deploying Go apps and static sites to a VPS with automatic HTTPS via Caddy. | ||
| 5 | |||
| 6 | ## Server Setup (One-time) | ||
| 7 | |||
| 8 | ### Build and Install the Deploy CLI on Your Laptop | ||
| 9 | ```bash | ||
| 10 | # Clone and build the deploy CLI | ||
| 11 | git clone <this-repo> deploy | ||
| 12 | cd deploy | ||
| 13 | go build -o ~/bin/deploy ./cmd/deploy | ||
| 14 | # Or: go install ./cmd/deploy (if ~/go/bin is in your PATH) | ||
| 15 | |||
| 16 | # Initialize a fresh VPS (one-time) | ||
| 17 | deploy init --host user@your-vps-ip | ||
| 18 | |||
| 19 | # The CLI will SSH into the VPS and: | ||
| 20 | # - Detect OS (Ubuntu/Debian supported) | ||
| 21 | # - Install Caddy from official repository | ||
| 22 | # - Configure Caddy to import `/etc/caddy/sites-enabled/*` | ||
| 23 | # - Create `/etc/ship/env/` directory for env files | ||
| 24 | # - Create `/etc/caddy/sites-enabled/` directory | ||
| 25 | # - Enable and start Caddy service | ||
| 26 | # - Verify installation | ||
| 27 | # | ||
| 28 | # State is stored locally at ~/.config/deploy/state.json | ||
| 29 | ``` | ||
| 30 | |||
| 31 | ## CLI Commands | ||
| 32 | |||
| 33 | All commands run from your laptop and use `--host` to specify the VPS. | ||
| 34 | |||
| 35 | ### Initialize VPS (One-time Setup) | ||
| 36 | ```bash | ||
| 37 | # On a fresh VPS, run this once to install all dependencies | ||
| 38 | deploy init --host user@your-vps-ip | ||
| 39 | |||
| 40 | # This will SSH to the VPS and: | ||
| 41 | # - Install Caddy | ||
| 42 | # - Configure Caddy to use sites-enabled pattern | ||
| 43 | # - Create /etc/ship/env/ directory for env files | ||
| 44 | # - Enable and start Caddy | ||
| 45 | # | ||
| 46 | # State is stored locally at ~/.config/deploy/state.json | ||
| 47 | ``` | ||
| 48 | |||
| 49 | ### Deploy Go App | ||
| 50 | ```bash | ||
| 51 | # Build locally, then deploy | ||
| 52 | deploy --host user@vps-ip --domain api.example.com | ||
| 53 | |||
| 54 | # With custom name (defaults to directory name or binary name) | ||
| 55 | deploy --host user@vps-ip --name myapi --domain api.example.com | ||
| 56 | |||
| 57 | # Deploy specific binary | ||
| 58 | deploy --host user@vps-ip --binary ./myapp --domain api.example.com | ||
| 59 | |||
| 60 | # With environment variables | ||
| 61 | deploy --host user@vps-ip --domain api.example.com --env DB_HOST=localhost --env API_KEY=secret | ||
| 62 | |||
| 63 | # Or from env file | ||
| 64 | deploy --host user@vps-ip --domain api.example.com --env-file .env.production | ||
| 65 | ``` | ||
| 66 | |||
| 67 | ### Deploy Static Site | ||
| 68 | ```bash | ||
| 69 | # Deploy current directory | ||
| 70 | deploy --host user@vps-ip --static --domain example.com | ||
| 71 | |||
| 72 | # Deploy specific directory | ||
| 73 | deploy --host user@vps-ip --static --dir ./dist --domain example.com | ||
| 74 | |||
| 75 | # With custom name | ||
| 76 | deploy --host user@vps-ip --static --name mysite --dir ./build --domain example.com | ||
| 77 | ``` | ||
| 78 | |||
| 79 | ### Management Commands | ||
| 80 | ```bash | ||
| 81 | # List all deployed apps/sites | ||
| 82 | deploy list --host user@vps-ip | ||
| 83 | |||
| 84 | # Remove deployment | ||
| 85 | deploy remove myapp --host user@vps-ip | ||
| 86 | |||
| 87 | # View logs | ||
| 88 | deploy logs myapp --host user@vps-ip | ||
| 89 | |||
| 90 | # Restart app | ||
| 91 | deploy restart myapp --host user@vps-ip | ||
| 92 | |||
| 93 | # View status | ||
| 94 | deploy status myapp --host user@vps-ip | ||
| 95 | |||
| 96 | # Set/update environment variables | ||
| 97 | deploy env myapi --host user@vps-ip --set DB_HOST=localhost --set API_KEY=secret | ||
| 98 | |||
| 99 | # View environment variables (secrets are masked) | ||
| 100 | deploy env myapi --host user@vps-ip | ||
| 101 | |||
| 102 | # Load from file | ||
| 103 | deploy env myapi --host user@vps-ip --file .env.production | ||
| 104 | |||
| 105 | # Remove specific env var | ||
| 106 | deploy env myapi --host user@vps-ip --unset API_KEY | ||
| 107 | ``` | ||
| 108 | |||
| 109 | ### Optional: Config File | ||
| 110 | ```bash | ||
| 111 | # Create ~/.config/deploy/config to avoid typing --host every time | ||
| 112 | mkdir -p ~/.config/deploy | ||
| 113 | cat > ~/.config/deploy/config <<EOF | ||
| 114 | host: user@your-vps-ip | ||
| 115 | EOF | ||
| 116 | |||
| 117 | # Now you can omit --host flag | ||
| 118 | deploy --domain api.example.com | ||
| 119 | deploy list | ||
| 120 | ``` | ||
| 121 | |||
| 122 | ## Deployment Workflow | ||
| 123 | |||
| 124 | ### Initial VPS Setup | ||
| 125 | ```bash | ||
| 126 | # 1. Build the deploy CLI (in this repo) | ||
| 127 | cd /path/to/deploy-repo | ||
| 128 | go build -o ~/bin/deploy ./cmd/deploy | ||
| 129 | |||
| 130 | # 2. Initialize fresh VPS from your laptop | ||
| 131 | deploy init --host user@your-vps-ip | ||
| 132 | # This SSHs to VPS, installs Caddy, creates directories | ||
| 133 | |||
| 134 | # 3. Optional: Create config file to avoid typing --host | ||
| 135 | mkdir -p ~/.config/deploy | ||
| 136 | echo "host: user@your-vps-ip" > ~/.config/deploy/config | ||
| 137 | |||
| 138 | # Done! VPS is ready for deployments | ||
| 139 | ``` | ||
| 140 | |||
| 141 | ### Go Apps | ||
| 142 | ```bash | ||
| 143 | # 1. Build locally | ||
| 144 | GOOS=linux GOARCH=amd64 go build -o myapp | ||
| 145 | |||
| 146 | # 2. Deploy from laptop (with config file) | ||
| 147 | deploy --binary ./myapp --domain api.example.com | ||
| 148 | |||
| 149 | # Or specify host explicitly | ||
| 150 | deploy --host user@vps-ip --binary ./myapp --domain api.example.com | ||
| 151 | # CLI will: scp binary to VPS, set up systemd, configure Caddy, start service | ||
| 152 | ``` | ||
| 153 | |||
| 154 | ### Static Sites | ||
| 155 | ```bash | ||
| 156 | # 1. Build locally | ||
| 157 | npm run build | ||
| 158 | |||
| 159 | # 2. Deploy from laptop | ||
| 160 | deploy --static --dir ./dist --domain example.com | ||
| 161 | # CLI will: rsync files to VPS, configure Caddy | ||
| 162 | ``` | ||
| 163 | |||
| 164 | ## App Requirements | ||
| 165 | |||
| 166 | ### Go Apps Must: | ||
| 167 | - Listen on HTTP (standard `http.ListenAndServe`) | ||
| 168 | - Accept port via env var `PORT` or flag `--port` | ||
| 169 | - Bind to `localhost` or `127.0.0.1` only (Caddy will handle external traffic) | ||
| 170 | |||
| 171 | **Example main.go:** | ||
| 172 | ```go | ||
| 173 | package main | ||
| 174 | |||
| 175 | import ( | ||
| 176 | "flag" | ||
| 177 | "fmt" | ||
| 178 | "net/http" | ||
| 179 | "os" | ||
| 180 | ) | ||
| 181 | |||
| 182 | func main() { | ||
| 183 | port := flag.String("port", os.Getenv("PORT"), "port to listen on") | ||
| 184 | flag.Parse() | ||
| 185 | |||
| 186 | if *port == "" { | ||
| 187 | *port = "8080" // fallback for local dev | ||
| 188 | } | ||
| 189 | |||
| 190 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { | ||
| 191 | w.Write([]byte("Hello World")) | ||
| 192 | }) | ||
| 193 | |||
| 194 | addr := "127.0.0.1:" + *port | ||
| 195 | fmt.Printf("Listening on %s\n", addr) | ||
| 196 | http.ListenAndServe(addr, nil) | ||
| 197 | } | ||
| 198 | ``` | ||
| 199 | |||
| 200 | ## CLI Implementation | ||
| 201 | |||
| 202 | ### How It Works | ||
| 203 | The CLI is a single binary that runs on your laptop and orchestrates deployments via SSH: | ||
| 204 | |||
| 205 | 1. **Connect**: Uses SSH to connect to VPS (via `--host` flag or config file) | ||
| 206 | 2. **Transfer**: Copies files to VPS using scp (binaries) or rsync (static sites) | ||
| 207 | 3. **Execute**: Runs commands on VPS over SSH to: | ||
| 208 | - Create system users | ||
| 209 | - Install systemd units | ||
| 210 | - Generate Caddy configs | ||
| 211 | - Manage services | ||
| 212 | 4. **Verify**: Checks deployment status and returns results | ||
| 213 | |||
| 214 | ### Package Structure | ||
| 215 | ``` | ||
| 216 | deploy/ | ||
| 217 | ├── cmd/deploy/ | ||
| 218 | │ └── main.go # CLI entry point | ||
| 219 | ├── internal/ | ||
| 220 | │ ├── ssh/ | ||
| 221 | │ │ └── ssh.go # SSH connection & command execution | ||
| 222 | │ ├── init/ | ||
| 223 | │ │ └── init.go # VPS initialization logic | ||
| 224 | │ ├── app/ | ||
| 225 | │ │ └── app.go # Go app deployment logic | ||
| 226 | │ ├── static/ | ||
| 227 | │ │ └── static.go # Static site deployment logic | ||
| 228 | │ ├── systemd/ | ||
| 229 | │ │ └── systemd.go # Systemd operations | ||
| 230 | │ ├── caddy/ | ||
| 231 | │ │ └── caddy.go # Caddy config generation | ||
| 232 | │ └── config/ | ||
| 233 | │ └── config.go # Deployment metadata & port allocation | ||
| 234 | └── templates/ | ||
| 235 | ├── service.tmpl # Systemd unit template | ||
| 236 | ├── app.caddy.tmpl # Caddy config for apps | ||
| 237 | └── static.caddy.tmpl # Caddy config for static sites | ||
| 238 | ``` | ||
| 239 | |||
| 240 | ### Deployment State (Laptop) | ||
| 241 | All deployment state stored locally at `~/.config/deploy/state.json`: | ||
| 242 | ```json | ||
| 243 | { | ||
| 244 | "hosts": { | ||
| 245 | "user@vps-ip": { | ||
| 246 | "next_port": 8003, | ||
| 247 | "apps": { | ||
| 248 | "myapi": { | ||
| 249 | "type": "app", | ||
| 250 | "domain": "api.example.com", | ||
| 251 | "port": 8001, | ||
| 252 | "env": { | ||
| 253 | "DB_HOST": "localhost", | ||
| 254 | "DB_PORT": "5432", | ||
| 255 | "API_KEY": "secret123", | ||
| 256 | "ENVIRONMENT": "production" | ||
| 257 | } | ||
| 258 | }, | ||
| 259 | "anotherapp": { | ||
| 260 | "type": "app", | ||
| 261 | "domain": "another.example.com", | ||
| 262 | "port": 8002, | ||
| 263 | "env": {} | ||
| 264 | }, | ||
| 265 | "mysite": { | ||
| 266 | "type": "static", | ||
| 267 | "domain": "example.com" | ||
| 268 | } | ||
| 269 | } | ||
| 270 | } | ||
| 271 | } | ||
| 272 | } | ||
| 273 | ``` | ||
| 274 | |||
| 275 | ### Environment Files (VPS) | ||
| 276 | Environment variables written to `/etc/ship/env/{appname}.env` on VPS for systemd to read: | ||
| 277 | ```bash | ||
| 278 | # /etc/ship/env/myapi.env (generated from state.json) | ||
| 279 | PORT=8001 | ||
| 280 | DB_HOST=localhost | ||
| 281 | DB_PORT=5432 | ||
| 282 | API_KEY=secret123 | ||
| 283 | ENVIRONMENT=production | ||
| 284 | ``` | ||
| 285 | |||
| 286 | ### CLI Behavior | ||
| 287 | |||
| 288 | **Host Resolution:** | ||
| 289 | - CLI runs on your laptop, connects to VPS via SSH | ||
| 290 | - Host specified via `--host user@vps-ip` flag | ||
| 291 | - Or read from `~/.config/deploy/config` file if present | ||
| 292 | - All operations execute on remote VPS over SSH | ||
| 293 | - Files transferred via scp/rsync | ||
| 294 | |||
| 295 | **Init Command:** | ||
| 296 | - Detects OS (Ubuntu 20.04+, Debian 11+) | ||
| 297 | - Checks if Caddy is already installed (skip if present) | ||
| 298 | - Installs Caddy via official APT repository | ||
| 299 | - Creates `/etc/caddy/Caddyfile` with `import /etc/caddy/sites-enabled/*` | ||
| 300 | - Creates directory structure: `/etc/ship/env/`, `/etc/caddy/sites-enabled/` | ||
| 301 | - Enables and starts Caddy | ||
| 302 | - Runs health check (verify Caddy is running) | ||
| 303 | - Initializes local state file at `~/.config/deploy/state.json` if not present | ||
| 304 | - Outputs success message with next steps | ||
| 305 | |||
| 306 | **Smart Defaults:** | ||
| 307 | - App name: infer from `--name` flag, or binary filename, or current directory | ||
| 308 | - Binary: use `--binary` flag to specify path, or look for binary in current directory | ||
| 309 | - Port: auto-allocate next available port (starting from 8001) | ||
| 310 | - Working directory: `/var/lib/{appname}` | ||
| 311 | - System user: `{appname}` | ||
| 312 | - Binary location: `/usr/local/bin/{appname}` | ||
| 313 | |||
| 314 | **Port Management:** | ||
| 315 | - Automatically allocates next available port from 8001+ | ||
| 316 | - Tracks allocations in local `~/.config/deploy/state.json` | ||
| 317 | - User can override with `--port` flag if desired | ||
| 318 | - Prevents port conflicts | ||
| 319 | |||
| 320 | **Validation:** | ||
| 321 | - Check if domain is already in use (check local state) | ||
| 322 | - Verify binary exists and is executable (for apps) | ||
| 323 | - Confirm Caddy is installed and running | ||
| 324 | - Check for port conflicts (check local state) | ||
| 325 | - Require root/sudo on VPS | ||
| 326 | |||
| 327 | **State Management:** | ||
| 328 | - All deployment state lives on laptop at `~/.config/deploy/state.json` | ||
| 329 | - VPS is "dumb" - just runs Caddy, systemd, and deployed apps | ||
| 330 | - `deploy list` is instant (no SSH needed, reads local state) | ||
| 331 | - Easy to recreate VPS from scratch by redeploying all apps from local state | ||
| 332 | - Can manage multiple VPSes from one laptop | ||
| 333 | - State backed up with your laptop backups | ||
| 334 | - Trade-off: State can drift if VPS is manually modified (user should use CLI only) | ||
| 335 | |||
| 336 | ### Installation Steps (Go Apps) | ||
| 337 | |||
| 338 | All steps executed remotely on VPS via SSH: | ||
| 339 | |||
| 340 | 1. Detect app name from `--name` flag, or binary filename, or current directory | ||
| 341 | 2. SCP binary from laptop to VPS temp directory | ||
| 342 | 3. Allocate port (auto-assign next available or use `--port` override) | ||
| 343 | 4. Create system user (e.g., `myapp`) | ||
| 344 | 5. Create working directory (`/var/lib/myapp`) | ||
| 345 | 6. Copy binary to `/usr/local/bin/myapp` | ||
| 346 | 7. Create env file at `/etc/ship/env/myapp.env` with PORT and any user-provided vars | ||
| 347 | 8. Set env file permissions (0600, owned by app user) | ||
| 348 | 9. Generate systemd unit at `/etc/systemd/system/myapp.service` with EnvironmentFile | ||
| 349 | 10. Generate Caddy config at `/etc/caddy/sites-enabled/myapp.caddy` pointing to localhost:port | ||
| 350 | 11. Update local state at `~/.config/deploy/state.json` (on laptop) | ||
| 351 | 12. Enable and start service | ||
| 352 | 13. Reload Caddy | ||
| 353 | |||
| 354 | ### Installation Steps (Static Sites) | ||
| 355 | |||
| 356 | All steps executed remotely on VPS via SSH: | ||
| 357 | |||
| 358 | 1. Detect site name from `--name` flag or directory name | ||
| 359 | 2. Rsync files from laptop to VPS `/var/www/{sitename}` | ||
| 360 | 3. Set ownership to `www-data:www-data` | ||
| 361 | 4. Generate Caddy config at `/etc/caddy/sites-enabled/{sitename}.caddy` | ||
| 362 | 5. Update local state at `~/.config/deploy/state.json` (on laptop) | ||
| 363 | 6. Reload Caddy | ||
| 364 | |||
| 365 | ## File Structure | ||
| 366 | |||
| 367 | ### On Laptop | ||
| 368 | ``` | ||
| 369 | ~/.config/deploy/state.json # All deployment state (apps, ports, env vars) | ||
| 370 | ~/.config/deploy/config # Optional: default host configuration | ||
| 371 | ``` | ||
| 372 | |||
| 373 | ### On VPS | ||
| 374 | ``` | ||
| 375 | /usr/local/bin/myapp # Go binary | ||
| 376 | /var/lib/myapp/ # Working directory | ||
| 377 | /etc/systemd/system/myapp.service # Systemd unit | ||
| 378 | /etc/caddy/sites-enabled/myapp.caddy # Caddy config | ||
| 379 | /etc/ship/env/myapp.env # Environment variables (0600 permissions) | ||
| 380 | |||
| 381 | /var/www/mysite/ # Static site files | ||
| 382 | /etc/caddy/sites-enabled/mysite.caddy # Caddy config | ||
| 383 | ``` | ||
| 384 | |||
| 385 | ## Templates | ||
| 386 | |||
| 387 | ### systemd unit (`service.tmpl`) | ||
| 388 | ```ini | ||
| 389 | [Unit] | ||
| 390 | Description={{.Name}} | ||
| 391 | After=network.target | ||
| 392 | |||
| 393 | [Service] | ||
| 394 | Type=simple | ||
| 395 | User={{.User}} | ||
| 396 | WorkingDirectory={{.WorkDir}} | ||
| 397 | EnvironmentFile={{.EnvFile}} | ||
| 398 | ExecStart={{.BinaryPath}} --port={{.Port}} | ||
| 399 | Restart=always | ||
| 400 | RestartSec=5s | ||
| 401 | NoNewPrivileges=true | ||
| 402 | PrivateTmp=true | ||
| 403 | |||
| 404 | [Install] | ||
| 405 | WantedBy=multi-user.target | ||
| 406 | ``` | ||
| 407 | |||
| 408 | ### Caddy config for Go apps (`app.caddy.tmpl`) | ||
| 409 | ``` | ||
| 410 | {{.Domain}} { | ||
| 411 | reverse_proxy 127.0.0.1:{{.Port}} | ||
| 412 | } | ||
| 413 | ``` | ||
| 414 | |||
| 415 | ### Caddy config for static sites (`static.caddy.tmpl`) | ||
| 416 | ``` | ||
| 417 | {{.Domain}} { | ||
| 418 | root * {{.RootDir}} | ||
| 419 | file_server | ||
| 420 | encode gzip | ||
| 421 | } | ||
| 422 | ``` | ||
| 423 | |||
| 424 | ## Optional Helper Module | ||
| 425 | |||
| 426 | For apps that want minimal boilerplate: | ||
| 427 | |||
| 428 | ```go | ||
| 429 | // github.com/yourusername/deploy/httpserver | ||
| 430 | |||
| 431 | package httpserver | ||
| 432 | |||
| 433 | import ( | ||
| 434 | "flag" | ||
| 435 | "fmt" | ||
| 436 | "net/http" | ||
| 437 | "os" | ||
| 438 | ) | ||
| 439 | |||
| 440 | // ListenAndServe starts HTTP server on PORT env var or --port flag | ||
| 441 | func ListenAndServe(handler http.Handler) error { | ||
| 442 | port := flag.String("port", os.Getenv("PORT"), "port to listen on") | ||
| 443 | flag.Parse() | ||
| 444 | |||
| 445 | if *port == "" { | ||
| 446 | *port = "8080" // fallback for local dev | ||
| 447 | } | ||
| 448 | |||
| 449 | addr := "127.0.0.1:" + *port | ||
| 450 | fmt.Printf("Listening on %s\n", addr) | ||
| 451 | return http.ListenAndServe(addr, handler) | ||
| 452 | } | ||
| 453 | ``` | ||
| 454 | |||
| 455 | ## Example Usage Scenarios | ||
| 456 | |||
| 457 | ### Scenario 1: New Go API (Fresh VPS) | ||
| 458 | ```bash | ||
| 459 | # ONE-TIME: Initialize fresh VPS from your laptop | ||
| 460 | deploy init --host user@your-vps-ip | ||
| 461 | # Installs Caddy, sets up directory structure | ||
| 462 | |||
| 463 | # Optional: Save host to config | ||
| 464 | mkdir -p ~/.config/deploy | ||
| 465 | echo "host: user@your-vps-ip" > ~/.config/deploy/config | ||
| 466 | |||
| 467 | # Develop locally | ||
| 468 | mkdir myapi && cd myapi | ||
| 469 | go mod init myapi | ||
| 470 | |||
| 471 | # Write standard HTTP app | ||
| 472 | cat > main.go <<'EOF' | ||
| 473 | package main | ||
| 474 | import ( | ||
| 475 | "flag" | ||
| 476 | "fmt" | ||
| 477 | "net/http" | ||
| 478 | "os" | ||
| 479 | ) | ||
| 480 | func main() { | ||
| 481 | port := flag.String("port", os.Getenv("PORT"), "port to listen on") | ||
| 482 | flag.Parse() | ||
| 483 | if *port == "" { | ||
| 484 | *port = "8080" | ||
| 485 | } | ||
| 486 | |||
| 487 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { | ||
| 488 | w.Write([]byte("Hello World")) | ||
| 489 | }) | ||
| 490 | |||
| 491 | addr := "127.0.0.1:" + *port | ||
| 492 | fmt.Printf("Listening on %s\n", addr) | ||
| 493 | http.ListenAndServe(addr, nil) | ||
| 494 | } | ||
| 495 | EOF | ||
| 496 | |||
| 497 | # Build and deploy from laptop | ||
| 498 | GOOS=linux GOARCH=amd64 go build | ||
| 499 | deploy --binary ./myapi --domain api.example.com | ||
| 500 | # CLI: SCPs binary to VPS, creates systemd service, configures Caddy | ||
| 501 | |||
| 502 | # Later, add environment variables from laptop | ||
| 503 | deploy env myapi --set DB_HOST=localhost --set DB_PORT=5432 | ||
| 504 | # Service automatically restarts to pick up new env vars | ||
| 505 | ``` | ||
| 506 | |||
| 507 | ### Scenario 2: React/Vue/etc Static Site | ||
| 508 | ```bash | ||
| 509 | # Build | ||
| 510 | npm run build | ||
| 511 | |||
| 512 | # Deploy from laptop | ||
| 513 | deploy --static --dir ./dist --domain example.com --name mysite | ||
| 514 | # CLI rsyncs files to VPS, configures Caddy | ||
| 515 | ``` | ||
| 516 | |||
| 517 | ### Scenario 3: Update Existing Deployment | ||
| 518 | ```bash | ||
| 519 | # Rebuild | ||
| 520 | GOOS=linux GOARCH=amd64 go build | ||
| 521 | |||
| 522 | # Redeploy (same command, it will update) | ||
| 523 | deploy --binary ./myapi --domain api.example.com | ||
| 524 | # CLI detects existing deployment, stops service, updates binary, restarts | ||
| 525 | # Keeps same port allocation | ||
| 526 | ``` | ||
| 527 | |||
| 528 | ### Scenario 4: Multiple Apps | ||
| 529 | ```bash | ||
| 530 | # Deploy first app from laptop | ||
| 531 | deploy --binary api1 --domain api1.example.com # Gets port 8001 | ||
| 532 | |||
| 533 | # Deploy second app | ||
| 534 | deploy --binary api2 --domain api2.example.com # Gets port 8002 | ||
| 535 | |||
| 536 | # Deploy third app | ||
| 537 | deploy --binary api3 --domain api3.example.com # Gets port 8003 | ||
| 538 | |||
| 539 | # View all deployments from laptop | ||
| 540 | deploy list | ||
| 541 | # Output: | ||
| 542 | # api1 app api1.example.com :8001 running | ||
| 543 | # api2 app api2.example.com :8002 running | ||
| 544 | # api3 app api3.example.com :8003 running | ||
| 545 | ``` | ||
| 546 | |||
| 547 | ## Security Considerations | ||
| 548 | |||
| 549 | - Run each Go app as dedicated system user | ||
| 550 | - Use systemd security hardening (NoNewPrivileges, PrivateTmp, ProtectSystem) | ||
| 551 | - Static sites served as www-data | ||
| 552 | - Caddy automatically handles TLS cert management | ||
| 553 | - Environment files stored at `/etc/ship/env/{app}.env` with 0600 permissions | ||
| 554 | - Env files owned by the app's system user | ||
| 555 | - `deploy env` command masks sensitive values when displaying (shows `API_KEY=***`) | ||
| 556 | - Consider using external secret management for production (out of scope for v1) | ||
| 557 | |||
| 558 | ## Features | ||
| 559 | |||
| 560 | ✓ Single command deployment from anywhere | ||
| 561 | ✓ Automatic HTTPS via Caddy + Let's Encrypt | ||
| 562 | ✓ Automatic port allocation (no manual tracking needed) | ||
| 563 | ✓ Standard HTTP - write apps the normal way | ||
| 564 | ✓ Environment variable management (set via CLI or .env files) | ||
| 565 | ✓ Secure env file storage (0600 permissions) | ||
| 566 | ✓ Systemd process management with auto-restart | ||
| 567 | ✓ Support for multiple apps/sites on one VPS | ||
| 568 | ✓ Manage multiple VPSes from one laptop | ||
| 569 | ✓ State stored locally (VPS is stateless and easily recreatable) | ||
| 570 | ✓ Security hardening by default | ||
| 571 | ✓ Update existing deployments with same command | ||
| 572 | ✓ Clean removal with `deploy remove` | ||
| 573 | ✓ View logs/status of any deployment | ||
| 574 | ✓ Minimal app requirements (just accept PORT env/flag) | ||
| 575 | |||
| 576 | ## Out of Scope (for v1) | ||
| 577 | |||
| 578 | - Database setup/management | ||
| 579 | - Secrets encryption at rest (env files are protected by file permissions) | ||
| 580 | - Secret rotation automation | ||
| 581 | - Log rotation (systemd journald handles this) | ||
| 582 | - Monitoring/alerting | ||
| 583 | - Blue/green or zero-downtime deployments | ||
| 584 | - Automatic updates/CI integration | ||
| 585 | - Custom systemd unit modifications | ||
| 586 | - Non-Caddy web servers | ||
