From 7a29f83af2d23f6f3399e19f3879c667287252ed Mon Sep 17 00:00:00 2001 From: bndw Date: Sun, 15 Feb 2026 17:17:18 -0800 Subject: random design files --- PLAN_v0.2.0.md | 356 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 356 insertions(+) create mode 100644 PLAN_v0.2.0.md (limited to 'PLAN_v0.2.0.md') diff --git a/PLAN_v0.2.0.md b/PLAN_v0.2.0.md new file mode 100644 index 0000000..d48e95c --- /dev/null +++ b/PLAN_v0.2.0.md @@ -0,0 +1,356 @@ +# Plan: Git-Centric Deployment with Docker Builds and Vanity Imports + +## Context + +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. + +Using Docker for builds means the VPS only needs Docker installed — no language-specific toolchains. Any language with a Dockerfile works. + +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`. + +The base domain pulls double duty: `example.com/foo` serves vanity imports, `foo.example.com` runs the app. + +No backward compatibility with the current binary-upload flow — clean slate. + +--- + +## User Flow + +``` +ship host init --host myserver --base-domain example.com # one-time VPS setup +ship init myapp # creates bare repo on VPS + .ship/ locally +git add .ship/ Dockerfile && git commit -m "initial deploy" # commit deploy config +git remote add ship git@myserver:/srv/git/myapp.git +git push ship main # installs configs + docker build + deploy +``` + +To change config (memory limit, Caddy rules, etc.): edit `.ship/service` or `.ship/Caddyfile`, commit, push. + +--- + +## Phase 1: State & Template Rewrite + +### 1A. State — `internal/state/state.go` + +Simplified structs: + +```go +type Host struct { + NextPort int `json:"next_port"` + BaseDomain string `json:"base_domain,omitempty"` + GitSetup bool `json:"git_setup,omitempty"` + Apps map[string]*App `json:"apps"` +} + +type App struct { + Type string `json:"type"` // "app" or "static" + Domain string `json:"domain"` + Port int `json:"port,omitempty"` // only for "app" + Env map[string]string `json:"env,omitempty"` // only for "app" + Repo string `json:"repo"` // e.g., "/srv/git/foo.git" +} +``` + +Removed from App: `Args`, `Files`, `Memory`, `CPU` — these are now in `.ship/service` inside the repo, not in ship's state. + +### 1B. Templates — `internal/templates/templates.go` + +Replace existing templates with: + +1. **`PostReceiveHook(data)`** — app deploy hook (see Hook Detail below) +2. **`PostReceiveHookStatic(data)`** — static site deploy hook +3. **`CodeCaddy(data)`** — base domain Caddy config for vanity imports + git HTTP +4. **`DockerService(data)`** — default systemd unit (generated into `.ship/service` locally) +5. **`AppCaddy(data)`** — default app Caddyfile (generated into `.ship/Caddyfile` locally) +6. **`StaticCaddy(data)`** — default static Caddyfile (generated into `.ship/Caddyfile` locally) + +Remove old: `serviceTemplate`, `appCaddyTemplate`, `staticCaddyTemplate` and their render functions (`SystemdService`, `AppCaddy`, `StaticCaddy`). + +--- + +## Phase 2: Caddy-Native Vanity Imports + Git HTTP + +Caddy handles everything directly. No separate server binary. + +### Vanity imports — Caddy `templates` directive + +HTML template at `/opt/ship/vanity/index.html` on VPS: + +```html + + +{{$path := trimPrefix "/" .Req.URL.Path}} +{{$parts := splitList "/" $path}} +{{$module := first $parts}} + + +go get {{.Host}}/{{$module}} + +``` + +### Git HTTP — fcgiwrap + git-http-backend + +`fcgiwrap` (apt package) bridges Caddy's FastCGI to git's CGI backend. + +### Base domain Caddy config + +Generated by `templates.CodeCaddy()`, written to `/etc/caddy/sites-enabled/ship-code.caddy`: + +```caddyfile +{baseDomain} { + @goget query go-get=1 + handle @goget { + root * /opt/ship/vanity + templates + rewrite * /index.html + file_server + } + + @git path_regexp "^.*/(HEAD|info/refs|objects/info/[^/]+|git-upload-pack|git-receive-pack)$" + handle @git { + reverse_proxy unix//run/fcgiwrap.socket { + transport fastcgi { + env SCRIPT_FILENAME /usr/lib/git-core/git-http-backend + env GIT_PROJECT_ROOT /srv/git + env GIT_HTTP_EXPORT_ALL 1 + env REQUEST_METHOD {method} + env QUERY_STRING {query} + env PATH_INFO {path} + } + } + } + + handle { + respond "not found" 404 + } +} +``` + +--- + +## Phase 3: `host init` Rewrite — `cmd/ship/host/init.go` + +Full setup flow: + +1. **Detect OS** — Ubuntu/Debian only (existing) +2. **Install Caddy** — (existing) +3. **Configure Caddyfile** — (existing) +4. **Install Docker** — Docker's official apt repo, `apt-get install -y docker-ce docker-ce-cli containerd.io` +5. **Install git + fcgiwrap** — `apt-get install -y git fcgiwrap` +6. **Create `git` user** — `useradd -r -m -d /home/git -s $(which git-shell) git`, `usermod -aG docker git` +7. **Copy SSH keys** — admin user's `authorized_keys` → `/home/git/.ssh/authorized_keys` +8. **Create `/srv/git`** — owned by `git:git` +9. **Write sudoers** — `/etc/sudoers.d/ship-git`: passwordless sudo for `systemctl`, `cp` to config dirs, `mkdir`/`chown` +10. **Create directories** — `/etc/ship/env`, `/etc/caddy/sites-enabled`, `/opt/ship/vanity` +11. **Write vanity template** — `/opt/ship/vanity/index.html` +12. **Write base domain Caddy config** — `/etc/caddy/sites-enabled/ship-code.caddy` +13. **Start services** — `systemctl enable --now docker fcgiwrap caddy` +14. **Set `hostState.GitSetup = true`**, save state + +--- + +## Phase 4: `ship init ` — New Command + +**New file:** `cmd/ship/init.go` + +``` +ship init [--static] [--domain custom.example.com] +``` + +Does **both local and remote work**. + +### Remote (on VPS via SSH): + +1. Verify `hostState.GitSetup == true` +2. Create bare repo: `sudo -u git git init --bare /srv/git/{name}.git` +3. For apps: create `/var/lib/{name}/data` and `/var/lib/{name}/src` +4. For static: create `/var/www/{name}` +5. Allocate port (apps only) +6. Write env file `/etc/ship/env/{name}.env` with `PORT={port}` and `DATA_DIR=/data` (apps only) +7. Write post-receive hook to `/srv/git/{name}.git/hooks/post-receive` +8. Save state + +### Local (in current directory): + +9. Create `.ship/` directory +10. Generate `.ship/Caddyfile` (resolved domain, port) +11. For apps: generate `.ship/service` (resolved name, port) +12. Print git remote URL and next steps + +--- + +## Phase 5: `ship deploy ` — Manual Rebuild + +**New file:** `cmd/ship/deploy_cmd.go` + +``` +ship deploy +``` + +SSH in, trigger same steps as post-receive hook. Stream output to terminal. + +--- + +## Phase 6: Rewrite Remaining Commands + +### `root.go` +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`. + +### `deploy.go` +**Delete entirely.** The `runDeploy`, `deployApp`, `deployStatic`, `updateAppConfig`, `DeployOptions` — all removed. Replaced by `ship init` + `git push` + `ship deploy`. + +### `remove.go` +- `"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 +- `"static"`: remove `/var/www/{name}`, remove `/srv/git/{name}.git`, remove Caddy config + +### `status.go`, `logs.go`, `restart.go` +Simplify type guard: only `"static"` type is rejected (no service to manage). + +### `list.go` +Show port for `"app"` type. + +### `env/` subcommands +Work the same — manage `/etc/ship/env/{name}.env` on VPS, restart service. + +--- + +## Post-Receive Hook Detail + +### App hook: + +```bash +#!/bin/bash +set -euo pipefail + +REPO=/srv/git/{name}.git +SRC=/var/lib/{name}/src +NAME={name} + +while read oldrev newrev refname; do + branch=$(git rev-parse --symbolic --abbrev-ref "$refname") + [ "$branch" = "main" ] || { echo "Pushed to $branch, skipping deploy."; exit 0; } +done + +echo "==> Checking out code..." +git --work-tree="$SRC" --git-dir="$REPO" checkout -f main +cd "$SRC" + +# Install deployment config from repo +if [ -f .ship/service ]; then + echo "==> Installing systemd unit..." + sudo cp .ship/service /etc/systemd/system/${NAME}.service + sudo systemctl daemon-reload +fi +if [ -f .ship/Caddyfile ]; then + echo "==> Installing Caddy config..." + sudo cp .ship/Caddyfile /etc/caddy/sites-enabled/${NAME}.caddy + sudo systemctl reload caddy +fi + +echo "==> Building Docker image..." +docker build -t ${NAME}:latest . + +echo "==> Restarting service..." +sudo systemctl restart ${NAME} + +echo "==> Deploy complete!" +``` + +### Static site hook: + +```bash +#!/bin/bash +set -euo pipefail + +REPO=/srv/git/{name}.git +WEBROOT=/var/www/{name} +NAME={name} + +while read oldrev newrev refname; do + branch=$(git rev-parse --symbolic --abbrev-ref "$refname") + [ "$branch" = "main" ] || { echo "Pushed to $branch, skipping deploy."; exit 0; } +done + +echo "==> Deploying static site..." +git --work-tree="$WEBROOT" --git-dir="$REPO" checkout -f main + +if [ -f "$WEBROOT/.ship/Caddyfile" ]; then + echo "==> Installing Caddy config..." + sudo cp "$WEBROOT/.ship/Caddyfile" /etc/caddy/sites-enabled/${NAME}.caddy + sudo systemctl reload caddy +fi + +echo "==> Deploy complete!" +``` + +--- + +## VPS File Layout + +``` +/srv/git/ # bare repos (owned by git) +/srv/git/{name}.git/ # per-project bare repo +/srv/git/{name}.git/hooks/post-receive # auto-deploy hook +/home/git/.ssh/authorized_keys # SSH keys for git push +/opt/ship/vanity/index.html # Caddy template for vanity imports +/etc/caddy/sites-enabled/ship-code.caddy # base domain Caddy config +/etc/sudoers.d/ship-git # sudo rules for git user + +# Per app: +/var/lib/{name}/src/ # checked-out source (for docker build) +/var/lib/{name}/data/ # persistent data (mounted as /data in container) +/etc/systemd/system/{name}.service # installed from .ship/service on push +/etc/caddy/sites-enabled/{name}.caddy # installed from .ship/Caddyfile on push +/etc/ship/env/{name}.env # env vars (managed by ship env, not in git) + +# Per static site: +/var/www/{name}/ # checked-out site files +/etc/caddy/sites-enabled/{name}.caddy # installed from .ship/Caddyfile on push +``` + +--- + +## Files Changed / Created + +| File | Action | +|------|--------| +| `internal/state/state.go` | Rewrite — simplified App struct, add `Repo`, `GitSetup` | +| `internal/templates/templates.go` | Rewrite — replace old templates with new ones | +| `cmd/ship/init.go` | **Create** — `ship init ` | +| `cmd/ship/deploy_cmd.go` | **Create** — `ship deploy ` | +| `cmd/ship/host/init.go` | Rewrite — add Docker/git/fcgiwrap/vanity/sudoers | +| `cmd/ship/root.go` | Rewrite — remove deploy flags, register new commands | +| `cmd/ship/deploy.go` | **Delete** | +| `cmd/ship/remove.go` | Rewrite — Docker + git cleanup | +| `cmd/ship/list.go` | Simplify | +| `cmd/ship/status.go` | Simplify | +| `cmd/ship/logs.go` | Simplify | +| `cmd/ship/restart.go` | Simplify | +| `cmd/ship/env/*.go` | Minor updates if needed | + +--- + +## Implementation Order + +1. Phase 1 — state + templates +2. Phase 3 — `host init` rewrite +3. Phase 4 — `ship init` command +4. Phase 5 — `ship deploy` command +5. Phase 6 — rewrite remaining commands (root, remove, list, status, logs, restart, delete deploy.go) + +--- + +## Verification + +1. `ship host init --host myserver --base-domain example.com` — installs Docker, git, fcgiwrap, Caddy, creates git user, writes vanity template + Caddy config +2. `ship init myapp` — creates bare repo on VPS, generates `.ship/service` and `.ship/Caddyfile` locally +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` +4. Edit `.ship/service` to add `--memory=512m`, push — service updated +5. `curl -s 'https://example.com/myapp?go-get=1'` — returns `` tag +6. `go get example.com/myapp` — vanity import → HTTPS clone → works +7. `ship list` — shows myapp +8. `ship logs myapp` / `ship status myapp` / `ship restart myapp` — work +9. `ship env set myapp SECRET=foo` — updates env, restarts +10. `ship deploy myapp` — manually rebuilds +11. `ship remove myapp` — cleans up everything +12. `ship init mysite --static` + push — static site deployed -- cgit v1.2.3