summaryrefslogtreecommitdiffstats
path: root/cmd
diff options
context:
space:
mode:
authorClawd <ai@clawd.bot>2026-02-17 07:59:50 -0800
committerClawd <ai@clawd.bot>2026-02-17 07:59:50 -0800
commitd97bb6f53eefd2139115d39bca7e17d565222472 (patch)
tree428f230dffad49d166bc521609dd3ee236a42010 /cmd
parent3e34265c8a6ea1ec300f987206afb25bad645677 (diff)
Fix port collision bug, add --container-port flag
Port allocation: - Use atomic flock-based allocation via /etc/ship/next_port - Prevents race conditions when multiple deploys run concurrently - Each app still gets its port stored in /etc/ship/ports/<name> Docker container port: - Add --container-port flag (default 80) - Template now uses {{.ContainerPort}} instead of hardcoded 80 - Supports containers that listen on 8080, 3000, etc.
Diffstat (limited to 'cmd')
-rw-r--r--cmd/ship/deploy_impl_v2.go45
-rw-r--r--cmd/ship/deploy_v2.go17
-rw-r--r--cmd/ship/root_v2.go2
3 files changed, 39 insertions, 25 deletions
diff --git a/cmd/ship/deploy_impl_v2.go b/cmd/ship/deploy_impl_v2.go
index e2989b2..9ff674e 100644
--- a/cmd/ship/deploy_impl_v2.go
+++ b/cmd/ship/deploy_impl_v2.go
@@ -132,9 +132,14 @@ func deployDockerV2(ctx *deployContext) *output.ErrorResponse {
132 } 132 }
133 133
134 // Generate systemd unit 134 // Generate systemd unit
135 containerPort := ctx.Opts.ContainerPort
136 if containerPort == 0 {
137 containerPort = 80
138 }
135 service, err := templates.DockerService(map[string]string{ 139 service, err := templates.DockerService(map[string]string{
136 "Name": name, 140 "Name": name,
137 "Port": strconv.Itoa(port), 141 "Port": strconv.Itoa(port),
142 "ContainerPort": strconv.Itoa(containerPort),
138 }) 143 })
139 if err != nil { 144 if err != nil {
140 return output.Err(output.ErrServiceFailed, "failed to generate systemd unit: "+err.Error()) 145 return output.Err(output.ErrServiceFailed, "failed to generate systemd unit: "+err.Error())
@@ -281,38 +286,44 @@ func deployBinaryV2(ctx *deployContext) *output.ErrorResponse {
281} 286}
282 287
283// allocatePort allocates or retrieves a port for a service 288// allocatePort allocates or retrieves a port for a service
289// Uses atomic increment on /etc/ship/next_port to avoid collisions
284func allocatePort(client *ssh.Client, name string) (int, error) { 290func allocatePort(client *ssh.Client, name string) (int, error) {
285 portFile := fmt.Sprintf("/etc/ship/ports/%s", name) 291 portFile := fmt.Sprintf("/etc/ship/ports/%s", name)
286 292
287 // Try to read existing port 293 // Try to read existing port for this app
288 out, err := client.Run(fmt.Sprintf("cat %s 2>/dev/null || echo ''", portFile)) 294 out, err := client.Run(fmt.Sprintf("cat %s 2>/dev/null || echo ''", portFile))
289 if err == nil && out != "" { 295 if err == nil && out != "" {
290 port, err := strconv.Atoi(out[:len(out)-1]) // Strip newline 296 out = strings.TrimSpace(out)
291 if err == nil && port > 0 { 297 if port, err := strconv.Atoi(out); err == nil && port > 0 {
292 return port, nil 298 return port, nil
293 } 299 }
294 } 300 }
295 301
296 // Allocate new port 302 // Allocate new port atomically using flock
297 // Find highest used port and increment 303 // This reads next_port, increments it, and writes back while holding a lock
298 out, err = client.RunSudo("mkdir -p /etc/ship/ports && ls /etc/ship/ports/ 2>/dev/null | xargs -I{} cat /etc/ship/ports/{} 2>/dev/null | sort -n | tail -1") 304 allocScript := `
305flock -x /etc/ship/.port.lock sh -c '
306 mkdir -p /etc/ship/ports
307 PORT=$(cat /etc/ship/next_port 2>/dev/null || echo 9000)
308 echo $((PORT + 1)) > /etc/ship/next_port
309 echo $PORT
310'`
311 out, err = client.RunSudo(allocScript)
299 if err != nil { 312 if err != nil {
300 out = "" 313 return 0, fmt.Errorf("failed to allocate port: %w", err)
301 } 314 }
302 315
303 nextPort := 9000 316 port, err := strconv.Atoi(strings.TrimSpace(out))
304 if out != "" { 317 if err != nil {
305 if lastPort, err := strconv.Atoi(out[:len(out)-1]); err == nil { 318 return 0, fmt.Errorf("invalid port allocated: %s", out)
306 nextPort = lastPort + 1
307 }
308 } 319 }
309 320
310 // Write port allocation 321 // Write port allocation for this app
311 if err := client.WriteSudoFile(portFile, strconv.Itoa(nextPort)); err != nil { 322 if err := client.WriteSudoFile(portFile, strconv.Itoa(port)); err != nil {
312 return 0, err 323 return 0, err
313 } 324 }
314 325
315 return nextPort, nil 326 return port, nil
316} 327}
317 328
318// setTTLV2 sets auto-expiry for a deploy 329// setTTLV2 sets auto-expiry for a deploy
diff --git a/cmd/ship/deploy_v2.go b/cmd/ship/deploy_v2.go
index b636709..7d498b2 100644
--- a/cmd/ship/deploy_v2.go
+++ b/cmd/ship/deploy_v2.go
@@ -143,14 +143,15 @@ func deployV2(path string, opts deployV2Options) {
143} 143}
144 144
145type deployV2Options struct { 145type deployV2Options struct {
146 Name string 146 Name string
147 Host string 147 Host string
148 Domain string 148 Domain string
149 Health string 149 Health string
150 TTL string 150 TTL string
151 Env []string 151 Env []string
152 EnvFile string 152 EnvFile string
153 Pretty bool 153 ContainerPort int // Port the container listens on (default 80 for Docker)
154 Pretty bool
154} 155}
155 156
156// deployContext holds all info needed for a deploy 157// deployContext holds all info needed for a deploy
diff --git a/cmd/ship/root_v2.go b/cmd/ship/root_v2.go
index 4101d4e..aa81d1e 100644
--- a/cmd/ship/root_v2.go
+++ b/cmd/ship/root_v2.go
@@ -43,6 +43,7 @@ func initV2() {
43 rootV2Cmd.Flags().String("ttl", "", "Auto-delete after duration (e.g., 1h, 7d)") 43 rootV2Cmd.Flags().String("ttl", "", "Auto-delete after duration (e.g., 1h, 7d)")
44 rootV2Cmd.Flags().StringArray("env", nil, "Environment variable (KEY=VALUE)") 44 rootV2Cmd.Flags().StringArray("env", nil, "Environment variable (KEY=VALUE)")
45 rootV2Cmd.Flags().String("env-file", "", "Path to .env file") 45 rootV2Cmd.Flags().String("env-file", "", "Path to .env file")
46 rootV2Cmd.Flags().Int("container-port", 80, "Port the container listens on (Docker only)")
46 47
47 // Check for SHIP_PRETTY env var 48 // Check for SHIP_PRETTY env var
48 if os.Getenv("SHIP_PRETTY") == "1" { 49 if os.Getenv("SHIP_PRETTY") == "1" {
@@ -78,6 +79,7 @@ func runDeployV2(cmd *cobra.Command, args []string) error {
78 opts.TTL, _ = cmd.Flags().GetString("ttl") 79 opts.TTL, _ = cmd.Flags().GetString("ttl")
79 opts.Env, _ = cmd.Flags().GetStringArray("env") 80 opts.Env, _ = cmd.Flags().GetStringArray("env")
80 opts.EnvFile, _ = cmd.Flags().GetString("env-file") 81 opts.EnvFile, _ = cmd.Flags().GetString("env-file")
82 opts.ContainerPort, _ = cmd.Flags().GetInt("container-port")
81 83
82 // deployV2 handles all output and exits 84 // deployV2 handles all output and exits
83 deployV2(path, opts) 85 deployV2(path, opts)