diff options
| author | Clawd <ai@clawd.bot> | 2026-02-17 07:59:50 -0800 |
|---|---|---|
| committer | Clawd <ai@clawd.bot> | 2026-02-17 07:59:50 -0800 |
| commit | d97bb6f53eefd2139115d39bca7e17d565222472 (patch) | |
| tree | 428f230dffad49d166bc521609dd3ee236a42010 /cmd/ship/deploy_impl_v2.go | |
| parent | 3e34265c8a6ea1ec300f987206afb25bad645677 (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.go | 45 |
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 | ||
| 284 | func allocatePort(client *ssh.Client, name string) (int, error) { | 290 | func 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 := ` |
| 305 | flock -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 |
