diff options
| author | bndw <ben@bdw.to> | 2026-02-14 07:56:22 -0800 |
|---|---|---|
| committer | bndw <ben@bdw.to> | 2026-02-14 07:56:22 -0800 |
| commit | f0dfabe5b7f1f8d23169c6e62a2f0c27bd6c5463 (patch) | |
| tree | e1f3b1f32b4f810cf957fef53d8fcec4d1fead18 /README.md | |
| parent | f5b667c80e49117c94481d49c5b0c77dbcf2804a (diff) | |
Add cgit web interface for browsing repos
Adds cgit as a web frontend for browsing git repositories. Visiting the
base domain now shows a cgit repo index with trees, commits, diffs, and
blame views. Public repos (marked with git-daemon-export-ok) are browsable
and cloneable over HTTPS.
- Install cgit during host init
- Configure cgit with dark theme and base domain integration
- Add cgit CGI handler to base domain Caddyfile
- Update README to emphasize git-centric workflow with cgit frontend
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 202 |
1 files changed, 40 insertions, 162 deletions
| @@ -1,13 +1,14 @@ | |||
| 1 | # Ship | 1 | # Ship |
| 2 | 2 | ||
| 3 | Ship deploys apps and static sites to a VPS over SSH. It handles HTTPS certificates, port allocation, systemd services, and reverse proxying — all with zero dependencies beyond SSH access. | 3 | Ship turns a VPS into a self-hosted git server with a web frontend — similar to running cgit on your own domain. Visiting your base domain in a browser shows a cgit repo index; clicking through shows trees, commit logs, diffs, and blame. Public repos are cloneable over HTTPS. If you host Go code, `go get` works with your domain out of the box. This is the read-only, public-facing side, and it works exactly the way cgit users expect. |
| 4 | 4 | ||
| 5 | There are two deployment modes: | 5 | The difference is what happens on the write side. When you `git push` over SSH, Ship doesn't just update the bare repo — it builds and deploys your code. A post-receive hook checks out the repo, runs `docker build`, installs a systemd service and Caddy reverse-proxy config, and restarts the app. Your deployment config (the systemd unit, the Caddyfile) lives in `.ship/` in your repo and is versioned alongside your code. Push to main and it's live; push to any other branch and nothing happens. |
| 6 | 6 | ||
| 7 | - **Git push** — push to a bare repo on the VPS, which triggers a Docker build and deploy via post-receive hooks. Deployment config (systemd unit, Caddyfile) lives in `.ship/` in your repo and is versioned alongside code. | 7 | Not every repo needs to be a running service. If there's no Dockerfile, the push is accepted but the deploy step is skipped — the repo just sits there as a browsable, cloneable library. This makes Ship useful for Go modules that only need vanity imports and a public source view, alongside apps that need the full build-and-deploy pipeline. The same base domain serves both. |
| 8 | - **Direct** — SCP a pre-built binary or rsync a static directory to the VPS. Ship generates and installs the systemd unit and Caddy config on your behalf. | ||
| 9 | 8 | ||
| 10 | If a base domain is configured, Ship also serves **Go vanity imports** and **git HTTPS cloning** from the same domain, so `go get yourdomain.com/foo` works with zero extra setup. | 9 | Ship also supports direct deploys (SCP a binary or rsync a static directory) for cases where git push isn't the right fit. |
| 10 | |||
| 11 | Ship is a client-side CLI. All state lives on your laptop at `~/.config/ship/state.json`. The VPS is configured entirely over SSH — no agent or daemon runs on the server. | ||
| 11 | 12 | ||
| 12 | ## Install | 13 | ## Install |
| 13 | 14 | ||
| @@ -29,13 +30,7 @@ go build -o ship ./cmd/ship | |||
| 29 | ship host init --host user@your-vps --base-domain example.com | 30 | ship host init --host user@your-vps --base-domain example.com |
| 30 | ``` | 31 | ``` |
| 31 | 32 | ||
| 32 | This installs Caddy, Docker, git, and fcgiwrap. It creates a `git` user for push access, configures sudoers for deploy hooks, sets up vanity import serving, and enables automatic HTTPS. The host becomes the default for subsequent commands. | 33 | This installs Caddy, Docker, git, fcgiwrap, and cgit. It creates a `git` user for push access, configures sudoers for deploy hooks, and enables automatic HTTPS. The host becomes the default for subsequent commands. |
| 33 | |||
| 34 | If you don't need git-push deploys or vanity imports, omit `--base-domain`: | ||
| 35 | |||
| 36 | ``` | ||
| 37 | ship host init --host user@your-vps | ||
| 38 | ``` | ||
| 39 | 34 | ||
| 40 | ### 2. Deploy | 35 | ### 2. Deploy |
| 41 | 36 | ||
| @@ -43,18 +38,11 @@ ship host init --host user@your-vps | |||
| 43 | 38 | ||
| 44 | ``` | 39 | ``` |
| 45 | ship init myapp | 40 | ship init myapp |
| 46 | ``` | ||
| 47 | |||
| 48 | This creates a bare git repo on the VPS, generates `.ship/Caddyfile` and `.ship/service` locally, initializes a local git repo if needed, and adds an `origin` remote. | ||
| 49 | |||
| 50 | ``` | ||
| 51 | git add .ship/ Dockerfile | 41 | git add .ship/ Dockerfile |
| 52 | git commit -m "initial deploy" | 42 | git commit -m "initial deploy" |
| 53 | git push origin main | 43 | git push origin main |
| 54 | ``` | 44 | ``` |
| 55 | 45 | ||
| 56 | The post-receive hook checks out code, installs `.ship/` configs, runs `docker build`, and restarts the service. If no Dockerfile is present, the push is accepted but deploy is skipped — useful for Go modules and libraries that only need vanity imports. | ||
| 57 | |||
| 58 | **Git push (static site):** | 46 | **Git push (static site):** |
| 59 | 47 | ||
| 60 | ``` | 48 | ``` |
| @@ -64,19 +52,26 @@ git commit -m "initial deploy" | |||
| 64 | git push origin main | 52 | git push origin main |
| 65 | ``` | 53 | ``` |
| 66 | 54 | ||
| 67 | **Direct (pre-built binary):** | 55 | **Git push (library / Go module):** |
| 68 | 56 | ||
| 69 | ``` | 57 | ``` |
| 70 | GOOS=linux GOARCH=amd64 go build -o myapp | 58 | ship init mylib --public |
| 71 | ship --binary ./myapp --domain api.example.com | 59 | git add . |
| 60 | git commit -m "initial" | ||
| 61 | git push origin main | ||
| 72 | ``` | 62 | ``` |
| 73 | 63 | ||
| 74 | **Direct (static site):** | 64 | No Dockerfile, so nothing is deployed — the repo is just browsable and cloneable at `https://example.com/mylib`. |
| 65 | |||
| 66 | **Direct (pre-built binary):** | ||
| 75 | 67 | ||
| 76 | ``` | 68 | ``` |
| 77 | ship --static --dir ./dist --domain example.com | 69 | GOOS=linux GOARCH=amd64 go build -o myapp |
| 70 | ship --binary ./myapp --domain api.example.com | ||
| 78 | ``` | 71 | ``` |
| 79 | 72 | ||
| 73 | You can version control `.ship/` or add it to `.gitignore` — it's your choice. | ||
| 74 | |||
| 80 | ## Commands | 75 | ## Commands |
| 81 | 76 | ||
| 82 | ### `ship init <name>` | 77 | ### `ship init <name>` |
| @@ -84,193 +79,76 @@ ship --static --dir ./dist --domain example.com | |||
| 84 | Create a bare git repo on the VPS and generate local `.ship/` config files. | 79 | Create a bare git repo on the VPS and generate local `.ship/` config files. |
| 85 | 80 | ||
| 86 | ``` | 81 | ``` |
| 87 | ship init myapp # Docker-based app | 82 | ship init myapp # Docker-based app |
| 88 | ship init mysite --static # static site | 83 | ship init mysite --static # static site |
| 89 | ship init myapp --domain custom.example.com # custom domain | 84 | ship init myapp --domain custom.example.com # custom domain |
| 90 | ship init mylib --public # publicly cloneable (for go get) | 85 | ship init mylib --public # publicly cloneable (for go get) |
| 91 | ``` | 86 | ``` |
| 92 | 87 | ||
| 93 | Flags: | ||
| 94 | - `--static` — initialize as a static site instead of a Docker app | ||
| 95 | - `--public` — make the repo publicly cloneable over HTTPS | ||
| 96 | - `--domain` — custom domain (default: `name.basedomain`) | ||
| 97 | |||
| 98 | ### `ship deploy <name>` | 88 | ### `ship deploy <name>` |
| 99 | 89 | ||
| 100 | Manually rebuild and deploy a git-deployed app. Runs the same steps as the post-receive hook: checkout, install configs, docker build, restart. | 90 | Manually rebuild and deploy a git-deployed app. |
| 101 | |||
| 102 | ``` | ||
| 103 | ship deploy myapp | ||
| 104 | ``` | ||
| 105 | 91 | ||
| 106 | ### `ship [deploy flags]` | 92 | ### `ship [deploy flags]` |
| 107 | 93 | ||
| 108 | Deploy a pre-built binary or static directory directly. | 94 | Deploy a pre-built binary or static directory directly. |
| 109 | 95 | ||
| 110 | ``` | 96 | ``` |
| 111 | # App | ||
| 112 | ship --binary ./myapp --domain api.example.com | 97 | ship --binary ./myapp --domain api.example.com |
| 113 | ship --binary ./myapp --domain api.example.com --env DB_HOST=localhost --env API_KEY=secret | 98 | ship --binary ./myapp --domain api.example.com --env DB_HOST=localhost |
| 114 | ship --binary ./myapp --domain api.example.com --env-file .env.production | ||
| 115 | ship --binary ./myapp --name myapi --memory 512M --cpu 50% | ||
| 116 | |||
| 117 | # Static site | ||
| 118 | ship --static --dir ./dist --domain example.com | 99 | ship --static --dir ./dist --domain example.com |
| 119 | 100 | ship --name myapi --memory 512M --cpu 50% | |
| 120 | # Config update (no binary, just change settings) | ||
| 121 | ship --name myapi --memory 1G | ||
| 122 | ship --name myapi --env DEBUG=true | ||
| 123 | ``` | 101 | ``` |
| 124 | 102 | ||
| 125 | Flags: | 103 | Flags: `--binary`, `--static`, `--dir`, `--domain`, `--name`, `--env`, `--env-file`, `--args`, `--file`, `--memory`, `--cpu` |
| 126 | - `--binary` — path to a compiled binary | ||
| 127 | - `--static` — deploy as a static site | ||
| 128 | - `--dir` — directory to deploy (default: `.`) | ||
| 129 | - `--domain` — custom domain | ||
| 130 | - `--name` — app name (default: inferred from binary or directory) | ||
| 131 | - `--env KEY=VALUE` — environment variable (repeatable) | ||
| 132 | - `--env-file` — path to a `.env` file | ||
| 133 | - `--args` — arguments passed to the binary | ||
| 134 | - `--file` — config file to upload to working directory (repeatable) | ||
| 135 | - `--memory` — memory limit (e.g., `512M`, `1G`) | ||
| 136 | - `--cpu` — CPU limit (e.g., `50%`, `200%` for 2 cores) | ||
| 137 | 104 | ||
| 138 | ### `ship list` | 105 | ### `ship list` |
| 139 | 106 | ||
| 140 | List all deployments on the default host. | 107 | List all deployments on the default host. |
| 141 | 108 | ||
| 142 | ``` | 109 | ### `ship status/logs/restart/remove <name>` |
| 143 | NAME TYPE VISIBILITY DOMAIN PORT | ||
| 144 | myapp git-app private myapp.example.com :8001 | ||
| 145 | mysite git-static public mysite.example.com | ||
| 146 | api app api.example.com :8002 | ||
| 147 | ``` | ||
| 148 | |||
| 149 | ### `ship status <name>` | ||
| 150 | |||
| 151 | Show systemd service status for an app. | ||
| 152 | |||
| 153 | ### `ship logs <name>` | ||
| 154 | |||
| 155 | Show service logs (via journalctl). | ||
| 156 | |||
| 157 | ### `ship restart <name>` | ||
| 158 | |||
| 159 | Restart an app's systemd service. | ||
| 160 | |||
| 161 | ### `ship remove <name>` | ||
| 162 | 110 | ||
| 163 | Remove a deployment. Stops the service, removes files, configs, and state. | 111 | Manage a deployment's systemd service. |
| 164 | 112 | ||
| 165 | ### `ship env` | 113 | ### `ship env` |
| 166 | 114 | ||
| 167 | Manage environment variables for an app. | ||
| 168 | |||
| 169 | ``` | 115 | ``` |
| 170 | ship env list myapp # show env vars (secrets masked) | 116 | ship env list myapp |
| 171 | ship env set myapp KEY=VALUE # set variable(s) | 117 | ship env set myapp KEY=VALUE |
| 172 | ship env set myapp -f .env # load from file | 118 | ship env unset myapp KEY |
| 173 | ship env unset myapp KEY # remove a variable | ||
| 174 | ``` | 119 | ``` |
| 175 | 120 | ||
| 176 | ### `ship host` | 121 | ### `ship host` |
| 177 | 122 | ||
| 178 | Manage the VPS. | ||
| 179 | |||
| 180 | ``` | 123 | ``` |
| 181 | ship host init --host user@vps --base-domain example.com # one-time setup | 124 | ship host init --host user@vps --base-domain example.com |
| 182 | ship host status # uptime, disk, memory, load | 125 | ship host status |
| 183 | ship host update # apt update && upgrade | 126 | ship host update |
| 184 | ship host ssh # open an SSH session | 127 | ship host ssh |
| 185 | ship host set-domain example.com # change base domain | ||
| 186 | ``` | 128 | ``` |
| 187 | 129 | ||
| 188 | ### `ship ui` | 130 | ### `ship ui` |
| 189 | 131 | ||
| 190 | Launch a local web UI for viewing deployments. | 132 | Launch a local web UI for viewing deployments. |
| 191 | 133 | ||
| 192 | ``` | ||
| 193 | ship ui # http://localhost:8080 | ||
| 194 | ship ui -p 3000 # custom port | ||
| 195 | ``` | ||
| 196 | |||
| 197 | ### `ship version` | ||
| 198 | |||
| 199 | Show version, commit, and build date. | ||
| 200 | |||
| 201 | ## How it works | ||
| 202 | |||
| 203 | ### Architecture | ||
| 204 | |||
| 205 | Ship is a client-side CLI. All state lives on your laptop at `~/.config/ship/state.json`. The VPS is configured entirely over SSH — no agent or daemon runs on the server. This means the VPS is stateless and easily recreatable from local state. | ||
| 206 | |||
| 207 | ### Git push flow | ||
| 208 | |||
| 209 | 1. `ship init` creates a bare repo at `/srv/git/<name>.git` with a post-receive hook | ||
| 210 | 2. `git push` triggers the hook, which: | ||
| 211 | - Checks out code to `/var/lib/<name>/src` | ||
| 212 | - Copies `.ship/service` to `/etc/systemd/system/<name>.service` | ||
| 213 | - Copies `.ship/Caddyfile` to `/etc/caddy/sites-enabled/<name>.caddy` | ||
| 214 | - Runs `docker build` (skipped if no Dockerfile) | ||
| 215 | - Restarts the systemd service | ||
| 216 | 3. The Docker container runs with: | ||
| 217 | - Port bound to `127.0.0.1:<port>` | ||
| 218 | - Env vars from `/etc/ship/env/<name>.env` | ||
| 219 | - Persistent data volume at `/var/lib/<name>/data` (mounted as `/data`) | ||
| 220 | 4. Caddy reverse-proxies HTTPS traffic to the container | ||
| 221 | |||
| 222 | ### Direct deploy flow | ||
| 223 | |||
| 224 | 1. Binary uploaded via SCP to `/usr/local/bin/<name>` | ||
| 225 | 2. A dedicated system user is created | ||
| 226 | 3. Ship generates and installs a systemd unit and Caddy config | ||
| 227 | 4. Service is started, Caddy is reloaded | ||
| 228 | |||
| 229 | ### Vanity imports and git cloning | ||
| 230 | |||
| 231 | When a base domain is configured, Ship sets up the base domain to serve: | ||
| 232 | |||
| 233 | - **Go vanity imports** — `go get example.com/myapp` returns the correct `<meta go-import>` tag pointing to `https://example.com/myapp.git` | ||
| 234 | - **Git HTTPS cloning** — `git clone https://example.com/myapp.git` works for public repos (those created with `--public`) | ||
| 235 | |||
| 236 | This is handled by Caddy's `templates` directive and `fcgiwrap` + `git-http-backend`, with no custom server. | ||
| 237 | |||
| 238 | ### Port allocation | ||
| 239 | |||
| 240 | Ports are allocated automatically starting from 8001 and never reused. You don't need to track which ports are in use. | ||
| 241 | |||
| 242 | ## VPS file layout | 134 | ## VPS file layout |
| 243 | 135 | ||
| 244 | ``` | 136 | ``` |
| 245 | /srv/git/<name>.git/ # bare git repos | 137 | /srv/git/<name>.git/ # bare git repos |
| 246 | /srv/git/<name>.git/hooks/post-receive # auto-deploy hook | 138 | /srv/git/<name>.git/hooks/post-receive # auto-deploy hook |
| 247 | |||
| 248 | /var/lib/<name>/src/ # checked-out source (for docker build) | 139 | /var/lib/<name>/src/ # checked-out source (for docker build) |
| 249 | /var/lib/<name>/data/ # persistent data volume | 140 | /var/lib/<name>/data/ # persistent data volume |
| 250 | /etc/systemd/system/<name>.service # systemd unit | ||
| 251 | /etc/caddy/sites-enabled/<name>.caddy # Caddy config | ||
| 252 | /etc/ship/env/<name>.env # environment variables | ||
| 253 | |||
| 254 | /var/www/<name>/ # static site files | 141 | /var/www/<name>/ # static site files |
| 255 | 142 | /etc/systemd/system/<name>.service # systemd unit | |
| 256 | /usr/local/bin/<name> # direct-deployed binaries | 143 | /etc/caddy/sites-enabled/<name>.caddy # per-app Caddy config |
| 257 | |||
| 258 | /opt/ship/vanity/index.html # vanity import template | ||
| 259 | /etc/caddy/sites-enabled/ship-code.caddy # base domain Caddy config | 144 | /etc/caddy/sites-enabled/ship-code.caddy # base domain Caddy config |
| 145 | /etc/cgitrc # cgit configuration | ||
| 146 | /etc/ship/env/<name>.env # environment variables | ||
| 260 | /etc/sudoers.d/ship-git # sudo rules for git user | 147 | /etc/sudoers.d/ship-git # sudo rules for git user |
| 148 | /opt/ship/vanity/index.html # vanity import template | ||
| 261 | /home/git/.ssh/authorized_keys # SSH keys for git push | 149 | /home/git/.ssh/authorized_keys # SSH keys for git push |
| 262 | ``` | 150 | ``` |
| 263 | 151 | ||
| 264 | ## App requirements | ||
| 265 | |||
| 266 | For direct-deployed Go apps, the binary must: | ||
| 267 | |||
| 268 | 1. Listen on HTTP (Caddy handles HTTPS) | ||
| 269 | 2. Read the port from the `PORT` environment variable or a `--port` flag | ||
| 270 | 3. Bind to `127.0.0.1` (not `0.0.0.0`) | ||
| 271 | |||
| 272 | For git-deployed Docker apps, the Dockerfile should expose a service that listens on the port specified by the `PORT` environment variable. | ||
| 273 | |||
| 274 | ## Supported platforms | 152 | ## Supported platforms |
| 275 | 153 | ||
| 276 | VPS: Ubuntu 20.04+ or Debian 11+ | 154 | VPS: Ubuntu 20.04+ or Debian 11+ |
