From b9120489c454877ff623e65db48ec97f402bf8ed Mon Sep 17 00:00:00 2001 From: bndw Date: Sat, 14 Feb 2026 08:11:33 -0800 Subject: Store deployment configs locally for binary deployments For binary and static deployments, .ship/service and .ship/Caddyfile are now written to the local working directory (similar to git deployments) instead of being regenerated on every deployment. - On initial deployment, create local .ship/ directory with generated configs - On subsequent deployments, upload from local .ship/ files - Caddyfile is never regenerated, preserving custom routes - Systemd service is regenerated only when --memory, --cpu, or --args change This prevents custom Caddyfile routes from being overwritten and makes binary deployment workflow consistent with git deployment workflow. --- README.md | 6 +++ cmd/ship/deploy.go | 124 +++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 94 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index cdd4127..c440c49 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,12 @@ 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 diff --git a/cmd/ship/deploy.go b/cmd/ship/deploy.go index 86d4878..6894e21 100644 --- a/cmd/ship/deploy.go +++ b/cmd/ship/deploy.go @@ -337,39 +337,65 @@ func deployApp(st *state.State, opts DeployOptions) error { return fmt.Errorf("error setting env file ownership: %w", err) } - fmt.Println("-> Creating systemd service...") - serviceContent, err := templates.SystemdService(map[string]string{ - "Name": opts.Name, - "User": opts.Name, - "WorkDir": workDir, - "BinaryPath": binaryDest, - "Port": strconv.Itoa(port), - "EnvFile": envFilePath, - "Args": opts.Args, - "Memory": opts.Memory, - "CPU": opts.CPU, - }) - if err != nil { - return fmt.Errorf("error generating systemd unit: %w", err) + // Create local .ship directory for deployment configs + if !opts.IsUpdate { + fmt.Println("-> Creating local .ship directory...") + if err := os.MkdirAll(".ship", 0755); err != nil { + return fmt.Errorf("error creating .ship directory: %w", err) + } + + fmt.Println("-> Generating systemd service...") + serviceContent, err := templates.SystemdService(map[string]string{ + "Name": opts.Name, + "User": opts.Name, + "WorkDir": workDir, + "BinaryPath": binaryDest, + "Port": strconv.Itoa(port), + "EnvFile": envFilePath, + "Args": opts.Args, + "Memory": opts.Memory, + "CPU": opts.CPU, + }) + if err != nil { + return fmt.Errorf("error generating systemd unit: %w", err) + } + if err := os.WriteFile(".ship/service", []byte(serviceContent), 0644); err != nil { + return fmt.Errorf("error writing .ship/service: %w", err) + } + + fmt.Println("-> Generating Caddyfile...") + caddyContent, err := templates.AppCaddy(map[string]string{ + "Domain": opts.Domain, + "Port": strconv.Itoa(port), + }) + if err != nil { + return fmt.Errorf("error generating Caddy config: %w", err) + } + if err := os.WriteFile(".ship/Caddyfile", []byte(caddyContent), 0644); err != nil { + return fmt.Errorf("error writing .ship/Caddyfile: %w", err) + } } + // Upload systemd service from .ship/service + fmt.Println("-> Installing systemd service...") + serviceContent, err := os.ReadFile(".ship/service") + if err != nil { + return fmt.Errorf("error reading .ship/service: %w (run initial deployment first)", err) + } servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", opts.Name) - if err := client.WriteSudoFile(servicePath, serviceContent); err != nil { - return fmt.Errorf("error creating systemd unit: %w", err) + if err := client.WriteSudoFile(servicePath, string(serviceContent)); err != nil { + return fmt.Errorf("error installing systemd unit: %w", err) } - fmt.Println("-> Configuring Caddy...") - caddyContent, err := templates.AppCaddy(map[string]string{ - "Domain": opts.Domain, - "Port": strconv.Itoa(port), - }) + // Upload Caddyfile from .ship/Caddyfile + fmt.Println("-> Installing Caddy config...") + caddyContent, err := os.ReadFile(".ship/Caddyfile") if err != nil { - return fmt.Errorf("error generating Caddy config: %w", err) + return fmt.Errorf("error reading .ship/Caddyfile: %w (run initial deployment first)", err) } - caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", opts.Name) - if err := client.WriteSudoFile(caddyPath, caddyContent); err != nil { - return fmt.Errorf("error creating Caddy config: %w", err) + if err := client.WriteSudoFile(caddyPath, string(caddyContent)); err != nil { + return fmt.Errorf("error installing Caddy config: %w", err) } fmt.Println("-> Reloading systemd...") @@ -447,7 +473,7 @@ func updateAppConfig(st *state.State, opts DeployOptions) error { // For git-app, the systemd unit comes from .ship/service in the repo, // so we only update the env file and restart. if existingApp.Type != "git-app" { - // Regenerate systemd unit + // Regenerate systemd unit to .ship/service (resource flags are being updated) fmt.Println("-> Updating systemd service...") workDir := fmt.Sprintf("/var/lib/%s", opts.Name) binaryDest := fmt.Sprintf("/usr/local/bin/%s", opts.Name) @@ -466,9 +492,18 @@ func updateAppConfig(st *state.State, opts DeployOptions) error { return fmt.Errorf("error generating systemd unit: %w", err) } + // Write to local .ship/service + if err := os.MkdirAll(".ship", 0755); err != nil { + return fmt.Errorf("error creating .ship directory: %w", err) + } + if err := os.WriteFile(".ship/service", []byte(serviceContent), 0644); err != nil { + return fmt.Errorf("error writing .ship/service: %w", err) + } + + // Upload to server servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", opts.Name) if err := client.WriteSudoFile(servicePath, serviceContent); err != nil { - return fmt.Errorf("error creating systemd unit: %w", err) + return fmt.Errorf("error installing systemd unit: %w", err) } fmt.Println("-> Reloading systemd...") @@ -546,18 +581,35 @@ func deployStatic(st *state.State, opts DeployOptions) error { return fmt.Errorf("error setting file permissions: %w", err) } - fmt.Println("-> Configuring Caddy...") - caddyContent, err := templates.StaticCaddy(map[string]string{ - "Domain": opts.Domain, - "RootDir": remoteDir, - }) - if err != nil { - return fmt.Errorf("error generating Caddy config: %w", err) + // Create local .ship directory and Caddyfile for static sites + if !opts.IsUpdate { + fmt.Println("-> Creating local .ship directory...") + if err := os.MkdirAll(".ship", 0755); err != nil { + return fmt.Errorf("error creating .ship directory: %w", err) + } + + fmt.Println("-> Generating Caddyfile...") + caddyContent, err := templates.StaticCaddy(map[string]string{ + "Domain": opts.Domain, + "RootDir": remoteDir, + }) + if err != nil { + return fmt.Errorf("error generating Caddy config: %w", err) + } + if err := os.WriteFile(".ship/Caddyfile", []byte(caddyContent), 0644); err != nil { + return fmt.Errorf("error writing .ship/Caddyfile: %w", err) + } } + // Upload Caddyfile from .ship/Caddyfile + fmt.Println("-> Installing Caddy config...") + caddyContent, err := os.ReadFile(".ship/Caddyfile") + if err != nil { + return fmt.Errorf("error reading .ship/Caddyfile: %w (run initial deployment first)", err) + } caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", opts.Name) - if err := client.WriteSudoFile(caddyPath, caddyContent); err != nil { - return fmt.Errorf("error creating Caddy config: %w", err) + if err := client.WriteSudoFile(caddyPath, string(caddyContent)); err != nil { + return fmt.Errorf("error installing Caddy config: %w", err) } fmt.Println("-> Reloading Caddy...") -- cgit v1.2.3