summaryrefslogtreecommitdiffstats
path: root/internal
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-02-08 12:32:59 -0800
committerbndw <ben@bdw.to>2026-02-08 12:32:59 -0800
commita8ad8e934d15d2bf84f942414a89af1d2691adbc (patch)
tree82e6765c9d35968b27ac7ee17f5c201a421dc1d3 /internal
parentaf109c04a3edd4dcd4e7b16242052442fb4a3b24 (diff)
Add git-centric deployment with Docker builds and vanity imports
New deployment model where projects start with a git remote on the VPS. Pushing to the remote triggers automatic docker build and deploy via post-receive hooks. The base domain serves Go vanity imports and git HTTPS cloning via Caddy + fcgiwrap. - Add `ship init <name>` command to create bare repos and .ship/ config - Add `ship deploy <name>` command for manual rebuilds - Extend `ship host init --base-domain` to set up Docker, git user, fcgiwrap, sudoers, and vanity import infrastructure - Add git-app and git-static types alongside existing app and static - Update remove, status, logs, restart, list, and config-update to handle new types
Diffstat (limited to 'internal')
-rw-r--r--internal/state/state.go8
-rw-r--r--internal/templates/templates.go168
2 files changed, 173 insertions, 3 deletions
diff --git a/internal/state/state.go b/internal/state/state.go
index 38657cb..324fd34 100644
--- a/internal/state/state.go
+++ b/internal/state/state.go
@@ -17,15 +17,17 @@ type State struct {
17type Host struct { 17type Host struct {
18 NextPort int `json:"next_port"` 18 NextPort int `json:"next_port"`
19 BaseDomain string `json:"base_domain,omitempty"` 19 BaseDomain string `json:"base_domain,omitempty"`
20 GitSetup bool `json:"git_setup,omitempty"`
20 Apps map[string]*App `json:"apps"` 21 Apps map[string]*App `json:"apps"`
21} 22}
22 23
23// App represents a deployed application or static site 24// App represents a deployed application or static site
24type App struct { 25type App struct {
25 Type string `json:"type"` // "app" or "static" 26 Type string `json:"type"` // "app", "static", "git-app", or "git-static"
26 Domain string `json:"domain"` 27 Domain string `json:"domain"`
27 Port int `json:"port,omitempty"` // only for type="app" 28 Port int `json:"port,omitempty"` // only for type="app" or "git-app"
28 Env map[string]string `json:"env,omitempty"` // only for type="app" 29 Repo string `json:"repo,omitempty"` // only for git types, e.g. "/srv/git/foo.git"
30 Env map[string]string `json:"env,omitempty"` // only for type="app" or "git-app"
29 Args string `json:"args,omitempty"` // only for type="app" 31 Args string `json:"args,omitempty"` // only for type="app"
30 Files []string `json:"files,omitempty"` // only for type="app" 32 Files []string `json:"files,omitempty"` // only for type="app"
31 Memory string `json:"memory,omitempty"` // only for type="app" 33 Memory string `json:"memory,omitempty"` // only for type="app"
diff --git a/internal/templates/templates.go b/internal/templates/templates.go
index ce1cbe5..8615117 100644
--- a/internal/templates/templates.go
+++ b/internal/templates/templates.go
@@ -86,3 +86,171 @@ func StaticCaddy(data map[string]string) (string, error) {
86 86
87 return buf.String(), nil 87 return buf.String(), nil
88} 88}
89
90var postReceiveHookTemplate = `#!/bin/bash
91set -euo pipefail
92
93REPO=/srv/git/{{.Name}}.git
94SRC=/var/lib/{{.Name}}/src
95NAME={{.Name}}
96
97while read oldrev newrev refname; do
98 branch=$(git rev-parse --symbolic --abbrev-ref "$refname")
99 [ "$branch" = "main" ] || { echo "Pushed to $branch, skipping deploy."; exit 0; }
100done
101
102echo "==> Checking out code..."
103git --work-tree="$SRC" --git-dir="$REPO" checkout -f main
104
105cd "$SRC"
106
107# Install deployment config from repo
108if [ -f .ship/service ]; then
109 echo "==> Installing systemd unit..."
110 sudo cp .ship/service /etc/systemd/system/${NAME}.service
111 sudo systemctl daemon-reload
112fi
113if [ -f .ship/Caddyfile ]; then
114 echo "==> Installing Caddy config..."
115 sudo cp .ship/Caddyfile /etc/caddy/sites-enabled/${NAME}.caddy
116 sudo systemctl reload caddy
117fi
118
119echo "==> Building Docker image..."
120docker build -t ${NAME}:latest .
121
122echo "==> Restarting service..."
123sudo systemctl restart ${NAME}
124
125echo "==> Deploy complete!"
126`
127
128var postReceiveHookStaticTemplate = `#!/bin/bash
129set -euo pipefail
130
131REPO=/srv/git/{{.Name}}.git
132WEBROOT=/var/www/{{.Name}}
133NAME={{.Name}}
134
135while read oldrev newrev refname; do
136 branch=$(git rev-parse --symbolic --abbrev-ref "$refname")
137 [ "$branch" = "main" ] || { echo "Pushed to $branch, skipping deploy."; exit 0; }
138done
139
140echo "==> Deploying static site..."
141git --work-tree="$WEBROOT" --git-dir="$REPO" checkout -f main
142
143if [ -f "$WEBROOT/.ship/Caddyfile" ]; then
144 echo "==> Installing Caddy config..."
145 sudo cp "$WEBROOT/.ship/Caddyfile" /etc/caddy/sites-enabled/${NAME}.caddy
146 sudo systemctl reload caddy
147fi
148
149echo "==> Deploy complete!"
150`
151
152var codeCaddyTemplate = `{{.BaseDomain}} {
153 @goget query go-get=1
154 handle @goget {
155 root * /opt/ship/vanity
156 templates
157 rewrite * /index.html
158 file_server
159 }
160
161 @git path_regexp "^.*/(HEAD|info/refs|objects/info/[^/]+|git-upload-pack|git-receive-pack)$"
162 handle @git {
163 reverse_proxy unix//run/fcgiwrap.socket {
164 transport fastcgi {
165 env SCRIPT_FILENAME /usr/lib/git-core/git-http-backend
166 env GIT_PROJECT_ROOT /srv/git
167 env GIT_HTTP_EXPORT_ALL 1
168 env REQUEST_METHOD {method}
169 env QUERY_STRING {query}
170 env PATH_INFO {path}
171 }
172 }
173 }
174
175 handle {
176 respond "not found" 404
177 }
178}
179`
180
181var dockerServiceTemplate = `[Unit]
182Description={{.Name}}
183After=network.target docker.service
184Requires=docker.service
185
186[Service]
187Type=simple
188ExecStartPre=-/usr/bin/docker rm -f {{.Name}}
189ExecStart=/usr/bin/docker run --rm --name {{.Name}} \
190 -p 127.0.0.1:{{.Port}}:{{.Port}} \
191 --env-file /etc/ship/env/{{.Name}}.env \
192 -v /var/lib/{{.Name}}/data:/data \
193 {{.Name}}:latest
194ExecStop=/usr/bin/docker stop -t 10 {{.Name}}
195Restart=always
196RestartSec=5s
197
198[Install]
199WantedBy=multi-user.target
200`
201
202var defaultAppCaddyTemplate = `{{.Domain}} {
203 reverse_proxy 127.0.0.1:{{.Port}}
204}
205`
206
207var defaultStaticCaddyTemplate = `{{.Domain}} {
208 root * /var/www/{{.Name}}
209 file_server
210 encode gzip
211}
212`
213
214// PostReceiveHook generates a post-receive hook for git-app repos
215func PostReceiveHook(data map[string]string) (string, error) {
216 return renderTemplate("post-receive", postReceiveHookTemplate, data)
217}
218
219// PostReceiveHookStatic generates a post-receive hook for git-static repos
220func PostReceiveHookStatic(data map[string]string) (string, error) {
221 return renderTemplate("post-receive-static", postReceiveHookStaticTemplate, data)
222}
223
224// CodeCaddy generates the base domain Caddy config for vanity imports + git HTTP
225func CodeCaddy(data map[string]string) (string, error) {
226 return renderTemplate("code-caddy", codeCaddyTemplate, data)
227}
228
229// DockerService generates a systemd unit for a Docker-based app
230func DockerService(data map[string]string) (string, error) {
231 return renderTemplate("docker-service", dockerServiceTemplate, data)
232}
233
234// DefaultAppCaddy generates a default Caddyfile for a git-app
235func DefaultAppCaddy(data map[string]string) (string, error) {
236 return renderTemplate("default-app-caddy", defaultAppCaddyTemplate, data)
237}
238
239// DefaultStaticCaddy generates a default Caddyfile for a git-static site
240func DefaultStaticCaddy(data map[string]string) (string, error) {
241 return renderTemplate("default-static-caddy", defaultStaticCaddyTemplate, data)
242}
243
244func renderTemplate(name, tmplStr string, data map[string]string) (string, error) {
245 tmpl, err := template.New(name).Parse(tmplStr)
246 if err != nil {
247 return "", err
248 }
249
250 var buf bytes.Buffer
251 if err := tmpl.Execute(&buf, data); err != nil {
252 return "", err
253 }
254
255 return buf.String(), nil
256}