summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-02-14 08:11:33 -0800
committerbndw <ben@bdw.to>2026-02-14 08:11:33 -0800
commitb9120489c454877ff623e65db48ec97f402bf8ed (patch)
tree4d903a9ad1307818fa4184c9cb7d329ea2386151
parentf0dfabe5b7f1f8d23169c6e62a2f0c27bd6c5463 (diff)
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.
-rw-r--r--README.md6
-rw-r--r--cmd/ship/deploy.go124
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
70ship --binary ./myapp --domain api.example.com 70ship --binary ./myapp --domain api.example.com
71``` 71```
72 72
73On first deployment, Ship creates a `.ship/` directory in your current working directory containing:
74- `.ship/service` - systemd unit file
75- `.ship/Caddyfile` - Caddy reverse proxy config
76
77These 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.
78
73You can version control `.ship/` or add it to `.gitignore` — it's your choice. 79You can version control `.ship/` or add it to `.gitignore` — it's your choice.
74 80
75## Commands 81## 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 {
337 return fmt.Errorf("error setting env file ownership: %w", err) 337 return fmt.Errorf("error setting env file ownership: %w", err)
338 } 338 }
339 339
340 fmt.Println("-> Creating systemd service...") 340 // Create local .ship directory for deployment configs
341 serviceContent, err := templates.SystemdService(map[string]string{ 341 if !opts.IsUpdate {
342 "Name": opts.Name, 342 fmt.Println("-> Creating local .ship directory...")
343 "User": opts.Name, 343 if err := os.MkdirAll(".ship", 0755); err != nil {
344 "WorkDir": workDir, 344 return fmt.Errorf("error creating .ship directory: %w", err)
345 "BinaryPath": binaryDest, 345 }
346 "Port": strconv.Itoa(port), 346
347 "EnvFile": envFilePath, 347 fmt.Println("-> Generating systemd service...")
348 "Args": opts.Args, 348 serviceContent, err := templates.SystemdService(map[string]string{
349 "Memory": opts.Memory, 349 "Name": opts.Name,
350 "CPU": opts.CPU, 350 "User": opts.Name,
351 }) 351 "WorkDir": workDir,
352 if err != nil { 352 "BinaryPath": binaryDest,
353 return fmt.Errorf("error generating systemd unit: %w", err) 353 "Port": strconv.Itoa(port),
354 "EnvFile": envFilePath,
355 "Args": opts.Args,
356 "Memory": opts.Memory,
357 "CPU": opts.CPU,
358 })
359 if err != nil {
360 return fmt.Errorf("error generating systemd unit: %w", err)
361 }
362 if err := os.WriteFile(".ship/service", []byte(serviceContent), 0644); err != nil {
363 return fmt.Errorf("error writing .ship/service: %w", err)
364 }
365
366 fmt.Println("-> Generating Caddyfile...")
367 caddyContent, err := templates.AppCaddy(map[string]string{
368 "Domain": opts.Domain,
369 "Port": strconv.Itoa(port),
370 })
371 if err != nil {
372 return fmt.Errorf("error generating Caddy config: %w", err)
373 }
374 if err := os.WriteFile(".ship/Caddyfile", []byte(caddyContent), 0644); err != nil {
375 return fmt.Errorf("error writing .ship/Caddyfile: %w", err)
376 }
354 } 377 }
355 378
379 // Upload systemd service from .ship/service
380 fmt.Println("-> Installing systemd service...")
381 serviceContent, err := os.ReadFile(".ship/service")
382 if err != nil {
383 return fmt.Errorf("error reading .ship/service: %w (run initial deployment first)", err)
384 }
356 servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", opts.Name) 385 servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", opts.Name)
357 if err := client.WriteSudoFile(servicePath, serviceContent); err != nil { 386 if err := client.WriteSudoFile(servicePath, string(serviceContent)); err != nil {
358 return fmt.Errorf("error creating systemd unit: %w", err) 387 return fmt.Errorf("error installing systemd unit: %w", err)
359 } 388 }
360 389
361 fmt.Println("-> Configuring Caddy...") 390 // Upload Caddyfile from .ship/Caddyfile
362 caddyContent, err := templates.AppCaddy(map[string]string{ 391 fmt.Println("-> Installing Caddy config...")
363 "Domain": opts.Domain, 392 caddyContent, err := os.ReadFile(".ship/Caddyfile")
364 "Port": strconv.Itoa(port),
365 })
366 if err != nil { 393 if err != nil {
367 return fmt.Errorf("error generating Caddy config: %w", err) 394 return fmt.Errorf("error reading .ship/Caddyfile: %w (run initial deployment first)", err)
368 } 395 }
369
370 caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", opts.Name) 396 caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", opts.Name)
371 if err := client.WriteSudoFile(caddyPath, caddyContent); err != nil { 397 if err := client.WriteSudoFile(caddyPath, string(caddyContent)); err != nil {
372 return fmt.Errorf("error creating Caddy config: %w", err) 398 return fmt.Errorf("error installing Caddy config: %w", err)
373 } 399 }
374 400
375 fmt.Println("-> Reloading systemd...") 401 fmt.Println("-> Reloading systemd...")
@@ -447,7 +473,7 @@ func updateAppConfig(st *state.State, opts DeployOptions) error {
447 // For git-app, the systemd unit comes from .ship/service in the repo, 473 // For git-app, the systemd unit comes from .ship/service in the repo,
448 // so we only update the env file and restart. 474 // so we only update the env file and restart.
449 if existingApp.Type != "git-app" { 475 if existingApp.Type != "git-app" {
450 // Regenerate systemd unit 476 // Regenerate systemd unit to .ship/service (resource flags are being updated)
451 fmt.Println("-> Updating systemd service...") 477 fmt.Println("-> Updating systemd service...")
452 workDir := fmt.Sprintf("/var/lib/%s", opts.Name) 478 workDir := fmt.Sprintf("/var/lib/%s", opts.Name)
453 binaryDest := fmt.Sprintf("/usr/local/bin/%s", opts.Name) 479 binaryDest := fmt.Sprintf("/usr/local/bin/%s", opts.Name)
@@ -466,9 +492,18 @@ func updateAppConfig(st *state.State, opts DeployOptions) error {
466 return fmt.Errorf("error generating systemd unit: %w", err) 492 return fmt.Errorf("error generating systemd unit: %w", err)
467 } 493 }
468 494
495 // Write to local .ship/service
496 if err := os.MkdirAll(".ship", 0755); err != nil {
497 return fmt.Errorf("error creating .ship directory: %w", err)
498 }
499 if err := os.WriteFile(".ship/service", []byte(serviceContent), 0644); err != nil {
500 return fmt.Errorf("error writing .ship/service: %w", err)
501 }
502
503 // Upload to server
469 servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", opts.Name) 504 servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", opts.Name)
470 if err := client.WriteSudoFile(servicePath, serviceContent); err != nil { 505 if err := client.WriteSudoFile(servicePath, serviceContent); err != nil {
471 return fmt.Errorf("error creating systemd unit: %w", err) 506 return fmt.Errorf("error installing systemd unit: %w", err)
472 } 507 }
473 508
474 fmt.Println("-> Reloading systemd...") 509 fmt.Println("-> Reloading systemd...")
@@ -546,18 +581,35 @@ func deployStatic(st *state.State, opts DeployOptions) error {
546 return fmt.Errorf("error setting file permissions: %w", err) 581 return fmt.Errorf("error setting file permissions: %w", err)
547 } 582 }
548 583
549 fmt.Println("-> Configuring Caddy...") 584 // Create local .ship directory and Caddyfile for static sites
550 caddyContent, err := templates.StaticCaddy(map[string]string{ 585 if !opts.IsUpdate {
551 "Domain": opts.Domain, 586 fmt.Println("-> Creating local .ship directory...")
552 "RootDir": remoteDir, 587 if err := os.MkdirAll(".ship", 0755); err != nil {
553 }) 588 return fmt.Errorf("error creating .ship directory: %w", err)
554 if err != nil { 589 }
555 return fmt.Errorf("error generating Caddy config: %w", err) 590
591 fmt.Println("-> Generating Caddyfile...")
592 caddyContent, err := templates.StaticCaddy(map[string]string{
593 "Domain": opts.Domain,
594 "RootDir": remoteDir,
595 })
596 if err != nil {
597 return fmt.Errorf("error generating Caddy config: %w", err)
598 }
599 if err := os.WriteFile(".ship/Caddyfile", []byte(caddyContent), 0644); err != nil {
600 return fmt.Errorf("error writing .ship/Caddyfile: %w", err)
601 }
556 } 602 }
557 603
604 // Upload Caddyfile from .ship/Caddyfile
605 fmt.Println("-> Installing Caddy config...")
606 caddyContent, err := os.ReadFile(".ship/Caddyfile")
607 if err != nil {
608 return fmt.Errorf("error reading .ship/Caddyfile: %w (run initial deployment first)", err)
609 }
558 caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", opts.Name) 610 caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", opts.Name)
559 if err := client.WriteSudoFile(caddyPath, caddyContent); err != nil { 611 if err := client.WriteSudoFile(caddyPath, string(caddyContent)); err != nil {
560 return fmt.Errorf("error creating Caddy config: %w", err) 612 return fmt.Errorf("error installing Caddy config: %w", err)
561 } 613 }
562 614
563 fmt.Println("-> Reloading Caddy...") 615 fmt.Println("-> Reloading Caddy...")