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 --- cmd/ship/host/init.go | 131 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) (limited to 'cmd/ship/host/init.go') diff --git a/cmd/ship/host/init.go b/cmd/ship/host/init.go index 27f67af..d7143ff 100644 --- a/cmd/ship/host/init.go +++ b/cmd/ship/host/init.go @@ -6,6 +6,7 @@ import ( "github.com/bdw/ship/internal/ssh" "github.com/bdw/ship/internal/state" + "github.com/bdw/ship/internal/templates" "github.com/spf13/cobra" ) @@ -105,6 +106,14 @@ import /etc/caddy/sites-enabled/* hostState.BaseDomain = baseDomain fmt.Printf(" Base domain: %s\n", baseDomain) } + + // Git-centric deployment setup (gated on base domain) + if baseDomain != "" { + if err := setupGitDeploy(client, baseDomain, hostState); err != nil { + return err + } + } + if st.GetDefaultHost() == "" { st.SetDefaultHost(host) fmt.Printf(" Set %s as default host\n", host) @@ -119,6 +128,128 @@ import /etc/caddy/sites-enabled/* fmt.Printf(" ship --binary ./myapp --domain api.example.com\n") fmt.Println(" 2. Deploy a static site:") fmt.Printf(" ship --static --dir ./dist --domain example.com\n") + if baseDomain != "" { + fmt.Println(" 3. Initialize a git-deployed app:") + fmt.Printf(" ship init myapp\n") + } + return nil +} + +func setupGitDeploy(client *ssh.Client, baseDomain string, hostState *state.Host) error { + fmt.Println("-> Installing Docker...") + dockerCommands := []string{ + "apt-get install -y ca-certificates curl gnupg", + "install -m 0755 -d /etc/apt/keyrings", + "curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc", + "chmod a+r /etc/apt/keyrings/docker.asc", + `echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo ${VERSION_CODENAME}) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null`, + "apt-get update", + "apt-get install -y docker-ce docker-ce-cli containerd.io", + } + for _, cmd := range dockerCommands { + if _, err := client.RunSudo(cmd); err != nil { + return fmt.Errorf("error installing Docker: %w", err) + } + } + fmt.Println(" Docker installed") + + fmt.Println("-> Installing git and fcgiwrap...") + if _, err := client.RunSudo("apt-get install -y git fcgiwrap"); err != nil { + return fmt.Errorf("error installing git/fcgiwrap: %w", err) + } + fmt.Println(" git and fcgiwrap installed") + + fmt.Println("-> Creating git user...") + // Create git user (ignore error if already exists) + client.RunSudo("useradd -r -m -d /home/git -s $(which git-shell) git") + if _, err := client.RunSudo("usermod -aG docker git"); err != nil { + return fmt.Errorf("error adding git user to docker group: %w", err) + } + fmt.Println(" git user created") + + fmt.Println("-> Copying SSH keys to git user...") + copyKeysCommands := []string{ + "mkdir -p /home/git/.ssh", + "cp ~/.ssh/authorized_keys /home/git/.ssh/authorized_keys", + "chown -R git:git /home/git/.ssh", + "chmod 700 /home/git/.ssh", + "chmod 600 /home/git/.ssh/authorized_keys", + } + for _, cmd := range copyKeysCommands { + if _, err := client.RunSudo(cmd); err != nil { + return fmt.Errorf("error copying SSH keys: %w", err) + } + } + fmt.Println(" SSH keys copied") + + fmt.Println("-> Creating /srv/git...") + if _, err := client.RunSudo("mkdir -p /srv/git"); err != nil { + return fmt.Errorf("error creating /srv/git: %w", err) + } + if _, err := client.RunSudo("chown git:git /srv/git"); err != nil { + return fmt.Errorf("error setting /srv/git ownership: %w", err) + } + fmt.Println(" /srv/git created") + + fmt.Println("-> Writing sudoers for git user...") + sudoersContent := `git ALL=(ALL) NOPASSWD: /bin/systemctl restart *, /bin/systemctl daemon-reload, /bin/systemctl reload caddy, /bin/systemctl enable *, /bin/cp * /etc/systemd/system/*, /bin/cp * /etc/caddy/sites-enabled/*, /bin/mkdir -p /var/lib/*, /bin/mkdir -p /var/www/*, /bin/chown * +` + if err := client.WriteSudoFile("/etc/sudoers.d/ship-git", sudoersContent); err != nil { + return fmt.Errorf("error writing sudoers: %w", err) + } + if _, err := client.RunSudo("chmod 440 /etc/sudoers.d/ship-git"); err != nil { + return fmt.Errorf("error setting sudoers permissions: %w", err) + } + fmt.Println(" sudoers configured") + + fmt.Println("-> Writing vanity import template...") + vanityHTML := ` + +{{$path := trimPrefix "/" .Req.URL.Path}} +{{$parts := splitList "/" $path}} +{{$module := first $parts}} + + +go get {{.Host}}/{{$module}} + +` + if _, err := client.RunSudo("mkdir -p /opt/ship/vanity"); err != nil { + return fmt.Errorf("error creating vanity directory: %w", err) + } + if err := client.WriteSudoFile("/opt/ship/vanity/index.html", vanityHTML); err != nil { + return fmt.Errorf("error writing vanity template: %w", err) + } + fmt.Println(" vanity template written") + + fmt.Println("-> Writing base domain Caddy config...") + codeCaddyContent, err := templates.CodeCaddy(map[string]string{ + "BaseDomain": baseDomain, + }) + if err != nil { + return fmt.Errorf("error generating code caddy config: %w", err) + } + if err := client.WriteSudoFile("/etc/caddy/sites-enabled/ship-code.caddy", codeCaddyContent); err != nil { + return fmt.Errorf("error writing code caddy config: %w", err) + } + fmt.Println(" base domain Caddy config written") + + fmt.Println("-> Starting Docker and fcgiwrap...") + if _, err := client.RunSudo("systemctl enable docker fcgiwrap"); err != nil { + return fmt.Errorf("error enabling services: %w", err) + } + if _, err := client.RunSudo("systemctl start docker fcgiwrap"); err != nil { + return fmt.Errorf("error starting services: %w", err) + } + fmt.Println(" Docker and fcgiwrap started") + + fmt.Println("-> Reloading Caddy...") + if _, err := client.RunSudo("systemctl reload caddy"); err != nil { + return fmt.Errorf("error reloading Caddy: %w", err) + } + fmt.Println(" Caddy reloaded") + + hostState.GitSetup = true + fmt.Println(" Git deployment setup complete") return nil } -- cgit v1.2.3