diff options
Diffstat (limited to 'PLAN_v0.2.0.md')
| -rw-r--r-- | PLAN_v0.2.0.md | 356 |
1 files changed, 0 insertions, 356 deletions
diff --git a/PLAN_v0.2.0.md b/PLAN_v0.2.0.md deleted file mode 100644 index d48e95c..0000000 --- a/PLAN_v0.2.0.md +++ /dev/null | |||
| @@ -1,356 +0,0 @@ | |||
| 1 | # Plan: Git-Centric Deployment with Docker Builds and Vanity Imports | ||
| 2 | |||
| 3 | ## Context | ||
| 4 | |||
| 5 | Ship currently deploys by SCP'ing pre-built binaries to a VPS. This plan replaces that model entirely: every project starts with a **git remote on the VPS**, pushing triggers an automatic `docker build` and deploy via post-receive hooks. Since all repos live at `/srv/git/`, the same domain serves **Go vanity imports** and **git HTTPS cloning**, making `go get yourdomain.com/foo` work with zero extra setup. | ||
| 6 | |||
| 7 | Using Docker for builds means the VPS only needs Docker installed — no language-specific toolchains. Any language with a Dockerfile works. | ||
| 8 | |||
| 9 | Deployment config (systemd unit, Caddyfile) lives **in the repo** under `.ship/`, making it versioned, customizable, and deployed via `git push` alongside code. Env vars (which may contain secrets) stay managed separately via `ship env`. | ||
| 10 | |||
| 11 | The base domain pulls double duty: `example.com/foo` serves vanity imports, `foo.example.com` runs the app. | ||
| 12 | |||
| 13 | No backward compatibility with the current binary-upload flow — clean slate. | ||
| 14 | |||
| 15 | --- | ||
| 16 | |||
| 17 | ## User Flow | ||
| 18 | |||
| 19 | ``` | ||
| 20 | ship host init --host myserver --base-domain example.com # one-time VPS setup | ||
| 21 | ship init myapp # creates bare repo on VPS + .ship/ locally | ||
| 22 | git add .ship/ Dockerfile && git commit -m "initial deploy" # commit deploy config | ||
| 23 | git remote add ship git@myserver:/srv/git/myapp.git | ||
| 24 | git push ship main # installs configs + docker build + deploy | ||
| 25 | ``` | ||
| 26 | |||
| 27 | To change config (memory limit, Caddy rules, etc.): edit `.ship/service` or `.ship/Caddyfile`, commit, push. | ||
| 28 | |||
| 29 | --- | ||
| 30 | |||
| 31 | ## Phase 1: State & Template Rewrite | ||
| 32 | |||
| 33 | ### 1A. State — `internal/state/state.go` | ||
| 34 | |||
| 35 | Simplified structs: | ||
| 36 | |||
| 37 | ```go | ||
| 38 | type Host struct { | ||
| 39 | NextPort int `json:"next_port"` | ||
| 40 | BaseDomain string `json:"base_domain,omitempty"` | ||
| 41 | GitSetup bool `json:"git_setup,omitempty"` | ||
| 42 | Apps map[string]*App `json:"apps"` | ||
| 43 | } | ||
| 44 | |||
| 45 | type App struct { | ||
| 46 | Type string `json:"type"` // "app" or "static" | ||
| 47 | Domain string `json:"domain"` | ||
| 48 | Port int `json:"port,omitempty"` // only for "app" | ||
| 49 | Env map[string]string `json:"env,omitempty"` // only for "app" | ||
| 50 | Repo string `json:"repo"` // e.g., "/srv/git/foo.git" | ||
| 51 | } | ||
| 52 | ``` | ||
| 53 | |||
| 54 | Removed from App: `Args`, `Files`, `Memory`, `CPU` — these are now in `.ship/service` inside the repo, not in ship's state. | ||
| 55 | |||
| 56 | ### 1B. Templates — `internal/templates/templates.go` | ||
| 57 | |||
| 58 | Replace existing templates with: | ||
| 59 | |||
| 60 | 1. **`PostReceiveHook(data)`** — app deploy hook (see Hook Detail below) | ||
| 61 | 2. **`PostReceiveHookStatic(data)`** — static site deploy hook | ||
| 62 | 3. **`CodeCaddy(data)`** — base domain Caddy config for vanity imports + git HTTP | ||
| 63 | 4. **`DockerService(data)`** — default systemd unit (generated into `.ship/service` locally) | ||
| 64 | 5. **`AppCaddy(data)`** — default app Caddyfile (generated into `.ship/Caddyfile` locally) | ||
| 65 | 6. **`StaticCaddy(data)`** — default static Caddyfile (generated into `.ship/Caddyfile` locally) | ||
| 66 | |||
| 67 | Remove old: `serviceTemplate`, `appCaddyTemplate`, `staticCaddyTemplate` and their render functions (`SystemdService`, `AppCaddy`, `StaticCaddy`). | ||
| 68 | |||
| 69 | --- | ||
| 70 | |||
| 71 | ## Phase 2: Caddy-Native Vanity Imports + Git HTTP | ||
| 72 | |||
| 73 | Caddy handles everything directly. No separate server binary. | ||
| 74 | |||
| 75 | ### Vanity imports — Caddy `templates` directive | ||
| 76 | |||
| 77 | HTML template at `/opt/ship/vanity/index.html` on VPS: | ||
| 78 | |||
| 79 | ```html | ||
| 80 | <!DOCTYPE html> | ||
| 81 | <html><head> | ||
| 82 | {{$path := trimPrefix "/" .Req.URL.Path}} | ||
| 83 | {{$parts := splitList "/" $path}} | ||
| 84 | {{$module := first $parts}} | ||
| 85 | <meta name="go-import" content="{{.Host}}/{{$module}} git https://{{.Host}}/{{$module}}.git"> | ||
| 86 | </head> | ||
| 87 | <body>go get {{.Host}}/{{$module}}</body> | ||
| 88 | </html> | ||
| 89 | ``` | ||
| 90 | |||
| 91 | ### Git HTTP — fcgiwrap + git-http-backend | ||
| 92 | |||
| 93 | `fcgiwrap` (apt package) bridges Caddy's FastCGI to git's CGI backend. | ||
| 94 | |||
| 95 | ### Base domain Caddy config | ||
| 96 | |||
| 97 | Generated by `templates.CodeCaddy()`, written to `/etc/caddy/sites-enabled/ship-code.caddy`: | ||
| 98 | |||
| 99 | ```caddyfile | ||
| 100 | {baseDomain} { | ||
| 101 | @goget query go-get=1 | ||
| 102 | handle @goget { | ||
| 103 | root * /opt/ship/vanity | ||
| 104 | templates | ||
| 105 | rewrite * /index.html | ||
| 106 | file_server | ||
| 107 | } | ||
| 108 | |||
| 109 | @git path_regexp "^.*/(HEAD|info/refs|objects/info/[^/]+|git-upload-pack|git-receive-pack)$" | ||
| 110 | handle @git { | ||
| 111 | reverse_proxy unix//run/fcgiwrap.socket { | ||
| 112 | transport fastcgi { | ||
| 113 | env SCRIPT_FILENAME /usr/lib/git-core/git-http-backend | ||
| 114 | env GIT_PROJECT_ROOT /srv/git | ||
| 115 | env GIT_HTTP_EXPORT_ALL 1 | ||
| 116 | env REQUEST_METHOD {method} | ||
| 117 | env QUERY_STRING {query} | ||
| 118 | env PATH_INFO {path} | ||
| 119 | } | ||
| 120 | } | ||
| 121 | } | ||
| 122 | |||
| 123 | handle { | ||
| 124 | respond "not found" 404 | ||
| 125 | } | ||
| 126 | } | ||
| 127 | ``` | ||
| 128 | |||
| 129 | --- | ||
| 130 | |||
| 131 | ## Phase 3: `host init` Rewrite — `cmd/ship/host/init.go` | ||
| 132 | |||
| 133 | Full setup flow: | ||
| 134 | |||
| 135 | 1. **Detect OS** — Ubuntu/Debian only (existing) | ||
| 136 | 2. **Install Caddy** — (existing) | ||
| 137 | 3. **Configure Caddyfile** — (existing) | ||
| 138 | 4. **Install Docker** — Docker's official apt repo, `apt-get install -y docker-ce docker-ce-cli containerd.io` | ||
| 139 | 5. **Install git + fcgiwrap** — `apt-get install -y git fcgiwrap` | ||
| 140 | 6. **Create `git` user** — `useradd -r -m -d /home/git -s $(which git-shell) git`, `usermod -aG docker git` | ||
| 141 | 7. **Copy SSH keys** — admin user's `authorized_keys` → `/home/git/.ssh/authorized_keys` | ||
| 142 | 8. **Create `/srv/git`** — owned by `git:git` | ||
| 143 | 9. **Write sudoers** — `/etc/sudoers.d/ship-git`: passwordless sudo for `systemctl`, `cp` to config dirs, `mkdir`/`chown` | ||
| 144 | 10. **Create directories** — `/etc/ship/env`, `/etc/caddy/sites-enabled`, `/opt/ship/vanity` | ||
| 145 | 11. **Write vanity template** — `/opt/ship/vanity/index.html` | ||
| 146 | 12. **Write base domain Caddy config** — `/etc/caddy/sites-enabled/ship-code.caddy` | ||
| 147 | 13. **Start services** — `systemctl enable --now docker fcgiwrap caddy` | ||
| 148 | 14. **Set `hostState.GitSetup = true`**, save state | ||
| 149 | |||
| 150 | --- | ||
| 151 | |||
| 152 | ## Phase 4: `ship init <name>` — New Command | ||
| 153 | |||
| 154 | **New file:** `cmd/ship/init.go` | ||
| 155 | |||
| 156 | ``` | ||
| 157 | ship init <name> [--static] [--domain custom.example.com] | ||
| 158 | ``` | ||
| 159 | |||
| 160 | Does **both local and remote work**. | ||
| 161 | |||
| 162 | ### Remote (on VPS via SSH): | ||
| 163 | |||
| 164 | 1. Verify `hostState.GitSetup == true` | ||
| 165 | 2. Create bare repo: `sudo -u git git init --bare /srv/git/{name}.git` | ||
| 166 | 3. For apps: create `/var/lib/{name}/data` and `/var/lib/{name}/src` | ||
| 167 | 4. For static: create `/var/www/{name}` | ||
| 168 | 5. Allocate port (apps only) | ||
| 169 | 6. Write env file `/etc/ship/env/{name}.env` with `PORT={port}` and `DATA_DIR=/data` (apps only) | ||
| 170 | 7. Write post-receive hook to `/srv/git/{name}.git/hooks/post-receive` | ||
| 171 | 8. Save state | ||
| 172 | |||
| 173 | ### Local (in current directory): | ||
| 174 | |||
| 175 | 9. Create `.ship/` directory | ||
| 176 | 10. Generate `.ship/Caddyfile` (resolved domain, port) | ||
| 177 | 11. For apps: generate `.ship/service` (resolved name, port) | ||
| 178 | 12. Print git remote URL and next steps | ||
| 179 | |||
| 180 | --- | ||
| 181 | |||
| 182 | ## Phase 5: `ship deploy <name>` — Manual Rebuild | ||
| 183 | |||
| 184 | **New file:** `cmd/ship/deploy_cmd.go` | ||
| 185 | |||
| 186 | ``` | ||
| 187 | ship deploy <name> | ||
| 188 | ``` | ||
| 189 | |||
| 190 | SSH in, trigger same steps as post-receive hook. Stream output to terminal. | ||
| 191 | |||
| 192 | --- | ||
| 193 | |||
| 194 | ## Phase 6: Rewrite Remaining Commands | ||
| 195 | |||
| 196 | ### `root.go` | ||
| 197 | Remove all deploy flags (`--binary`, `--static`, `--dir`, `--port`, `--args`, `--file`, `--memory`, `--cpu`, `--env`, `--env-file`). Root command shows help. Register: `init`, `deploy`, `list`, `logs`, `status`, `restart`, `remove`, `env`, `host`, `ui`, `version`. | ||
| 198 | |||
| 199 | ### `deploy.go` | ||
| 200 | **Delete entirely.** The `runDeploy`, `deployApp`, `deployStatic`, `updateAppConfig`, `DeployOptions` — all removed. Replaced by `ship init` + `git push` + `ship deploy`. | ||
| 201 | |||
| 202 | ### `remove.go` | ||
| 203 | - `"app"`: stop service, remove systemd unit, `docker rmi {name}:latest`, remove `/var/lib/{name}`, remove `/srv/git/{name}.git`, remove Caddy config, remove env file | ||
| 204 | - `"static"`: remove `/var/www/{name}`, remove `/srv/git/{name}.git`, remove Caddy config | ||
| 205 | |||
| 206 | ### `status.go`, `logs.go`, `restart.go` | ||
| 207 | Simplify type guard: only `"static"` type is rejected (no service to manage). | ||
| 208 | |||
| 209 | ### `list.go` | ||
| 210 | Show port for `"app"` type. | ||
| 211 | |||
| 212 | ### `env/` subcommands | ||
| 213 | Work the same — manage `/etc/ship/env/{name}.env` on VPS, restart service. | ||
| 214 | |||
| 215 | --- | ||
| 216 | |||
| 217 | ## Post-Receive Hook Detail | ||
| 218 | |||
| 219 | ### App hook: | ||
| 220 | |||
| 221 | ```bash | ||
| 222 | #!/bin/bash | ||
| 223 | set -euo pipefail | ||
| 224 | |||
| 225 | REPO=/srv/git/{name}.git | ||
| 226 | SRC=/var/lib/{name}/src | ||
| 227 | NAME={name} | ||
| 228 | |||
| 229 | while read oldrev newrev refname; do | ||
| 230 | branch=$(git rev-parse --symbolic --abbrev-ref "$refname") | ||
| 231 | [ "$branch" = "main" ] || { echo "Pushed to $branch, skipping deploy."; exit 0; } | ||
| 232 | done | ||
| 233 | |||
| 234 | echo "==> Checking out code..." | ||
| 235 | git --work-tree="$SRC" --git-dir="$REPO" checkout -f main | ||
| 236 | cd "$SRC" | ||
| 237 | |||
| 238 | # Install deployment config from repo | ||
| 239 | if [ -f .ship/service ]; then | ||
| 240 | echo "==> Installing systemd unit..." | ||
| 241 | sudo cp .ship/service /etc/systemd/system/${NAME}.service | ||
| 242 | sudo systemctl daemon-reload | ||
| 243 | fi | ||
| 244 | if [ -f .ship/Caddyfile ]; then | ||
| 245 | echo "==> Installing Caddy config..." | ||
| 246 | sudo cp .ship/Caddyfile /etc/caddy/sites-enabled/${NAME}.caddy | ||
| 247 | sudo systemctl reload caddy | ||
| 248 | fi | ||
| 249 | |||
| 250 | echo "==> Building Docker image..." | ||
| 251 | docker build -t ${NAME}:latest . | ||
| 252 | |||
| 253 | echo "==> Restarting service..." | ||
| 254 | sudo systemctl restart ${NAME} | ||
| 255 | |||
| 256 | echo "==> Deploy complete!" | ||
| 257 | ``` | ||
| 258 | |||
| 259 | ### Static site hook: | ||
| 260 | |||
| 261 | ```bash | ||
| 262 | #!/bin/bash | ||
| 263 | set -euo pipefail | ||
| 264 | |||
| 265 | REPO=/srv/git/{name}.git | ||
| 266 | WEBROOT=/var/www/{name} | ||
| 267 | NAME={name} | ||
| 268 | |||
| 269 | while read oldrev newrev refname; do | ||
| 270 | branch=$(git rev-parse --symbolic --abbrev-ref "$refname") | ||
| 271 | [ "$branch" = "main" ] || { echo "Pushed to $branch, skipping deploy."; exit 0; } | ||
| 272 | done | ||
| 273 | |||
| 274 | echo "==> Deploying static site..." | ||
| 275 | git --work-tree="$WEBROOT" --git-dir="$REPO" checkout -f main | ||
| 276 | |||
| 277 | if [ -f "$WEBROOT/.ship/Caddyfile" ]; then | ||
| 278 | echo "==> Installing Caddy config..." | ||
| 279 | sudo cp "$WEBROOT/.ship/Caddyfile" /etc/caddy/sites-enabled/${NAME}.caddy | ||
| 280 | sudo systemctl reload caddy | ||
| 281 | fi | ||
| 282 | |||
| 283 | echo "==> Deploy complete!" | ||
| 284 | ``` | ||
| 285 | |||
| 286 | --- | ||
| 287 | |||
| 288 | ## VPS File Layout | ||
| 289 | |||
| 290 | ``` | ||
| 291 | /srv/git/ # bare repos (owned by git) | ||
| 292 | /srv/git/{name}.git/ # per-project bare repo | ||
| 293 | /srv/git/{name}.git/hooks/post-receive # auto-deploy hook | ||
| 294 | /home/git/.ssh/authorized_keys # SSH keys for git push | ||
| 295 | /opt/ship/vanity/index.html # Caddy template for vanity imports | ||
| 296 | /etc/caddy/sites-enabled/ship-code.caddy # base domain Caddy config | ||
| 297 | /etc/sudoers.d/ship-git # sudo rules for git user | ||
| 298 | |||
| 299 | # Per app: | ||
| 300 | /var/lib/{name}/src/ # checked-out source (for docker build) | ||
| 301 | /var/lib/{name}/data/ # persistent data (mounted as /data in container) | ||
| 302 | /etc/systemd/system/{name}.service # installed from .ship/service on push | ||
| 303 | /etc/caddy/sites-enabled/{name}.caddy # installed from .ship/Caddyfile on push | ||
| 304 | /etc/ship/env/{name}.env # env vars (managed by ship env, not in git) | ||
| 305 | |||
| 306 | # Per static site: | ||
| 307 | /var/www/{name}/ # checked-out site files | ||
| 308 | /etc/caddy/sites-enabled/{name}.caddy # installed from .ship/Caddyfile on push | ||
| 309 | ``` | ||
| 310 | |||
| 311 | --- | ||
| 312 | |||
| 313 | ## Files Changed / Created | ||
| 314 | |||
| 315 | | File | Action | | ||
| 316 | |------|--------| | ||
| 317 | | `internal/state/state.go` | Rewrite — simplified App struct, add `Repo`, `GitSetup` | | ||
| 318 | | `internal/templates/templates.go` | Rewrite — replace old templates with new ones | | ||
| 319 | | `cmd/ship/init.go` | **Create** — `ship init <name>` | | ||
| 320 | | `cmd/ship/deploy_cmd.go` | **Create** — `ship deploy <name>` | | ||
| 321 | | `cmd/ship/host/init.go` | Rewrite — add Docker/git/fcgiwrap/vanity/sudoers | | ||
| 322 | | `cmd/ship/root.go` | Rewrite — remove deploy flags, register new commands | | ||
| 323 | | `cmd/ship/deploy.go` | **Delete** | | ||
| 324 | | `cmd/ship/remove.go` | Rewrite — Docker + git cleanup | | ||
| 325 | | `cmd/ship/list.go` | Simplify | | ||
| 326 | | `cmd/ship/status.go` | Simplify | | ||
| 327 | | `cmd/ship/logs.go` | Simplify | | ||
| 328 | | `cmd/ship/restart.go` | Simplify | | ||
| 329 | | `cmd/ship/env/*.go` | Minor updates if needed | | ||
| 330 | |||
| 331 | --- | ||
| 332 | |||
| 333 | ## Implementation Order | ||
| 334 | |||
| 335 | 1. Phase 1 — state + templates | ||
| 336 | 2. Phase 3 — `host init` rewrite | ||
| 337 | 3. Phase 4 — `ship init` command | ||
| 338 | 4. Phase 5 — `ship deploy` command | ||
| 339 | 5. Phase 6 — rewrite remaining commands (root, remove, list, status, logs, restart, delete deploy.go) | ||
| 340 | |||
| 341 | --- | ||
| 342 | |||
| 343 | ## Verification | ||
| 344 | |||
| 345 | 1. `ship host init --host myserver --base-domain example.com` — installs Docker, git, fcgiwrap, Caddy, creates git user, writes vanity template + Caddy config | ||
| 346 | 2. `ship init myapp` — creates bare repo on VPS, generates `.ship/service` and `.ship/Caddyfile` locally | ||
| 347 | 3. `git add .ship/ Dockerfile && git commit && git push ship main` — hook installs configs, builds Docker image, app goes live at `https://myapp.example.com` | ||
| 348 | 4. Edit `.ship/service` to add `--memory=512m`, push — service updated | ||
| 349 | 5. `curl -s 'https://example.com/myapp?go-get=1'` — returns `<meta go-import>` tag | ||
| 350 | 6. `go get example.com/myapp` — vanity import → HTTPS clone → works | ||
| 351 | 7. `ship list` — shows myapp | ||
| 352 | 8. `ship logs myapp` / `ship status myapp` / `ship restart myapp` — work | ||
| 353 | 9. `ship env set myapp SECRET=foo` — updates env, restarts | ||
| 354 | 10. `ship deploy myapp` — manually rebuilds | ||
| 355 | 11. `ship remove myapp` — cleans up everything | ||
| 356 | 12. `ship init mysite --static` + push — static site deployed | ||
