summaryrefslogtreecommitdiffstats
path: root/DESIGN_SPEC.md
diff options
context:
space:
mode:
Diffstat (limited to 'DESIGN_SPEC.md')
-rw-r--r--DESIGN_SPEC.md586
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
4Standalone 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
11git clone <this-repo> deploy
12cd deploy
13go 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)
17deploy 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
33All 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
38deploy 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
52deploy --host user@vps-ip --domain api.example.com
53
54# With custom name (defaults to directory name or binary name)
55deploy --host user@vps-ip --name myapi --domain api.example.com
56
57# Deploy specific binary
58deploy --host user@vps-ip --binary ./myapp --domain api.example.com
59
60# With environment variables
61deploy --host user@vps-ip --domain api.example.com --env DB_HOST=localhost --env API_KEY=secret
62
63# Or from env file
64deploy --host user@vps-ip --domain api.example.com --env-file .env.production
65```
66
67### Deploy Static Site
68```bash
69# Deploy current directory
70deploy --host user@vps-ip --static --domain example.com
71
72# Deploy specific directory
73deploy --host user@vps-ip --static --dir ./dist --domain example.com
74
75# With custom name
76deploy --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
82deploy list --host user@vps-ip
83
84# Remove deployment
85deploy remove myapp --host user@vps-ip
86
87# View logs
88deploy logs myapp --host user@vps-ip
89
90# Restart app
91deploy restart myapp --host user@vps-ip
92
93# View status
94deploy status myapp --host user@vps-ip
95
96# Set/update environment variables
97deploy env myapi --host user@vps-ip --set DB_HOST=localhost --set API_KEY=secret
98
99# View environment variables (secrets are masked)
100deploy env myapi --host user@vps-ip
101
102# Load from file
103deploy env myapi --host user@vps-ip --file .env.production
104
105# Remove specific env var
106deploy 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
112mkdir -p ~/.config/deploy
113cat > ~/.config/deploy/config <<EOF
114host: user@your-vps-ip
115EOF
116
117# Now you can omit --host flag
118deploy --domain api.example.com
119deploy list
120```
121
122## Deployment Workflow
123
124### Initial VPS Setup
125```bash
126# 1. Build the deploy CLI (in this repo)
127cd /path/to/deploy-repo
128go build -o ~/bin/deploy ./cmd/deploy
129
130# 2. Initialize fresh VPS from your laptop
131deploy 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
135mkdir -p ~/.config/deploy
136echo "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
144GOOS=linux GOARCH=amd64 go build -o myapp
145
146# 2. Deploy from laptop (with config file)
147deploy --binary ./myapp --domain api.example.com
148
149# Or specify host explicitly
150deploy --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
157npm run build
158
159# 2. Deploy from laptop
160deploy --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
173package main
174
175import (
176 "flag"
177 "fmt"
178 "net/http"
179 "os"
180)
181
182func 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
203The CLI is a single binary that runs on your laptop and orchestrates deployments via SSH:
204
2051. **Connect**: Uses SSH to connect to VPS (via `--host` flag or config file)
2062. **Transfer**: Copies files to VPS using scp (binaries) or rsync (static sites)
2073. **Execute**: Runs commands on VPS over SSH to:
208 - Create system users
209 - Install systemd units
210 - Generate Caddy configs
211 - Manage services
2124. **Verify**: Checks deployment status and returns results
213
214### Package Structure
215```
216deploy/
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)
241All 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)
276Environment 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)
279PORT=8001
280DB_HOST=localhost
281DB_PORT=5432
282API_KEY=secret123
283ENVIRONMENT=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
338All steps executed remotely on VPS via SSH:
339
3401. Detect app name from `--name` flag, or binary filename, or current directory
3412. SCP binary from laptop to VPS temp directory
3423. Allocate port (auto-assign next available or use `--port` override)
3434. Create system user (e.g., `myapp`)
3445. Create working directory (`/var/lib/myapp`)
3456. Copy binary to `/usr/local/bin/myapp`
3467. Create env file at `/etc/ship/env/myapp.env` with PORT and any user-provided vars
3478. Set env file permissions (0600, owned by app user)
3489. Generate systemd unit at `/etc/systemd/system/myapp.service` with EnvironmentFile
34910. Generate Caddy config at `/etc/caddy/sites-enabled/myapp.caddy` pointing to localhost:port
35011. Update local state at `~/.config/deploy/state.json` (on laptop)
35112. Enable and start service
35213. Reload Caddy
353
354### Installation Steps (Static Sites)
355
356All steps executed remotely on VPS via SSH:
357
3581. Detect site name from `--name` flag or directory name
3592. Rsync files from laptop to VPS `/var/www/{sitename}`
3603. Set ownership to `www-data:www-data`
3614. Generate Caddy config at `/etc/caddy/sites-enabled/{sitename}.caddy`
3625. Update local state at `~/.config/deploy/state.json` (on laptop)
3636. 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]
390Description={{.Name}}
391After=network.target
392
393[Service]
394Type=simple
395User={{.User}}
396WorkingDirectory={{.WorkDir}}
397EnvironmentFile={{.EnvFile}}
398ExecStart={{.BinaryPath}} --port={{.Port}}
399Restart=always
400RestartSec=5s
401NoNewPrivileges=true
402PrivateTmp=true
403
404[Install]
405WantedBy=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
426For apps that want minimal boilerplate:
427
428```go
429// github.com/yourusername/deploy/httpserver
430
431package httpserver
432
433import (
434 "flag"
435 "fmt"
436 "net/http"
437 "os"
438)
439
440// ListenAndServe starts HTTP server on PORT env var or --port flag
441func 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
460deploy init --host user@your-vps-ip
461# Installs Caddy, sets up directory structure
462
463# Optional: Save host to config
464mkdir -p ~/.config/deploy
465echo "host: user@your-vps-ip" > ~/.config/deploy/config
466
467# Develop locally
468mkdir myapi && cd myapi
469go mod init myapi
470
471# Write standard HTTP app
472cat > main.go <<'EOF'
473package main
474import (
475 "flag"
476 "fmt"
477 "net/http"
478 "os"
479)
480func 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}
495EOF
496
497# Build and deploy from laptop
498GOOS=linux GOARCH=amd64 go build
499deploy --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
503deploy 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
510npm run build
511
512# Deploy from laptop
513deploy --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
520GOOS=linux GOARCH=amd64 go build
521
522# Redeploy (same command, it will update)
523deploy --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
531deploy --binary api1 --domain api1.example.com # Gets port 8001
532
533# Deploy second app
534deploy --binary api2 --domain api2.example.com # Gets port 8002
535
536# Deploy third app
537deploy --binary api3 --domain api3.example.com # Gets port 8003
538
539# View all deployments from laptop
540deploy 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