summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-02-15 17:17:18 -0800
committerbndw <ben@bdw.to>2026-02-15 17:17:18 -0800
commit7a29f83af2d23f6f3399e19f3879c667287252ed (patch)
tree561bfc460759333196da687a6d93014e59ac4b36
parent702cb02831be836eeda91cd58504aeca45b1a260 (diff)
random design files
-rw-r--r--GIT_AND_GOMOD_PLAN.md230
-rw-r--r--PLAN_v0.2.0.md356
2 files changed, 586 insertions, 0 deletions
diff --git a/GIT_AND_GOMOD_PLAN.md b/GIT_AND_GOMOD_PLAN.md
new file mode 100644
index 0000000..183154f
--- /dev/null
+++ b/GIT_AND_GOMOD_PLAN.md
@@ -0,0 +1,230 @@
1# Go Vanity Import + Git Server Architecture
2
3This document describes a **simple, robust architecture** for hosting Go module source code and enabling `go get` on a **custom domain**, using:
4
5* A basic Git server (authoritative source)
6* Go vanity imports
7* Caddy for HTTPS with automatic TLS
8
9The design prioritizes:
10
11* Simplicity
12* Long-term correctness
13* Zero client-side configuration
14* Easy future migration to a module proxy if needed
15
16---
17
18## Goals
19
20* Host all authored Go modules under a single domain (e.g. `yourdomain.com/foo`)
21* Allow anyone to run:
22
23 ```bash
24 go get yourdomain.com/foo
25 ```
26
27 with no environment variables or flags
28* Retain full control of source code and hosting
29* Avoid unnecessary complexity (S3 proxies, CI artifact pipelines) for a single author
30
31---
32
33## High-Level Architecture
34
35```
36yourdomain.com
37 ├─ /<module> → Vanity import discovery (?go-get=1)
38 ├─ /<module>.git → Git repository (HTTPS, read-only)
39 └─ ssh://git@yourdomain.com/<module>.git → Git writes (SSH)
40```
41
42Components:
43
44* **Caddy**: Fronts everything, provides HTTPS automatically
45* **Vanity Import Server**: Tiny Go HTTP server that serves `<meta go-import>` tags
46* **Git Server**: Bare Git repositories served over HTTPS and SSH
47
48---
49
50## How `go get` Works in This Setup
51
52When a user runs:
53
54```bash
55go get yourdomain.com/foo
56```
57
58Go performs the following steps:
59
601. **Vanity discovery**
61
62 ```
63 GET https://yourdomain.com/foo?go-get=1
64 ```
65
662. **Vanity server responds** with:
67
68 ```html
69 <meta name="go-import"
70 content="yourdomain.com/foo git https://yourdomain.com/foo.git">
71 ```
72
733. **Go clones the repository**:
74
75 ```
76 git clone https://yourdomain.com/foo.git
77 ```
78
794. **Go reads `go.mod`**, checks tags (e.g. `v1.2.3`), and builds the module
80
81No Go module proxy is involved.
82
83---
84
85## Module Requirements
86
87Each module repository **must**:
88
89* Be located at `/srv/git/<module>.git`
90* Have a `go.mod` file with:
91
92 ```go
93 module yourdomain.com/<module>
94 ```
95* Use semantic version tags:
96
97 ```
98 v1.0.0
99 v1.2.3
100 ```
101
102Tags must not be rewritten after publication.
103
104---
105
106## Vanity Import Server
107
108A single Go HTTP server can serve vanity imports for **all modules**.
109
110### Responsibilities
111
112* Respond only to `?go-get=1` requests
113* Dynamically map paths to Git repositories
114* Serve static HTML with `<meta name="go-import">`
115
116### Behavior
117
118For a request to:
119
120```
121/foo?go-get=1
122```
123
124The server returns:
125
126```html
127<meta name="go-import"
128 content="yourdomain.com/foo git https://yourdomain.com/foo.git">
129```
130
131All non-`go-get` requests return `404` (or are handled elsewhere).
132
133---
134
135## Git Server
136
137### Repository Layout
138
139```
140/srv/git/
141 ├─ foo.git
142 ├─ bar.git
143 └─ baz.git
144```
145
146Repositories are:
147
148* **Bare**
149* Read-only over HTTPS
150* Writable only via SSH
151
152### Write Access
153
154* SSH only
155* Single `git` user
156* Access controlled via `authorized_keys`
157
158Example push:
159
160```bash
161git push git@yourdomain.com:/srv/git/foo.git
162```
163
164---
165
166## Caddy Configuration
167
168Caddy is used as the front-facing server.
169
170### Responsibilities
171
172* Automatic HTTPS (Let’s Encrypt)
173* Route `.git` paths to `git-http-backend`
174* Route all other paths to the vanity import server
175
176### Conceptual Routing
177
178* `/*.git*` → Git HTTP backend
179* `/*` → Vanity import server
180
181This keeps TLS, routing, and process management simple and centralized.
182
183---
184
185## Why This Design Is “Bullet-Proof Simple”
186
187### Advantages
188
189* No module proxy implementation required
190* No S3, CloudFront, or artifact format rules
191* No CI publishing pipeline needed
192* Fully compatible with Go tooling
193* Easy to debug (if `git clone` works, `go get` works)
194* Mirrors Go’s original and still-supported design
195
196### Trade-offs
197
198* Git-based fetching is slower than a proxy
199* No immutable release guarantees beyond Git tags
200* Less suitable for very high traffic
201
202For a single-author, self-hosted setup, these trade-offs are acceptable.
203
204---
205
206## Future Migration Path (Optional)
207
208If needed later, this setup can evolve into a module proxy:
209
210* Keep vanity import paths unchanged
211* Switch `<meta go-import>` from `git` → `mod`
212* Introduce an S3-backed module proxy
213* Users do not need to change imports or commands
214
215This design does not lock you into Git forever.
216
217---
218
219## Summary
220
221This architecture provides:
222
223* A single, authoritative domain for all Go modules
224* Simple Git-based source hosting
225* Zero-config `go get` for users
226* Minimal operational complexity
227* A clean upgrade path to a full module proxy
228
229It is the simplest solution that is still fully correct and future-proof for Go modules.
230
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 @@
1# Plan: Git-Centric Deployment with Docker Builds and Vanity Imports
2
3## Context
4
5Ship 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
7Using Docker for builds means the VPS only needs Docker installed — no language-specific toolchains. Any language with a Dockerfile works.
8
9Deployment 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
11The base domain pulls double duty: `example.com/foo` serves vanity imports, `foo.example.com` runs the app.
12
13No backward compatibility with the current binary-upload flow — clean slate.
14
15---
16
17## User Flow
18
19```
20ship host init --host myserver --base-domain example.com # one-time VPS setup
21ship init myapp # creates bare repo on VPS + .ship/ locally
22git add .ship/ Dockerfile && git commit -m "initial deploy" # commit deploy config
23git remote add ship git@myserver:/srv/git/myapp.git
24git push ship main # installs configs + docker build + deploy
25```
26
27To 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
35Simplified structs:
36
37```go
38type 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
45type 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
54Removed 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
58Replace existing templates with:
59
601. **`PostReceiveHook(data)`** — app deploy hook (see Hook Detail below)
612. **`PostReceiveHookStatic(data)`** — static site deploy hook
623. **`CodeCaddy(data)`** — base domain Caddy config for vanity imports + git HTTP
634. **`DockerService(data)`** — default systemd unit (generated into `.ship/service` locally)
645. **`AppCaddy(data)`** — default app Caddyfile (generated into `.ship/Caddyfile` locally)
656. **`StaticCaddy(data)`** — default static Caddyfile (generated into `.ship/Caddyfile` locally)
66
67Remove old: `serviceTemplate`, `appCaddyTemplate`, `staticCaddyTemplate` and their render functions (`SystemdService`, `AppCaddy`, `StaticCaddy`).
68
69---
70
71## Phase 2: Caddy-Native Vanity Imports + Git HTTP
72
73Caddy handles everything directly. No separate server binary.
74
75### Vanity imports — Caddy `templates` directive
76
77HTML 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
97Generated 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
133Full setup flow:
134
1351. **Detect OS** — Ubuntu/Debian only (existing)
1362. **Install Caddy** — (existing)
1373. **Configure Caddyfile** — (existing)
1384. **Install Docker** — Docker's official apt repo, `apt-get install -y docker-ce docker-ce-cli containerd.io`
1395. **Install git + fcgiwrap** — `apt-get install -y git fcgiwrap`
1406. **Create `git` user** — `useradd -r -m -d /home/git -s $(which git-shell) git`, `usermod -aG docker git`
1417. **Copy SSH keys** — admin user's `authorized_keys` → `/home/git/.ssh/authorized_keys`
1428. **Create `/srv/git`** — owned by `git:git`
1439. **Write sudoers** — `/etc/sudoers.d/ship-git`: passwordless sudo for `systemctl`, `cp` to config dirs, `mkdir`/`chown`
14410. **Create directories** — `/etc/ship/env`, `/etc/caddy/sites-enabled`, `/opt/ship/vanity`
14511. **Write vanity template** — `/opt/ship/vanity/index.html`
14612. **Write base domain Caddy config** — `/etc/caddy/sites-enabled/ship-code.caddy`
14713. **Start services** — `systemctl enable --now docker fcgiwrap caddy`
14814. **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```
157ship init <name> [--static] [--domain custom.example.com]
158```
159
160Does **both local and remote work**.
161
162### Remote (on VPS via SSH):
163
1641. Verify `hostState.GitSetup == true`
1652. Create bare repo: `sudo -u git git init --bare /srv/git/{name}.git`
1663. For apps: create `/var/lib/{name}/data` and `/var/lib/{name}/src`
1674. For static: create `/var/www/{name}`
1685. Allocate port (apps only)
1696. Write env file `/etc/ship/env/{name}.env` with `PORT={port}` and `DATA_DIR=/data` (apps only)
1707. Write post-receive hook to `/srv/git/{name}.git/hooks/post-receive`
1718. Save state
172
173### Local (in current directory):
174
1759. Create `.ship/` directory
17610. Generate `.ship/Caddyfile` (resolved domain, port)
17711. For apps: generate `.ship/service` (resolved name, port)
17812. 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```
187ship deploy <name>
188```
189
190SSH 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`
197Remove 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`
207Simplify type guard: only `"static"` type is rejected (no service to manage).
208
209### `list.go`
210Show port for `"app"` type.
211
212### `env/` subcommands
213Work 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
223set -euo pipefail
224
225REPO=/srv/git/{name}.git
226SRC=/var/lib/{name}/src
227NAME={name}
228
229while 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; }
232done
233
234echo "==> Checking out code..."
235git --work-tree="$SRC" --git-dir="$REPO" checkout -f main
236cd "$SRC"
237
238# Install deployment config from repo
239if [ -f .ship/service ]; then
240 echo "==> Installing systemd unit..."
241 sudo cp .ship/service /etc/systemd/system/${NAME}.service
242 sudo systemctl daemon-reload
243fi
244if [ -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
248fi
249
250echo "==> Building Docker image..."
251docker build -t ${NAME}:latest .
252
253echo "==> Restarting service..."
254sudo systemctl restart ${NAME}
255
256echo "==> Deploy complete!"
257```
258
259### Static site hook:
260
261```bash
262#!/bin/bash
263set -euo pipefail
264
265REPO=/srv/git/{name}.git
266WEBROOT=/var/www/{name}
267NAME={name}
268
269while 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; }
272done
273
274echo "==> Deploying static site..."
275git --work-tree="$WEBROOT" --git-dir="$REPO" checkout -f main
276
277if [ -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
281fi
282
283echo "==> 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
3351. Phase 1 — state + templates
3362. Phase 3 — `host init` rewrite
3373. Phase 4 — `ship init` command
3384. Phase 5 — `ship deploy` command
3395. Phase 6 — rewrite remaining commands (root, remove, list, status, logs, restart, delete deploy.go)
340
341---
342
343## Verification
344
3451. `ship host init --host myserver --base-domain example.com` — installs Docker, git, fcgiwrap, Caddy, creates git user, writes vanity template + Caddy config
3462. `ship init myapp` — creates bare repo on VPS, generates `.ship/service` and `.ship/Caddyfile` locally
3473. `git add .ship/ Dockerfile && git commit && git push ship main` — hook installs configs, builds Docker image, app goes live at `https://myapp.example.com`
3484. Edit `.ship/service` to add `--memory=512m`, push — service updated
3495. `curl -s 'https://example.com/myapp?go-get=1'` — returns `<meta go-import>` tag
3506. `go get example.com/myapp` — vanity import → HTTPS clone → works
3517. `ship list` — shows myapp
3528. `ship logs myapp` / `ship status myapp` / `ship restart myapp` — work
3539. `ship env set myapp SECRET=foo` — updates env, restarts
35410. `ship deploy myapp` — manually rebuilds
35511. `ship remove myapp` — cleans up everything
35612. `ship init mysite --static` + push — static site deployed