From 5427758eaadc4fa1327402b958b7e7e1f43aecdd Mon Sep 17 00:00:00 2001 From: Alistair Young Date: Sun, 31 May 2026 04:39:04 -0500 Subject: [PATCH] feat(backend,helm)!: bind to IPv6 wildcard by default for dual-stack support * fix: Enable Garage UI to work on IPv6-based clusters. * fix: building on nonstandard-uid machines. * fix: Enable Garage UI to work on IPv6-based clusters. --- Dockerfile | 3 +-- README.md | 10 ++++++++++ backend/internal/config/config.go | 6 ++++-- backend/internal/config/config_test.go | 5 +++++ backend/main.go | 3 ++- config.example.yaml | 2 +- helm/garage-ui/values.schema.json | 4 ++-- helm/garage-ui/values.yaml | 2 +- 8 files changed, 26 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4dbbd9b..c778882 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,7 @@ RUN addgroup -g 1000 garageui && \ adduser -D -u 1000 -G garageui garageui COPY --from=backend-builder --chown=garageui:garageui /app/garage-ui . -COPY --from=frontend-builder /app/frontend/dist ./frontend/dist +COPY --from=frontend-builder --chown=garageui:garageui /app/frontend/dist ./frontend/dist USER garageui @@ -58,4 +58,3 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 CMD ["./garage-ui"] - diff --git a/README.md b/README.md index 5ad0b3e..ced5765 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,16 @@ garage: region: "garage" ``` +Server bind host is configured by `server.host` (default: `::`). IPv6 literals like `::` and `::1` are supported. + +```yaml +server: + host: "::" # IPv6 wildcard (dual-stack-preferred) + port: 8080 +``` + +If your environment needs explicit IPv4-only binding, set `server.host: "0.0.0.0"`. + See [config.example.yaml](config.example.yaml) for all options including authentication, CORS, and logging. ### Environment Variables diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 8b8c5dc..b5a1350 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -2,7 +2,9 @@ package config import ( "fmt" + "net" "os" + "strconv" "strings" "github.com/spf13/viper" @@ -160,7 +162,7 @@ func Load(configPath string, opts ...LoadOption) (*Config, error) { viper.SetConfigType("yaml") // Built-in defaults (lowest priority) - viper.SetDefault("server.host", "0.0.0.0") + viper.SetDefault("server.host", "::") viper.SetDefault("server.port", 8080) viper.SetDefault("server.environment", "production") viper.SetDefault("garage.force_path_style", true) @@ -383,7 +385,7 @@ func (c *Config) Validate() error { // GetAddress returns the full server address (host:port) func (c *Config) GetAddress() string { - return fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port) + return net.JoinHostPort(c.Server.Host, strconv.Itoa(c.Server.Port)) } // IsDevelopment returns true if running in development mode diff --git a/backend/internal/config/config_test.go b/backend/internal/config/config_test.go index 20ad36c..c9a4118 100644 --- a/backend/internal/config/config_test.go +++ b/backend/internal/config/config_test.go @@ -83,6 +83,9 @@ func TestLoad_EnvOnly_MissingFile(t *testing.T) { if cfg.Server.Port != 9090 { t.Errorf("Server.Port = %d, want 9090 (from env)", cfg.Server.Port) } + if cfg.Server.Host != "::" { + t.Errorf("Server.Host = %q, want :: (default)", cfg.Server.Host) + } if cfg.Garage.AdminToken != "env-token" { t.Errorf("Garage.AdminToken = %q, want env-token", cfg.Garage.AdminToken) } @@ -367,6 +370,8 @@ func TestGetAddress(t *testing.T) { }{ {"localhost", 8080, "localhost:8080"}, {"0.0.0.0", 80, "0.0.0.0:80"}, + {"::", 80, "[::]:80"}, + {"::1", 443, "[::1]:443"}, {"", 443, ":443"}, } for _, tc := range tests { diff --git a/backend/main.go b/backend/main.go index f22302a..432c677 100644 --- a/backend/main.go +++ b/backend/main.go @@ -219,11 +219,12 @@ func main() { addr := cfg.GetAddress() logger.Info(). Str("address", addr). + Str("network", fiber.NetworkTCP). Str("health_endpoint", fmt.Sprintf("http://%s/health", addr)). Str("api_docs", fmt.Sprintf("http://%s/api/v1/", addr)). Msg("Server starting") - if err := app.Listen(addr); err != nil { + if err := app.Listen(addr, fiber.ListenConfig{ListenerNetwork: fiber.NetworkTCP}); err != nil { logger.Fatal().Err(err).Msg("Failed to start server") } }() diff --git a/config.example.yaml b/config.example.yaml index 29a7314..b137744 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -2,7 +2,7 @@ # Server configuration server: - host: "0.0.0.0" + host: "::" # IPv6 wildcard; dual-stack behavior depends on OS/runtime socket settings port: 8080 environment: "development" # development, production domain: "localhost" # Domain name for the application diff --git a/helm/garage-ui/values.schema.json b/helm/garage-ui/values.schema.json index 69c9da8..4e351f3 100644 --- a/helm/garage-ui/values.schema.json +++ b/helm/garage-ui/values.schema.json @@ -69,8 +69,8 @@ "properties": { "host": { "type": "string", - "description": "Network interface to bind to (0.0.0.0 for all interfaces)", - "default": "0.0.0.0" + "description": "Network interface to bind to (use :: for IPv6 wildcard / dual-stack-preferred, or 0.0.0.0 for IPv4 wildcard)", + "default": "::" }, "port": { "type": "integer", diff --git a/helm/garage-ui/values.yaml b/helm/garage-ui/values.yaml index dfdcb9d..c16aecd 100644 --- a/helm/garage-ui/values.yaml +++ b/helm/garage-ui/values.yaml @@ -36,7 +36,7 @@ extraObjects: [] config: server: - host: "0.0.0.0" + host: "::" port: 8080 environment: "production" domain: "garage-ui.example.com"