diff options
| author | bndw <ben@bdw.to> | 2026-02-14 08:11:33 -0800 |
|---|---|---|
| committer | bndw <ben@bdw.to> | 2026-02-14 08:11:33 -0800 |
| commit | b9120489c454877ff623e65db48ec97f402bf8ed (patch) | |
| tree | 4d903a9ad1307818fa4184c9cb7d329ea2386151 | |
| parent | f0dfabe5b7f1f8d23169c6e62a2f0c27bd6c5463 (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.md | 6 | ||||
| -rw-r--r-- | cmd/ship/deploy.go | 124 |
2 files changed, 94 insertions, 36 deletions
| @@ -70,6 +70,12 @@ GOOS=linux GOARCH=amd64 go build -o myapp | |||
| 70 | ship --binary ./myapp --domain api.example.com | 70 | ship --binary ./myapp --domain api.example.com |
| 71 | ``` | 71 | ``` |
| 72 | 72 | ||
| 73 | On 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 | |||
| 77 | 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. | ||
| 78 | |||
| 73 | You can version control `.ship/` or add it to `.gitignore` — it's your choice. | 79 | You 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...") |
