From a8ad8e934d15d2bf84f942414a89af1d2691adbc Mon Sep 17 00:00:00 2001 From: bndw Date: Sun, 8 Feb 2026 12:32:59 -0800 Subject: 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 ` command to create bare repos and .ship/ config - Add `ship deploy ` 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 --- internal/state/state.go | 8 +- internal/templates/templates.go | 168 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+), 3 deletions(-) (limited to 'internal') 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 { 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"` } // App represents a deployed application or static site type App struct { - Type string `json:"type"` // "app" or "static" + Type string `json:"type"` // "app", "static", "git-app", or "git-static" Domain string `json:"domain"` - Port int `json:"port,omitempty"` // only for type="app" - Env map[string]string `json:"env,omitempty"` // only for type="app" + Port int `json:"port,omitempty"` // only for type="app" or "git-app" + Repo string `json:"repo,omitempty"` // only for git types, e.g. "/srv/git/foo.git" + Env map[string]string `json:"env,omitempty"` // only for type="app" or "git-app" Args string `json:"args,omitempty"` // only for type="app" Files []string `json:"files,omitempty"` // only for type="app" 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) { return buf.String(), nil } + +var postReceiveHookTemplate = `#!/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!" +` + +var postReceiveHookStaticTemplate = `#!/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!" +` + +var codeCaddyTemplate = `{{.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 + } +} +` + +var dockerServiceTemplate = `[Unit] +Description={{.Name}} +After=network.target docker.service +Requires=docker.service + +[Service] +Type=simple +ExecStartPre=-/usr/bin/docker rm -f {{.Name}} +ExecStart=/usr/bin/docker run --rm --name {{.Name}} \ + -p 127.0.0.1:{{.Port}}:{{.Port}} \ + --env-file /etc/ship/env/{{.Name}}.env \ + -v /var/lib/{{.Name}}/data:/data \ + {{.Name}}:latest +ExecStop=/usr/bin/docker stop -t 10 {{.Name}} +Restart=always +RestartSec=5s + +[Install] +WantedBy=multi-user.target +` + +var defaultAppCaddyTemplate = `{{.Domain}} { + reverse_proxy 127.0.0.1:{{.Port}} +} +` + +var defaultStaticCaddyTemplate = `{{.Domain}} { + root * /var/www/{{.Name}} + file_server + encode gzip +} +` + +// PostReceiveHook generates a post-receive hook for git-app repos +func PostReceiveHook(data map[string]string) (string, error) { + return renderTemplate("post-receive", postReceiveHookTemplate, data) +} + +// PostReceiveHookStatic generates a post-receive hook for git-static repos +func PostReceiveHookStatic(data map[string]string) (string, error) { + return renderTemplate("post-receive-static", postReceiveHookStaticTemplate, data) +} + +// CodeCaddy generates the base domain Caddy config for vanity imports + git HTTP +func CodeCaddy(data map[string]string) (string, error) { + return renderTemplate("code-caddy", codeCaddyTemplate, data) +} + +// DockerService generates a systemd unit for a Docker-based app +func DockerService(data map[string]string) (string, error) { + return renderTemplate("docker-service", dockerServiceTemplate, data) +} + +// DefaultAppCaddy generates a default Caddyfile for a git-app +func DefaultAppCaddy(data map[string]string) (string, error) { + return renderTemplate("default-app-caddy", defaultAppCaddyTemplate, data) +} + +// DefaultStaticCaddy generates a default Caddyfile for a git-static site +func DefaultStaticCaddy(data map[string]string) (string, error) { + return renderTemplate("default-static-caddy", defaultStaticCaddyTemplate, data) +} + +func renderTemplate(name, tmplStr string, data map[string]string) (string, error) { + tmpl, err := template.New(name).Parse(tmplStr) + if err != nil { + return "", err + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, data); err != nil { + return "", err + } + + return buf.String(), nil +} -- cgit v1.2.3