From 48b9cc525def684e4639c5255a4e6fafd82b5e9f Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Tue, 28 Apr 2026 10:56:03 +0000 Subject: [PATCH 1/3] Fix: inject SERVER_PORT=$PORT for Spring Boot JAR apps Apps with server.port set in application.yml would bind to the wrong port at startup, causing CF health checks to fail. Apps with a privileged port (< 1024, e.g. 443) would crash immediately with java.net.BindException: Permission denied. Mirrors the Ruby buildpack behaviour in spring_boot.rb release(). Fixes cloudfoundry/java-buildpack#1255 --- src/java/containers/spring_boot.go | 9 +++++++++ src/java/containers/spring_boot_test.go | 10 ++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/java/containers/spring_boot.go b/src/java/containers/spring_boot.go index d6016d52b..a884d7982 100644 --- a/src/java/containers/spring_boot.go +++ b/src/java/containers/spring_boot.go @@ -233,6 +233,15 @@ func (s *SpringBootContainer) Finalize() error { return fmt.Errorf("failed to write JAVA_OPTS: %w", err) } + // Ensure the app binds to CF's assigned port, overriding any server.port set in + // application.yml or other Spring config. Without this, apps with a hardcoded + // server.port will either bind to the wrong port (health check fails) or crash + // with java.net.BindException: Permission denied for privileged ports (< 1024). + // Mirrors Ruby buildpack: lib/java_buildpack/container/spring_boot.rb release() + if err := s.context.Stager.WriteEnvFile("SERVER_PORT", "$PORT"); err != nil { + return fmt.Errorf("failed to write SERVER_PORT: %w", err) + } + return nil } diff --git a/src/java/containers/spring_boot_test.go b/src/java/containers/spring_boot_test.go index f5897c827..a8e1822a8 100644 --- a/src/java/containers/spring_boot_test.go +++ b/src/java/containers/spring_boot_test.go @@ -169,5 +169,15 @@ var _ = Describe("Spring Boot Container", func() { err := container.Finalize() Expect(err).NotTo(HaveOccurred()) }) + + It("writes SERVER_PORT=$PORT so app binds to CF's assigned port regardless of server.port in application.yml", func() { + err := container.Finalize() + Expect(err).NotTo(HaveOccurred()) + + envFile := filepath.Join(depsDir, "0", "env", "SERVER_PORT") + data, err := os.ReadFile(envFile) + Expect(err).NotTo(HaveOccurred()) + Expect(string(data)).To(Equal("$PORT")) + }) }) }) From 30aeaf8e43f4881364324f4bac4781cd836dfca3 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Tue, 28 Apr 2026 12:41:10 +0000 Subject: [PATCH 2/3] fix: use WriteProfileD for SERVER_PORT so $PORT is shell-expanded at runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WriteEnvFile writes the literal string "$PORT" to deps/0/env/SERVER_PORT. CF's launcher reads env files as plain text (no shell expansion), so Spring Boot received the literal string "$PORT" as the port, which is invalid and ignored, leaving server.port from application.yml in effect. Replace with WriteProfileD which writes a bash profile.d script that is *sourced* at container start — so $PORT is expanded to the actual CF-assigned port number before Spring Boot initialises its embedded server. This fixes: - apps binding to a hardcoded port instead of the CF-assigned $PORT - java.net.BindException: Permission denied for privileged ports (e.g. 443) --- src/java/containers/spring_boot.go | 5 +++-- src/java/containers/spring_boot_test.go | 19 +++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/java/containers/spring_boot.go b/src/java/containers/spring_boot.go index a884d7982..ac6119c1c 100644 --- a/src/java/containers/spring_boot.go +++ b/src/java/containers/spring_boot.go @@ -237,9 +237,10 @@ func (s *SpringBootContainer) Finalize() error { // application.yml or other Spring config. Without this, apps with a hardcoded // server.port will either bind to the wrong port (health check fails) or crash // with java.net.BindException: Permission denied for privileged ports (< 1024). + // Uses WriteProfileD (not WriteEnvFile) so that $PORT is shell-expanded at runtime. // Mirrors Ruby buildpack: lib/java_buildpack/container/spring_boot.rb release() - if err := s.context.Stager.WriteEnvFile("SERVER_PORT", "$PORT"); err != nil { - return fmt.Errorf("failed to write SERVER_PORT: %w", err) + if err := s.context.Stager.WriteProfileD("spring_boot_server_port.sh", "export SERVER_PORT=$PORT\n"); err != nil { + return fmt.Errorf("failed to write SERVER_PORT profile.d script: %w", err) } return nil diff --git a/src/java/containers/spring_boot_test.go b/src/java/containers/spring_boot_test.go index a8e1822a8..752dd1ec7 100644 --- a/src/java/containers/spring_boot_test.go +++ b/src/java/containers/spring_boot_test.go @@ -1,8 +1,11 @@ package containers_test import ( + "fmt" "os" + "os/exec" "path/filepath" + "strings" "github.com/cloudfoundry/java-buildpack/src/java/common" "github.com/cloudfoundry/java-buildpack/src/java/containers" @@ -170,14 +173,22 @@ var _ = Describe("Spring Boot Container", func() { Expect(err).NotTo(HaveOccurred()) }) - It("writes SERVER_PORT=$PORT so app binds to CF's assigned port regardless of server.port in application.yml", func() { + It("writes a profile.d script that exports SERVER_PORT=$PORT so the variable is shell-expanded at runtime", func() { err := container.Finalize() Expect(err).NotTo(HaveOccurred()) - envFile := filepath.Join(depsDir, "0", "env", "SERVER_PORT") - data, err := os.ReadFile(envFile) + profileScript := filepath.Join(depsDir, "0", "profile.d", "spring_boot_server_port.sh") + data, err := os.ReadFile(profileScript) Expect(err).NotTo(HaveOccurred()) - Expect(string(data)).To(Equal("$PORT")) + Expect(string(data)).To(Equal("export SERVER_PORT=$PORT\n")) + + // Verify $PORT is actually shell-expanded at runtime (not left as literal "$PORT"). + // Simulates what CF's launcher does: source the profile.d script with PORT set in env. + cmd := exec.Command("bash", "-c", fmt.Sprintf("PORT=8080 . %s && echo $SERVER_PORT", profileScript)) + out, bashErr := cmd.Output() + Expect(bashErr).NotTo(HaveOccurred()) + Expect(strings.TrimSpace(string(out))).To(Equal("8080"), + "SERVER_PORT should be the expanded value of $PORT, not the literal string \"$PORT\"") }) }) }) From 745b7001827c1a6d915ef2aaa4b877bc48fd0aaf Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Tue, 28 Apr 2026 13:19:11 +0000 Subject: [PATCH 3/3] fix: use WriteProfileD for SERVER_PORT in Spring Boot CLI so $PORT expands at runtime --- src/java/containers/spring_boot_cli.go | 12 ++- src/java/containers/spring_boot_cli_test.go | 82 +++++++++++++++++++++ 2 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 src/java/containers/spring_boot_cli_test.go diff --git a/src/java/containers/spring_boot_cli.go b/src/java/containers/spring_boot_cli.go index ffeefaf4d..ff0c80211 100644 --- a/src/java/containers/spring_boot_cli.go +++ b/src/java/containers/spring_boot_cli.go @@ -107,15 +107,13 @@ func (s *SpringBootCLIContainer) Finalize() error { s.context.Log.BeginStep("Finalizing Spring Boot CLI") // Set environment variables for Spring Boot CLI - envVars := map[string]string{ - "JAVA_OPTS": "$JAVA_OPTS", - "SERVER_PORT": "$PORT", + if err := s.context.Stager.WriteEnvFile("JAVA_OPTS", "$JAVA_OPTS"); err != nil { + s.context.Log.Warning("Failed to set JAVA_OPTS: %s", err.Error()) } - for key, value := range envVars { - if err := s.context.Stager.WriteEnvFile(key, value); err != nil { - s.context.Log.Warning("Failed to set %s: %s", key, err.Error()) - } + // Use WriteProfileD so $PORT is shell-expanded at runtime (WriteEnvFile writes plain text, no expansion). + if err := s.context.Stager.WriteProfileD("spring_boot_cli_server_port.sh", "export SERVER_PORT=$PORT\n"); err != nil { + return fmt.Errorf("failed to write SERVER_PORT profile.d script: %w", err) } return nil diff --git a/src/java/containers/spring_boot_cli_test.go b/src/java/containers/spring_boot_cli_test.go new file mode 100644 index 000000000..cf68dd395 --- /dev/null +++ b/src/java/containers/spring_boot_cli_test.go @@ -0,0 +1,82 @@ +package containers_test + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/cloudfoundry/java-buildpack/src/java/common" + "github.com/cloudfoundry/java-buildpack/src/java/containers" + "github.com/cloudfoundry/libbuildpack" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Spring Boot CLI Container", func() { + var ( + ctx *common.Context + container *containers.SpringBootCLIContainer + buildDir string + depsDir string + cacheDir string + ) + + BeforeEach(func() { + var err error + buildDir, err = os.MkdirTemp("", "build") + Expect(err).NotTo(HaveOccurred()) + + depsDir, err = os.MkdirTemp("", "deps") + Expect(err).NotTo(HaveOccurred()) + + cacheDir, err = os.MkdirTemp("", "cache") + Expect(err).NotTo(HaveOccurred()) + + err = os.MkdirAll(filepath.Join(depsDir, "0"), 0755) + Expect(err).NotTo(HaveOccurred()) + + logger := libbuildpack.NewLogger(os.Stdout) + manifest := &libbuildpack.Manifest{} + installer := &libbuildpack.Installer{} + stager := libbuildpack.NewStager([]string{buildDir, cacheDir, depsDir, "0"}, logger, manifest) + command := &libbuildpack.Command{} + + ctx = &common.Context{ + Stager: stager, + Manifest: manifest, + Installer: installer, + Log: logger, + Command: command, + } + + container = containers.NewSpringBootCLIContainer(ctx) + }) + + AfterEach(func() { + os.RemoveAll(buildDir) + os.RemoveAll(depsDir) + os.RemoveAll(cacheDir) + }) + + Describe("Finalize", func() { + It("writes a profile.d script that exports SERVER_PORT=$PORT so the variable is shell-expanded at runtime", func() { + err := container.Finalize() + Expect(err).NotTo(HaveOccurred()) + + profileScript := filepath.Join(depsDir, "0", "profile.d", "spring_boot_cli_server_port.sh") + data, err := os.ReadFile(profileScript) + Expect(err).NotTo(HaveOccurred()) + Expect(string(data)).To(Equal("export SERVER_PORT=$PORT\n")) + + // Verify $PORT is actually shell-expanded at runtime (not left as literal "$PORT"). + // Simulates what CF's launcher does: source the profile.d script with PORT set in env. + cmd := exec.Command("bash", "-c", fmt.Sprintf("PORT=8080 . %s && echo $SERVER_PORT", profileScript)) + out, bashErr := cmd.Output() + Expect(bashErr).NotTo(HaveOccurred()) + Expect(strings.TrimSpace(string(out))).To(Equal("8080"), + "SERVER_PORT should be the expanded value of $PORT, not the literal string \"$PORT\"") + }) + }) +})