From 7a640fb3e50878a2377fa8985f43f25ee15236be Mon Sep 17 00:00:00 2001 From: bndw Date: Sat, 24 Jan 2026 10:26:38 -0800 Subject: remove old design spec --- DESIGN_SPEC.md | 586 --------------------------------------------------------- 1 file changed, 586 deletions(-) delete mode 100644 DESIGN_SPEC.md (limited to 'DESIGN_SPEC.md') 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 @@ -# VPS Deployment CLI - Design Spec - -## Overview -Standalone CLI tool for deploying Go apps and static sites to a VPS with automatic HTTPS via Caddy. - -## Server Setup (One-time) - -### Build and Install the Deploy CLI on Your Laptop -```bash -# Clone and build the deploy CLI -git clone deploy -cd deploy -go build -o ~/bin/deploy ./cmd/deploy -# Or: go install ./cmd/deploy (if ~/go/bin is in your PATH) - -# Initialize a fresh VPS (one-time) -deploy init --host user@your-vps-ip - -# The CLI will SSH into the VPS and: -# - Detect OS (Ubuntu/Debian supported) -# - Install Caddy from official repository -# - Configure Caddy to import `/etc/caddy/sites-enabled/*` -# - Create `/etc/ship/env/` directory for env files -# - Create `/etc/caddy/sites-enabled/` directory -# - Enable and start Caddy service -# - Verify installation -# -# State is stored locally at ~/.config/deploy/state.json -``` - -## CLI Commands - -All commands run from your laptop and use `--host` to specify the VPS. - -### Initialize VPS (One-time Setup) -```bash -# On a fresh VPS, run this once to install all dependencies -deploy init --host user@your-vps-ip - -# This will SSH to the VPS and: -# - Install Caddy -# - Configure Caddy to use sites-enabled pattern -# - Create /etc/ship/env/ directory for env files -# - Enable and start Caddy -# -# State is stored locally at ~/.config/deploy/state.json -``` - -### Deploy Go App -```bash -# Build locally, then deploy -deploy --host user@vps-ip --domain api.example.com - -# With custom name (defaults to directory name or binary name) -deploy --host user@vps-ip --name myapi --domain api.example.com - -# Deploy specific binary -deploy --host user@vps-ip --binary ./myapp --domain api.example.com - -# With environment variables -deploy --host user@vps-ip --domain api.example.com --env DB_HOST=localhost --env API_KEY=secret - -# Or from env file -deploy --host user@vps-ip --domain api.example.com --env-file .env.production -``` - -### Deploy Static Site -```bash -# Deploy current directory -deploy --host user@vps-ip --static --domain example.com - -# Deploy specific directory -deploy --host user@vps-ip --static --dir ./dist --domain example.com - -# With custom name -deploy --host user@vps-ip --static --name mysite --dir ./build --domain example.com -``` - -### Management Commands -```bash -# List all deployed apps/sites -deploy list --host user@vps-ip - -# Remove deployment -deploy remove myapp --host user@vps-ip - -# View logs -deploy logs myapp --host user@vps-ip - -# Restart app -deploy restart myapp --host user@vps-ip - -# View status -deploy status myapp --host user@vps-ip - -# Set/update environment variables -deploy env myapi --host user@vps-ip --set DB_HOST=localhost --set API_KEY=secret - -# View environment variables (secrets are masked) -deploy env myapi --host user@vps-ip - -# Load from file -deploy env myapi --host user@vps-ip --file .env.production - -# Remove specific env var -deploy env myapi --host user@vps-ip --unset API_KEY -``` - -### Optional: Config File -```bash -# Create ~/.config/deploy/config to avoid typing --host every time -mkdir -p ~/.config/deploy -cat > ~/.config/deploy/config < ~/.config/deploy/config - -# Done! VPS is ready for deployments -``` - -### Go Apps -```bash -# 1. Build locally -GOOS=linux GOARCH=amd64 go build -o myapp - -# 2. Deploy from laptop (with config file) -deploy --binary ./myapp --domain api.example.com - -# Or specify host explicitly -deploy --host user@vps-ip --binary ./myapp --domain api.example.com -# CLI will: scp binary to VPS, set up systemd, configure Caddy, start service -``` - -### Static Sites -```bash -# 1. Build locally -npm run build - -# 2. Deploy from laptop -deploy --static --dir ./dist --domain example.com -# CLI will: rsync files to VPS, configure Caddy -``` - -## App Requirements - -### Go Apps Must: -- Listen on HTTP (standard `http.ListenAndServe`) -- Accept port via env var `PORT` or flag `--port` -- Bind to `localhost` or `127.0.0.1` only (Caddy will handle external traffic) - -**Example main.go:** -```go -package main - -import ( - "flag" - "fmt" - "net/http" - "os" -) - -func main() { - port := flag.String("port", os.Getenv("PORT"), "port to listen on") - flag.Parse() - - if *port == "" { - *port = "8080" // fallback for local dev - } - - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("Hello World")) - }) - - addr := "127.0.0.1:" + *port - fmt.Printf("Listening on %s\n", addr) - http.ListenAndServe(addr, nil) -} -``` - -## CLI Implementation - -### How It Works -The CLI is a single binary that runs on your laptop and orchestrates deployments via SSH: - -1. **Connect**: Uses SSH to connect to VPS (via `--host` flag or config file) -2. **Transfer**: Copies files to VPS using scp (binaries) or rsync (static sites) -3. **Execute**: Runs commands on VPS over SSH to: - - Create system users - - Install systemd units - - Generate Caddy configs - - Manage services -4. **Verify**: Checks deployment status and returns results - -### Package Structure -``` -deploy/ -├── cmd/deploy/ -│ └── main.go # CLI entry point -├── internal/ -│ ├── ssh/ -│ │ └── ssh.go # SSH connection & command execution -│ ├── init/ -│ │ └── init.go # VPS initialization logic -│ ├── app/ -│ │ └── app.go # Go app deployment logic -│ ├── static/ -│ │ └── static.go # Static site deployment logic -│ ├── systemd/ -│ │ └── systemd.go # Systemd operations -│ ├── caddy/ -│ │ └── caddy.go # Caddy config generation -│ └── config/ -│ └── config.go # Deployment metadata & port allocation -└── templates/ - ├── service.tmpl # Systemd unit template - ├── app.caddy.tmpl # Caddy config for apps - └── static.caddy.tmpl # Caddy config for static sites -``` - -### Deployment State (Laptop) -All deployment state stored locally at `~/.config/deploy/state.json`: -```json -{ - "hosts": { - "user@vps-ip": { - "next_port": 8003, - "apps": { - "myapi": { - "type": "app", - "domain": "api.example.com", - "port": 8001, - "env": { - "DB_HOST": "localhost", - "DB_PORT": "5432", - "API_KEY": "secret123", - "ENVIRONMENT": "production" - } - }, - "anotherapp": { - "type": "app", - "domain": "another.example.com", - "port": 8002, - "env": {} - }, - "mysite": { - "type": "static", - "domain": "example.com" - } - } - } - } -} -``` - -### Environment Files (VPS) -Environment variables written to `/etc/ship/env/{appname}.env` on VPS for systemd to read: -```bash -# /etc/ship/env/myapi.env (generated from state.json) -PORT=8001 -DB_HOST=localhost -DB_PORT=5432 -API_KEY=secret123 -ENVIRONMENT=production -``` - -### CLI Behavior - -**Host Resolution:** -- CLI runs on your laptop, connects to VPS via SSH -- Host specified via `--host user@vps-ip` flag -- Or read from `~/.config/deploy/config` file if present -- All operations execute on remote VPS over SSH -- Files transferred via scp/rsync - -**Init Command:** -- Detects OS (Ubuntu 20.04+, Debian 11+) -- Checks if Caddy is already installed (skip if present) -- Installs Caddy via official APT repository -- Creates `/etc/caddy/Caddyfile` with `import /etc/caddy/sites-enabled/*` -- Creates directory structure: `/etc/ship/env/`, `/etc/caddy/sites-enabled/` -- Enables and starts Caddy -- Runs health check (verify Caddy is running) -- Initializes local state file at `~/.config/deploy/state.json` if not present -- Outputs success message with next steps - -**Smart Defaults:** -- App name: infer from `--name` flag, or binary filename, or current directory -- Binary: use `--binary` flag to specify path, or look for binary in current directory -- Port: auto-allocate next available port (starting from 8001) -- Working directory: `/var/lib/{appname}` -- System user: `{appname}` -- Binary location: `/usr/local/bin/{appname}` - -**Port Management:** -- Automatically allocates next available port from 8001+ -- Tracks allocations in local `~/.config/deploy/state.json` -- User can override with `--port` flag if desired -- Prevents port conflicts - -**Validation:** -- Check if domain is already in use (check local state) -- Verify binary exists and is executable (for apps) -- Confirm Caddy is installed and running -- Check for port conflicts (check local state) -- Require root/sudo on VPS - -**State Management:** -- All deployment state lives on laptop at `~/.config/deploy/state.json` -- VPS is "dumb" - just runs Caddy, systemd, and deployed apps -- `deploy list` is instant (no SSH needed, reads local state) -- Easy to recreate VPS from scratch by redeploying all apps from local state -- Can manage multiple VPSes from one laptop -- State backed up with your laptop backups -- Trade-off: State can drift if VPS is manually modified (user should use CLI only) - -### Installation Steps (Go Apps) - -All steps executed remotely on VPS via SSH: - -1. Detect app name from `--name` flag, or binary filename, or current directory -2. SCP binary from laptop to VPS temp directory -3. Allocate port (auto-assign next available or use `--port` override) -4. Create system user (e.g., `myapp`) -5. Create working directory (`/var/lib/myapp`) -6. Copy binary to `/usr/local/bin/myapp` -7. Create env file at `/etc/ship/env/myapp.env` with PORT and any user-provided vars -8. Set env file permissions (0600, owned by app user) -9. Generate systemd unit at `/etc/systemd/system/myapp.service` with EnvironmentFile -10. Generate Caddy config at `/etc/caddy/sites-enabled/myapp.caddy` pointing to localhost:port -11. Update local state at `~/.config/deploy/state.json` (on laptop) -12. Enable and start service -13. Reload Caddy - -### Installation Steps (Static Sites) - -All steps executed remotely on VPS via SSH: - -1. Detect site name from `--name` flag or directory name -2. Rsync files from laptop to VPS `/var/www/{sitename}` -3. Set ownership to `www-data:www-data` -4. Generate Caddy config at `/etc/caddy/sites-enabled/{sitename}.caddy` -5. Update local state at `~/.config/deploy/state.json` (on laptop) -6. Reload Caddy - -## File Structure - -### On Laptop -``` -~/.config/deploy/state.json # All deployment state (apps, ports, env vars) -~/.config/deploy/config # Optional: default host configuration -``` - -### On VPS -``` -/usr/local/bin/myapp # Go binary -/var/lib/myapp/ # Working directory -/etc/systemd/system/myapp.service # Systemd unit -/etc/caddy/sites-enabled/myapp.caddy # Caddy config -/etc/ship/env/myapp.env # Environment variables (0600 permissions) - -/var/www/mysite/ # Static site files -/etc/caddy/sites-enabled/mysite.caddy # Caddy config -``` - -## Templates - -### systemd unit (`service.tmpl`) -```ini -[Unit] -Description={{.Name}} -After=network.target - -[Service] -Type=simple -User={{.User}} -WorkingDirectory={{.WorkDir}} -EnvironmentFile={{.EnvFile}} -ExecStart={{.BinaryPath}} --port={{.Port}} -Restart=always -RestartSec=5s -NoNewPrivileges=true -PrivateTmp=true - -[Install] -WantedBy=multi-user.target -``` - -### Caddy config for Go apps (`app.caddy.tmpl`) -``` -{{.Domain}} { - reverse_proxy 127.0.0.1:{{.Port}} -} -``` - -### Caddy config for static sites (`static.caddy.tmpl`) -``` -{{.Domain}} { - root * {{.RootDir}} - file_server - encode gzip -} -``` - -## Optional Helper Module - -For apps that want minimal boilerplate: - -```go -// github.com/yourusername/deploy/httpserver - -package httpserver - -import ( - "flag" - "fmt" - "net/http" - "os" -) - -// ListenAndServe starts HTTP server on PORT env var or --port flag -func ListenAndServe(handler http.Handler) error { - port := flag.String("port", os.Getenv("PORT"), "port to listen on") - flag.Parse() - - if *port == "" { - *port = "8080" // fallback for local dev - } - - addr := "127.0.0.1:" + *port - fmt.Printf("Listening on %s\n", addr) - return http.ListenAndServe(addr, handler) -} -``` - -## Example Usage Scenarios - -### Scenario 1: New Go API (Fresh VPS) -```bash -# ONE-TIME: Initialize fresh VPS from your laptop -deploy init --host user@your-vps-ip -# Installs Caddy, sets up directory structure - -# Optional: Save host to config -mkdir -p ~/.config/deploy -echo "host: user@your-vps-ip" > ~/.config/deploy/config - -# Develop locally -mkdir myapi && cd myapi -go mod init myapi - -# Write standard HTTP app -cat > main.go <<'EOF' -package main -import ( - "flag" - "fmt" - "net/http" - "os" -) -func main() { - port := flag.String("port", os.Getenv("PORT"), "port to listen on") - flag.Parse() - if *port == "" { - *port = "8080" - } - - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("Hello World")) - }) - - addr := "127.0.0.1:" + *port - fmt.Printf("Listening on %s\n", addr) - http.ListenAndServe(addr, nil) -} -EOF - -# Build and deploy from laptop -GOOS=linux GOARCH=amd64 go build -deploy --binary ./myapi --domain api.example.com -# CLI: SCPs binary to VPS, creates systemd service, configures Caddy - -# Later, add environment variables from laptop -deploy env myapi --set DB_HOST=localhost --set DB_PORT=5432 -# Service automatically restarts to pick up new env vars -``` - -### Scenario 2: React/Vue/etc Static Site -```bash -# Build -npm run build - -# Deploy from laptop -deploy --static --dir ./dist --domain example.com --name mysite -# CLI rsyncs files to VPS, configures Caddy -``` - -### Scenario 3: Update Existing Deployment -```bash -# Rebuild -GOOS=linux GOARCH=amd64 go build - -# Redeploy (same command, it will update) -deploy --binary ./myapi --domain api.example.com -# CLI detects existing deployment, stops service, updates binary, restarts -# Keeps same port allocation -``` - -### Scenario 4: Multiple Apps -```bash -# Deploy first app from laptop -deploy --binary api1 --domain api1.example.com # Gets port 8001 - -# Deploy second app -deploy --binary api2 --domain api2.example.com # Gets port 8002 - -# Deploy third app -deploy --binary api3 --domain api3.example.com # Gets port 8003 - -# View all deployments from laptop -deploy list -# Output: -# api1 app api1.example.com :8001 running -# api2 app api2.example.com :8002 running -# api3 app api3.example.com :8003 running -``` - -## Security Considerations - -- Run each Go app as dedicated system user -- Use systemd security hardening (NoNewPrivileges, PrivateTmp, ProtectSystem) -- Static sites served as www-data -- Caddy automatically handles TLS cert management -- Environment files stored at `/etc/ship/env/{app}.env` with 0600 permissions -- Env files owned by the app's system user -- `deploy env` command masks sensitive values when displaying (shows `API_KEY=***`) -- Consider using external secret management for production (out of scope for v1) - -## Features - -✓ Single command deployment from anywhere -✓ Automatic HTTPS via Caddy + Let's Encrypt -✓ Automatic port allocation (no manual tracking needed) -✓ Standard HTTP - write apps the normal way -✓ Environment variable management (set via CLI or .env files) -✓ Secure env file storage (0600 permissions) -✓ Systemd process management with auto-restart -✓ Support for multiple apps/sites on one VPS -✓ Manage multiple VPSes from one laptop -✓ State stored locally (VPS is stateless and easily recreatable) -✓ Security hardening by default -✓ Update existing deployments with same command -✓ Clean removal with `deploy remove` -✓ View logs/status of any deployment -✓ Minimal app requirements (just accept PORT env/flag) - -## Out of Scope (for v1) - -- Database setup/management -- Secrets encryption at rest (env files are protected by file permissions) -- Secret rotation automation -- Log rotation (systemd journald handles this) -- Monitoring/alerting -- Blue/green or zero-downtime deployments -- Automatic updates/CI integration -- Custom systemd unit modifications -- Non-Caddy web servers -- cgit v1.2.3