summaryrefslogtreecommitdiffstats
path: root/cmd/ship/deploy_impl_v2.go
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/ship/deploy_impl_v2.go
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/ship/deploy_impl_v2.go')
-rw-r--r--cmd/ship/deploy_impl_v2.go45
1 files changed, 28 insertions, 17 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