1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
|
# Ship
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.
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.
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.
Ship also supports direct deploys (SCP a binary or rsync a static directory) for cases where git push isn't the right fit.
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.
## Install
```
go install github.com/bdw/ship/cmd/ship@latest
```
Or build from source:
```
go build -o ship ./cmd/ship
```
## Quick start
### 1. Set up the VPS
```
ship host init --host user@your-vps --base-domain example.com
```
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.
### 2. Deploy
**Git push (Docker-based app):**
```
ship init myapp
git add .ship/ Dockerfile
git commit -m "initial deploy"
git push origin main
```
**Git push (static site):**
```
ship init mysite --static
git add .ship/ index.html
git commit -m "initial deploy"
git push origin main
```
**Git push (library / Go module):**
```
ship init mylib --public
git add .
git commit -m "initial"
git push origin main
```
No Dockerfile, so nothing is deployed — the repo is just browsable and cloneable at `https://example.com/mylib`.
**Direct (pre-built binary):**
```
GOOS=linux GOARCH=amd64 go build -o myapp
ship --binary ./myapp --domain api.example.com
```
On first deployment, Ship creates a `.ship/` directory in your current working directory containing:
- `.ship/service` - systemd unit file
- `.ship/Caddyfile` - Caddy reverse proxy config
These files are uploaded on each deployment. You can edit them locally to customize your deployment (add extra Caddy routes, adjust systemd settings). The systemd service is regenerated when you update resource limits with `--memory`, `--cpu`, or `--args` flags. The Caddyfile is never regenerated, so your custom routes won't be overwritten.
You can version control `.ship/` or add it to `.gitignore` — it's your choice.
## Commands
### `ship init <name>`
Create a bare git repo on the VPS and generate local `.ship/` config files.
```
ship init myapp # Docker-based app
ship init mysite --static # static site
ship init myapp --domain custom.example.com # custom domain
ship init mylib --public # publicly cloneable (for go get)
```
### `ship deploy <name>`
Manually rebuild and deploy a git-deployed app.
### `ship [deploy flags]`
Deploy a pre-built binary or static directory directly.
```
ship --binary ./myapp --domain api.example.com
ship --binary ./myapp --domain api.example.com --env DB_HOST=localhost
ship --static --dir ./dist --domain example.com
ship --name myapi --memory 512M --cpu 50%
```
Flags: `--binary`, `--static`, `--dir`, `--domain`, `--name`, `--env`, `--env-file`, `--args`, `--file`, `--memory`, `--cpu`
### `ship list`
List all deployments on the default host.
### `ship status/logs/restart/remove <name>`
Manage a deployment's systemd service.
### `ship env`
```
ship env list myapp
ship env set myapp KEY=VALUE
ship env unset myapp KEY
```
### `ship host`
```
ship host init --host user@vps --base-domain example.com
ship host status
ship host update
ship host ssh
```
### `ship ui`
Launch a local web UI for viewing deployments.
## VPS file layout
```
/srv/git/<name>.git/ # bare git repos
/srv/git/<name>.git/hooks/post-receive # auto-deploy hook
/var/lib/<name>/src/ # checked-out source (for docker build)
/var/lib/<name>/data/ # persistent data volume
/var/www/<name>/ # static site files
/etc/systemd/system/<name>.service # systemd unit
/etc/caddy/sites-enabled/<name>.caddy # per-app Caddy config
/etc/caddy/sites-enabled/ship-code.caddy # base domain Caddy config
/etc/cgitrc # cgit configuration
/etc/ship/env/<name>.env # environment variables
/etc/sudoers.d/ship-git # sudo rules for git user
/opt/ship/vanity/index.html # vanity import template
/home/git/.ssh/authorized_keys # SSH keys for git push
```
## Supported platforms
VPS: Ubuntu 20.04+ or Debian 11+
## Security
See [SECURITY.md](SECURITY.md) for the threat model, mitigations, and known gaps.
## License
MIT
|