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.
This commit is contained in:
Alistair Young
2026-05-31 04:39:04 -05:00
committed by GitHub
parent c30400cf84
commit 5427758eaa
8 changed files with 26 additions and 9 deletions
+1 -2
View File
@@ -48,7 +48,7 @@ RUN addgroup -g 1000 garageui && \
adduser -D -u 1000 -G garageui garageui adduser -D -u 1000 -G garageui garageui
COPY --from=backend-builder --chown=garageui:garageui /app/garage-ui . 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 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 wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
CMD ["./garage-ui"] CMD ["./garage-ui"]
+10
View File
@@ -127,6 +127,16 @@ garage:
region: "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. See [config.example.yaml](config.example.yaml) for all options including authentication, CORS, and logging.
### Environment Variables ### Environment Variables
+4 -2
View File
@@ -2,7 +2,9 @@ package config
import ( import (
"fmt" "fmt"
"net"
"os" "os"
"strconv"
"strings" "strings"
"github.com/spf13/viper" "github.com/spf13/viper"
@@ -160,7 +162,7 @@ func Load(configPath string, opts ...LoadOption) (*Config, error) {
viper.SetConfigType("yaml") viper.SetConfigType("yaml")
// Built-in defaults (lowest priority) // 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.port", 8080)
viper.SetDefault("server.environment", "production") viper.SetDefault("server.environment", "production")
viper.SetDefault("garage.force_path_style", true) viper.SetDefault("garage.force_path_style", true)
@@ -383,7 +385,7 @@ func (c *Config) Validate() error {
// GetAddress returns the full server address (host:port) // GetAddress returns the full server address (host:port)
func (c *Config) GetAddress() string { 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 // IsDevelopment returns true if running in development mode
+5
View File
@@ -83,6 +83,9 @@ func TestLoad_EnvOnly_MissingFile(t *testing.T) {
if cfg.Server.Port != 9090 { if cfg.Server.Port != 9090 {
t.Errorf("Server.Port = %d, want 9090 (from env)", cfg.Server.Port) 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" { if cfg.Garage.AdminToken != "env-token" {
t.Errorf("Garage.AdminToken = %q, want env-token", cfg.Garage.AdminToken) t.Errorf("Garage.AdminToken = %q, want env-token", cfg.Garage.AdminToken)
} }
@@ -367,6 +370,8 @@ func TestGetAddress(t *testing.T) {
}{ }{
{"localhost", 8080, "localhost:8080"}, {"localhost", 8080, "localhost:8080"},
{"0.0.0.0", 80, "0.0.0.0:80"}, {"0.0.0.0", 80, "0.0.0.0:80"},
{"::", 80, "[::]:80"},
{"::1", 443, "[::1]:443"},
{"", 443, ":443"}, {"", 443, ":443"},
} }
for _, tc := range tests { for _, tc := range tests {
+2 -1
View File
@@ -219,11 +219,12 @@ func main() {
addr := cfg.GetAddress() addr := cfg.GetAddress()
logger.Info(). logger.Info().
Str("address", addr). Str("address", addr).
Str("network", fiber.NetworkTCP).
Str("health_endpoint", fmt.Sprintf("http://%s/health", addr)). Str("health_endpoint", fmt.Sprintf("http://%s/health", addr)).
Str("api_docs", fmt.Sprintf("http://%s/api/v1/", addr)). Str("api_docs", fmt.Sprintf("http://%s/api/v1/", addr)).
Msg("Server starting") 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") logger.Fatal().Err(err).Msg("Failed to start server")
} }
}() }()
+1 -1
View File
@@ -2,7 +2,7 @@
# Server configuration # Server configuration
server: server:
host: "0.0.0.0" host: "::" # IPv6 wildcard; dual-stack behavior depends on OS/runtime socket settings
port: 8080 port: 8080
environment: "development" # development, production environment: "development" # development, production
domain: "localhost" # Domain name for the application domain: "localhost" # Domain name for the application
+2 -2
View File
@@ -69,8 +69,8 @@
"properties": { "properties": {
"host": { "host": {
"type": "string", "type": "string",
"description": "Network interface to bind to (0.0.0.0 for all interfaces)", "description": "Network interface to bind to (use :: for IPv6 wildcard / dual-stack-preferred, or 0.0.0.0 for IPv4 wildcard)",
"default": "0.0.0.0" "default": "::"
}, },
"port": { "port": {
"type": "integer", "type": "integer",
+1 -1
View File
@@ -36,7 +36,7 @@ extraObjects: []
config: config:
server: server:
host: "0.0.0.0" host: "::"
port: 8080 port: 8080
environment: "production" environment: "production"
domain: "garage-ui.example.com" domain: "garage-ui.example.com"