From d97bb6f53eefd2139115d39bca7e17d565222472 Mon Sep 17 00:00:00 2001 From: Clawd Date: Tue, 17 Feb 2026 07:59:50 -0800 Subject: 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/ 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. --- cmd/ship/deploy_impl_v2.go | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) (limited to 'cmd/ship/deploy_impl_v2.go') 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 { } // Generate systemd unit + containerPort := ctx.Opts.ContainerPort + if containerPort == 0 { + containerPort = 80 + } service, err := templates.DockerService(map[string]string{ - "Name": name, - "Port": strconv.Itoa(port), + "Name": name, + "Port": strconv.Itoa(port), + "ContainerPort": strconv.Itoa(containerPort), }) if err != nil { return output.Err(output.ErrServiceFailed, "failed to generate systemd unit: "+err.Error()) @@ -281,38 +286,44 @@ func deployBinaryV2(ctx *deployContext) *output.ErrorResponse { } // allocatePort allocates or retrieves a port for a service +// Uses atomic increment on /etc/ship/next_port to avoid collisions func allocatePort(client *ssh.Client, name string) (int, error) { portFile := fmt.Sprintf("/etc/ship/ports/%s", name) - // Try to read existing port + // Try to read existing port for this app out, err := client.Run(fmt.Sprintf("cat %s 2>/dev/null || echo ''", portFile)) if err == nil && out != "" { - port, err := strconv.Atoi(out[:len(out)-1]) // Strip newline - if err == nil && port > 0 { + out = strings.TrimSpace(out) + if port, err := strconv.Atoi(out); err == nil && port > 0 { return port, nil } } - // Allocate new port - // Find highest used port and increment - 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") + // Allocate new port atomically using flock + // This reads next_port, increments it, and writes back while holding a lock + allocScript := ` +flock -x /etc/ship/.port.lock sh -c ' + mkdir -p /etc/ship/ports + PORT=$(cat /etc/ship/next_port 2>/dev/null || echo 9000) + echo $((PORT + 1)) > /etc/ship/next_port + echo $PORT +'` + out, err = client.RunSudo(allocScript) if err != nil { - out = "" + return 0, fmt.Errorf("failed to allocate port: %w", err) } - nextPort := 9000 - if out != "" { - if lastPort, err := strconv.Atoi(out[:len(out)-1]); err == nil { - nextPort = lastPort + 1 - } + port, err := strconv.Atoi(strings.TrimSpace(out)) + if err != nil { + return 0, fmt.Errorf("invalid port allocated: %s", out) } - // Write port allocation - if err := client.WriteSudoFile(portFile, strconv.Itoa(nextPort)); err != nil { + // Write port allocation for this app + if err := client.WriteSudoFile(portFile, strconv.Itoa(port)); err != nil { return 0, err } - return nextPort, nil + return port, nil } // setTTLV2 sets auto-expiry for a deploy -- cgit v1.2.3