feat: allow to bind to multiple host IP addresses specified as CIDR prefix in x-ports (#358)

Signed-off-by: Miek Gieben <miek@miek.nl>
Co-authored-by: Pasha Sviderski <me@psviderski.name>
This commit is contained in:
Miek Gieben
2026-06-04 20:07:58 +10:00
committed by GitHub
co-authored by Pasha Sviderski
parent 3586a32987
commit 1247f961c2
11 changed files with 312 additions and 76 deletions
+6 -4
View File
@@ -93,15 +93,17 @@ func NewRunCommand(groupID string) *cobra.Command {
"Give extended privileges to service containers. This is a security risk and should be used with caution.") "Give extended privileges to service containers. This is a security risk and should be used with caution.")
cmd.Flags().StringSliceVarP(&opts.publish, "publish", "p", nil, cmd.Flags().StringSliceVarP(&opts.publish, "publish", "p", nil,
"Publish a service port to make it accessible outside the cluster. Can be specified multiple times.\n"+ "Publish a service port to make it accessible outside the cluster. Can be specified multiple times.\n"+
"Format: [hostname:]container_port[/protocol] or [host_ip:]host_port:container_port[/protocol]@host\n"+ "Format: [hostname:]container_port[/protocol] or [host_ip|host_prefix:]host_port:container_port[/protocol]@host\n"+
"Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified\n"+ "Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified\n"+
"and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname.\n"+ "and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname.\n"+
"Examples:\n"+ "Examples:\n"+
" -p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname\n"+ " -p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname\n"+
" -p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname\n"+ " -p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname\n"+
// TODO: add support for publishing L4 tcp/udp ports. // TODO: add support for publishing L4 tcp/udp ports.
//" -p 9000:8080 Publish port 8080 as TCP port 9000 via reverse proxy\n"+ //" -p 9000:8080 Publish port 8080 as TCP port 9000 via reverse proxy\n"+
" -p 53:5353/udp@host Bind UDP port 5353 to host port 53") " -p 53:5353/udp@host Bind UDP port 5353 to host port 53\n"+
" -p 192.168.76.0/24:53:5353/udp@host Bind UDP port 5353 to host port 53 on every host IP address\n"+
" contained in the prefix 192.168.76.0/24")
cmd.Flags().StringVar(&opts.pull, "pull", api.PullPolicyMissing, cmd.Flags().StringVar(&opts.pull, "pull", api.PullPolicyMissing,
fmt.Sprintf("Pull image from the registry before running service containers ('%s', '%s', '%s').", fmt.Sprintf("Pull image from the registry before running service containers ('%s', '%s', '%s').",
api.PullPolicyAlways, api.PullPolicyMissing, api.PullPolicyNever)) api.PullPolicyAlways, api.PullPolicyMissing, api.PullPolicyNever))
+34
View File
@@ -0,0 +1,34 @@
package docker
import (
"fmt"
"net"
"net/netip"
)
// addrOfPrefix checks the interfaces and returns the address of each interface that was contained in the prefix.
func addrOfPrefix(prefix netip.Prefix) ([]string, error) {
ifis, err := net.Interfaces()
if err != nil {
return nil, err
}
var addrs []string
for _, ifi := range ifis {
ifaddrs, _ := ifi.Addrs()
for _, addr := range ifaddrs {
ipnet, ok := addr.(*net.IPNet)
if !ok {
continue
}
nip, _ := netip.ParseAddr(ipnet.IP.String()) // round about way is needed to get ipv6 addrs, not mapped v4 in v6.
if prefix.Contains(nip) {
addrs = append(addrs, nip.String())
}
}
}
if len(addrs) == 0 {
return nil, fmt.Errorf("no host addresses are contained in prefix '%s'", prefix)
}
return addrs, nil
}
+17
View File
@@ -620,7 +620,24 @@ func (s *Server) CreateServiceContainer(
if p.HostIP.IsValid() { if p.HostIP.IsValid() {
portBindings[port][0].HostIP = p.HostIP.String() portBindings[port][0].HostIP = p.HostIP.String()
} }
if p.HostPrefix.IsValid() {
addrs, err := addrOfPrefix(p.HostPrefix)
if err != nil {
return nil, err
}
// p.HostIP was not valid, so the first IP can be set in the above added PortBindings, the rest is
// just appended.
portBindings[port][0].HostIP = addrs[0]
for _, addr := range addrs[1:] {
portBindings[port] = append(portBindings[port], nat.PortBinding{
HostPort: strconv.Itoa(int(p.PublishedPort)),
HostIP: addr,
})
}
}
} }
hostConfig := &container.HostConfig{ hostConfig := &container.HostConfig{
CapAdd: spec.Container.CapAdd, CapAdd: spec.Container.CapAdd,
CapDrop: spec.Container.CapDrop, CapDrop: spec.Container.CapDrop,
+85 -50
View File
@@ -23,6 +23,9 @@ type PortSpec struct {
Hostname string Hostname string
// HostIP is the host IP to bind the PublishedPort to. Only valid in host mode. // HostIP is the host IP to bind the PublishedPort to. Only valid in host mode.
HostIP netip.Addr HostIP netip.Addr
// HostPrefix is the host prefix to bind the PublishedPort to. Only valid in host mode. Either HostIP
// is set or HostPrefix
HostPrefix netip.Prefix
// PublishedPort is the port number exposed outside the container. // PublishedPort is the port number exposed outside the container.
// In ingress mode, this is the load balancer port. In host mode, this is the port bound on the host. // In ingress mode, this is the load balancer port. In host mode, this is the port bound on the host.
PublishedPort uint16 PublishedPort uint16
@@ -55,6 +58,9 @@ func (p *PortSpec) Validate() error {
if p.HostIP.IsValid() { if p.HostIP.IsValid() {
return fmt.Errorf("host IP cannot be specified in %s mode", PortModeIngress) return fmt.Errorf("host IP cannot be specified in %s mode", PortModeIngress)
} }
if p.HostPrefix.IsValid() {
return fmt.Errorf("host prefix cannot be specified in %s mode", PortModeIngress)
}
if p.Hostname != "" { if p.Hostname != "" {
if p.Protocol != ProtocolHTTP && p.Protocol != ProtocolHTTPS { if p.Protocol != ProtocolHTTP && p.Protocol != ProtocolHTTPS {
return fmt.Errorf("hostname is only valid with '%s' or '%s' protocols", ProtocolHTTP, ProtocolHTTPS) return fmt.Errorf("hostname is only valid with '%s' or '%s' protocols", ProtocolHTTP, ProtocolHTTPS)
@@ -64,6 +70,9 @@ func (p *PortSpec) Validate() error {
} }
} }
case PortModeHost: case PortModeHost:
if p.HostIP.IsValid() && p.HostPrefix.IsValid() {
return fmt.Errorf("host IP and prefix cannot both be specified in %s mode", PortModeHost)
}
if p.PublishedPort == 0 { if p.PublishedPort == 0 {
return fmt.Errorf("published port is required in %s mode", PortModeHost) return fmt.Errorf("published port is required in %s mode", PortModeHost)
} }
@@ -111,6 +120,16 @@ func (p *PortSpec) String() (string, error) {
parts = append(parts, p.HostIP.String()) parts = append(parts, p.HostIP.String())
} }
} }
if p.HostPrefix.IsValid() {
if p.HostPrefix.Addr().Is6() {
// Enclose the IPv6 address part in square brackets to disambiguate its colons from the
// port separators, e.g. [2001:db8::]/64.
parts = append(parts, fmt.Sprintf("[%s]/%d", p.HostPrefix.Addr(), p.HostPrefix.Bits()))
} else {
parts = append(parts, p.HostPrefix.String())
}
}
parts = append(parts, fmt.Sprint(p.PublishedPort)) parts = append(parts, fmt.Sprint(p.PublishedPort))
parts = append(parts, fmt.Sprint(p.ContainerPort)) parts = append(parts, fmt.Sprint(p.ContainerPort))
@@ -122,56 +141,70 @@ func (p *PortSpec) String() (string, error) {
func ParsePortSpec(port string) (PortSpec, error) { func ParsePortSpec(port string) (PortSpec, error) {
spec := PortSpec{ spec := PortSpec{
Protocol: ProtocolTCP, // Default protocol. Protocol: ProtocolTCP, // Default protocol.
Mode: PortModeIngress, // Default mode.
} }
// Split off mode first. if strings.Count(port, "@") > 1 {
parts := strings.Split(port, "@")
if len(parts) > 2 {
return spec, fmt.Errorf("too many '@' symbols") return spec, fmt.Errorf("too many '@' symbols")
} }
if len(parts) == 2 {
if parts[1] != PortModeHost {
return spec, fmt.Errorf("invalid mode: '%s', only 'host' mode is supported", parts[1])
}
spec.Mode = PortModeHost
}
port = parts[0]
// Parse protocol. parts := splitPortParts(port)
parts = strings.Split(port, "/") specifiedProtocol := "" // Save the set protocol for PortModeIngress to set the correct default later.
if len(parts) > 2 {
return spec, fmt.Errorf("too many '/' symbols") mode := parts[len(parts)-1]
} if i := strings.Index(mode, "@"); i > -1 {
specifiedProtocol := "" spec.Mode = PortModeHost
if len(parts) == 2 {
protocol := parts[1] if mode[i:] != "@"+PortModeHost {
switch protocol { return spec, fmt.Errorf("invalid mode: '%s'", mode[i+1:])
case ProtocolHTTP, ProtocolHTTPS, ProtocolTCP, ProtocolUDP: }
spec.Protocol = protocol mode = mode[:i] // drop @host, leave PORT/udp PORT/tcp or PORT
specifiedProtocol = protocol if i := strings.Index(mode, "/"); i > -1 {
default: switch mode[i+1:] {
return spec, fmt.Errorf("unsupported protocol: '%s'", protocol) case ProtocolTCP:
case ProtocolUDP:
spec.Protocol = ProtocolUDP
default:
return spec, fmt.Errorf("unsupported protocol '%s' in host mode, only 'tcp' and 'udp' are supported",
mode[i+1:])
}
mode = mode[:i] // drop /udp or /tcp, leaving the port only
}
} else {
spec.Mode = PortModeIngress
if i := strings.Index(mode, "/"); i > -1 {
switch mode[i+1:] {
case ProtocolTCP:
spec.Protocol = ProtocolTCP
case ProtocolUDP:
spec.Protocol = ProtocolUDP
case ProtocolHTTP:
spec.Protocol = ProtocolHTTP
case ProtocolHTTPS:
spec.Protocol = ProtocolHTTPS
default:
return spec, fmt.Errorf("unsupported protocol: '%s'", mode[i+1:])
}
specifiedProtocol = mode[i+1:]
mode = mode[:i] // drop /xxx, leaving the port only
} }
} }
port = parts[0]
var err error
if spec.ContainerPort, err = parsePort(mode); err != nil {
return spec, fmt.Errorf("invalid container port '%s': %w", mode, err)
}
// Parse hostname/host IP and ports. // Parse hostname/host IP and ports.
parts = splitPortParts(port)
var err error
switch len(parts) { switch len(parts) {
case 1: // Just container port. case 1:
if spec.ContainerPort, err = parsePort(parts[0]); err != nil { // Container port already done.
return spec, fmt.Errorf("invalid container port '%s': %w", parts[0], err)
}
case 2: // hostname:container_port or [load_balancer_port|host_port]:container_port case 2: // hostname:container_port or [load_balancer_port|host_port]:container_port
if spec.ContainerPort, err = parsePort(parts[1]); err != nil { // Container port (parts[1]) already done
return spec, fmt.Errorf("invalid container port '%s': %w", parts[1], err)
}
if parts[0] == "" { if parts[0] == "" {
return spec, fmt.Errorf("hostname or published port must be specified, format: " + return spec, fmt.Errorf("hostname or published port must be specified, format: " +
"hostname:container_port or published_port:container_port") "hostname:container_port or published_port:container_port")
@@ -188,30 +221,32 @@ func ParsePortSpec(port string) (PortSpec, error) {
} }
case 3: // hostname:load_balancer_port:container_port or host_ip:host_port:container_port case 3: // hostname:load_balancer_port:container_port or host_ip:host_port:container_port
if spec.ContainerPort, err = parsePort(parts[2]); err != nil { // Container port (parts[2]) already done
return spec, fmt.Errorf("invalid container port '%s': %w", parts[2], err)
}
if spec.PublishedPort, err = parsePort(parts[1]); err != nil { if spec.PublishedPort, err = parsePort(parts[1]); err != nil {
return spec, fmt.Errorf("invalid published port '%s': %w", parts[1], err) return spec, fmt.Errorf("invalid published port '%s': %w", parts[1], err)
} }
if spec.Mode == PortModeHost { if spec.Mode == PortModeHost {
// In host mode, the first part must be IP. // In host mode, the first part must be a host IP or prefix.
ip := parts[0] ip := parts[0]
// Strip brackets from IPv6 address if present. // Strip brackets from an IPv6 address if present.
if strings.Contains(ip, ":") { if strings.Contains(ip, ":") {
if !strings.HasPrefix(ip, "[") { end := strings.Index(ip, "]")
if !strings.HasPrefix(ip, "[") || end < 0 {
return spec, fmt.Errorf( return spec, fmt.Errorf(
"invalid host IP '%s': IPv6 address must be enclosed in square brackets", ip) "invalid host IP '%s': IPv6 address must be enclosed in square brackets", ip)
} }
if !strings.HasSuffix(ip, "]") { ip = ip[1:end] + ip[end+1:]
return spec, fmt.Errorf("invalid host IP '%s': missing closing bracket", ip)
}
ip = ip[1 : len(ip)-1]
} }
if spec.HostIP, err = netip.ParseAddr(ip); err != nil { if strings.Contains(ip, "/") {
return spec, fmt.Errorf("invalid host IP '%s': %w", parts[0], err) if spec.HostPrefix, err = netip.ParsePrefix(ip); err != nil {
return spec, fmt.Errorf("invalid host prefix '%s': %w", parts[0], err)
}
} else {
if spec.HostIP, err = netip.ParseAddr(ip); err != nil {
return spec, fmt.Errorf("invalid host IP '%s': %w", parts[0], err)
}
} }
} else { } else {
// Hostname may be empty. // Hostname may be empty.
+118 -1
View File
@@ -191,6 +191,16 @@ func TestPortSpec_Validate(t *testing.T) {
}, },
wantErr: "host IP cannot be specified in ingress mode", wantErr: "host IP cannot be specified in ingress mode",
}, },
{
name: "host prefix in ingress mode",
spec: PortSpec{
HostPrefix: netip.MustParsePrefix("127.0.0.1/8"),
ContainerPort: 8080,
Protocol: ProtocolTCP,
Mode: PortModeIngress,
},
wantErr: "host prefix cannot be specified in ingress mode",
},
{ {
name: "zero published port in host mode", name: "zero published port in host mode",
spec: PortSpec{ spec: PortSpec{
@@ -231,6 +241,18 @@ func TestPortSpec_Validate(t *testing.T) {
}, },
wantErr: "unsupported protocol 'https' in host mode", wantErr: "unsupported protocol 'https' in host mode",
}, },
{
name: "both host ip and prefix",
spec: PortSpec{
HostIP: netip.MustParseAddr("127.0.0.1"),
HostPrefix: netip.MustParsePrefix("127.0.0.0/8"),
PublishedPort: 80,
ContainerPort: 8080,
Protocol: ProtocolTCP,
Mode: PortModeHost,
},
wantErr: "host IP and prefix cannot both be specified in host mode",
},
} }
for _, tt := range tests { for _, tt := range tests {
@@ -369,6 +391,17 @@ func TestPortSpec_String(t *testing.T) {
}, },
expected: "127.0.0.1:80:8080/udp@host", expected: "127.0.0.1:80:8080/udp@host",
}, },
{
name: "host mode with IPv4 prefix",
spec: PortSpec{
HostPrefix: netip.MustParsePrefix("127.0.0.1/8"),
PublishedPort: 80,
ContainerPort: 8080,
Protocol: ProtocolUDP,
Mode: PortModeHost,
},
expected: "127.0.0.1/8:80:8080/udp@host",
},
{ {
name: "host mode with IPv6", name: "host mode with IPv6",
spec: PortSpec{ spec: PortSpec{
@@ -380,6 +413,17 @@ func TestPortSpec_String(t *testing.T) {
}, },
expected: "[2001:db8::1234:5678]:80:8080/tcp@host", expected: "[2001:db8::1234:5678]:80:8080/tcp@host",
}, },
{
name: "host mode with IPv6 prefix",
spec: PortSpec{
HostPrefix: netip.MustParsePrefix("2001:db8::/64"),
PublishedPort: 80,
ContainerPort: 8080,
Protocol: ProtocolUDP,
Mode: PortModeHost,
},
expected: "[2001:db8::]/64:80:8080/udp@host",
},
} }
for _, tt := range tests { for _, tt := range tests {
@@ -553,6 +597,39 @@ func TestParsePortSpec(t *testing.T) {
Mode: PortModeHost, Mode: PortModeHost,
}, },
}, },
{
name: "host mode with prefix and protocol",
port: "192.168.76.0/24:80:8080/udp@host",
expected: PortSpec{
HostPrefix: netip.MustParsePrefix("192.168.76.0/24"),
PublishedPort: 80,
ContainerPort: 8080,
Protocol: ProtocolUDP,
Mode: PortModeHost,
},
},
{
name: "host mode with prefix without protocol",
port: "192.168.76.0/24:80:8080@host",
expected: PortSpec{
HostPrefix: netip.MustParsePrefix("192.168.76.0/24"),
PublishedPort: 80,
ContainerPort: 8080,
Protocol: ProtocolTCP,
Mode: PortModeHost,
},
},
{
name: "host mode with IPv6 prefix and protocol",
port: "[2001:db8::]/64:80:8080/udp@host",
expected: PortSpec{
HostPrefix: netip.MustParsePrefix("2001:db8::/64"),
PublishedPort: 80,
ContainerPort: 8080,
Protocol: ProtocolUDP,
Mode: PortModeHost,
},
},
// Error cases. // Error cases.
{ {
@@ -560,11 +637,31 @@ func TestParsePortSpec(t *testing.T) {
port: "", port: "",
wantErr: "invalid container port", wantErr: "invalid container port",
}, },
{
name: "slash",
port: "/",
wantErr: "unsupported protocol",
},
{
name: "at",
port: "@",
wantErr: "invalid mode",
},
{ {
name: "invalid container port", name: "invalid container port",
port: "invalid", port: "invalid",
wantErr: "invalid container port", wantErr: "invalid container port",
}, },
{
name: "no protocol",
port: "53/",
wantErr: "unsupported protocol",
},
{
name: "no host modifier",
port: "53@",
wantErr: "invalid mode",
},
{ {
name: "container port zero", name: "container port zero",
port: "0", port: "0",
@@ -598,7 +695,7 @@ func TestParsePortSpec(t *testing.T) {
{ {
name: "multiple protocols", name: "multiple protocols",
port: "8080/tcp/udp", port: "8080/tcp/udp",
wantErr: "too many '/' symbols", wantErr: "unsupported protocol: 'tcp/udp'",
}, },
{ {
name: "invalid protocol", name: "invalid protocol",
@@ -671,6 +768,26 @@ func TestParsePortSpec(t *testing.T) {
port: "app.example.com:invalid:8080@host", port: "app.example.com:invalid:8080@host",
wantErr: "invalid published port", wantErr: "invalid published port",
}, },
{
name: "invalid prefix",
port: "192.168.76.0/45:53:5353/udp@host",
wantErr: "invalid host prefix",
},
{
name: "valid prefix invalid protocol",
port: "192.168.76.0/24:53:5353/http@host",
wantErr: "unsupported protocol 'http' in host mode, only 'tcp' and 'udp' are supported",
},
{
name: "valid prefix invalid protocol",
port: "192.168.76.0/24:53:5353/invalid@host",
wantErr: "unsupported protocol 'invalid' in host mode, only 'tcp' and 'udp' are supported",
},
{
name: "valid prefix no protocol",
port: "192.168.76.0/24:53:5353/@host",
wantErr: "unsupported protocol '' in host mode, only 'tcp' and 'udp' are supported",
},
} }
for _, tt := range tests { for _, tt := range tests {
+7 -3
View File
@@ -112,10 +112,14 @@ func convertServicePortConfigToPortSpec(port types.ServicePortConfig) (api.PortS
// Set host IP if specified // Set host IP if specified
if port.HostIP != "" { if port.HostIP != "" {
hostIP, err := netip.ParseAddr(port.HostIP) hostIP, err := netip.ParseAddr(port.HostIP)
if err != nil { if err == nil {
return spec, fmt.Errorf("invalid host IP %q: %w", port.HostIP, err) spec.HostIP = hostIP
} else {
spec.HostPrefix, err = netip.ParsePrefix(port.HostIP)
if err != nil {
return spec, fmt.Errorf("invalid host IP or prefix '%s': %w", port.HostIP, err)
}
} }
spec.HostIP = hostIP
} }
// Validate the resulting spec // Validate the resulting spec
+9
View File
@@ -58,6 +58,15 @@ func TestConvertStandardPortsToPortSpecs(t *testing.T) {
{ContainerPort: 8080, PublishedPort: 80, Protocol: "tcp", Mode: "host", HostIP: mustParseAddr("::1")}, {ContainerPort: 8080, PublishedPort: 80, Protocol: "tcp", Mode: "host", HostIP: mustParseAddr("::1")},
}, },
}, },
{
name: "host prefix",
ports: []types.ServicePortConfig{
{Target: 8080, Published: "80", Protocol: "tcp", HostIP: "192.168.76.0/24", Mode: "host"},
},
expected: []api.PortSpec{
{ContainerPort: 8080, PublishedPort: 80, Protocol: "tcp", Mode: "host", HostPrefix: netip.MustParsePrefix("192.168.76.0/24")},
},
},
} }
for _, tt := range tests { for _, tt := range tests {
+1 -1
View File
@@ -315,7 +315,7 @@ func TestServiceSpecFromCompose(t *testing.T) {
return strings.Compare(a.Name, b.Name) return strings.Compare(a.Name, b.Name)
}) })
cmpOpts := cmp.Options{cmpopts.EquateEmpty(), cmpopts.EquateComparable(netip.Addr{})} cmpOpts := cmp.Options{cmpopts.EquateEmpty(), cmpopts.EquateComparable(netip.Addr{}, netip.Prefix{})}
assert.True(t, cmp.Equal(spec, expectedSpec, cmpOpts...), cmp.Diff(spec, expectedSpec, cmpOpts...)) assert.True(t, cmp.Equal(spec, expectedSpec, cmpOpts...), cmp.Diff(spec, expectedSpec, cmpOpts...))
} }
}) })
@@ -45,20 +45,23 @@ extension:
network interface(s). This is useful for non-HTTP services that need direct port access (bypasses Caddy): network interface(s). This is useful for non-HTTP services that need direct port access (bypasses Caddy):
``` ```
[host_ip:]host_port:container_port[/protocol]@host [host_ip|host_prefix:]host_port:container_port[/protocol]@host
``` ```
- `host_ip` (optional): The IP address on the host to bind to. If omitted, binds to all interfaces. - `host_ip` / `host_prefix` (optional): The IP address on the host to bind to. Or an IP prefix in
[CIDR](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) notation, which binds to every host IP address
that is contained in the prefix. If omitted, binds to all interfaces.
- `host_port`: The port number on the host to bind to. - `host_port`: The port number on the host to bind to.
- `container_port`: The port number within the container that's listening for traffic. - `container_port`: The port number within the container that's listening for traffic.
- `protocol` (optional): `tcp` or `udp` (default: `tcp`) - `protocol` (optional): `tcp` or `udp` (default: `tcp`)
| Port value | Description | | Port value | Description |
|------------------------------|--------------------------------------------------------------------------------------| |--------------------------------------|--------------------------------------------------------------------------------------|
| `8000/http` | Publish port 8000 as HTTP via Caddy using hostname `<service-name>.<cluster-domain>` | | `8000/http` | Publish port 8000 as HTTP via Caddy using hostname `<service-name>.<cluster-domain>` |
| `app.example.com:8080/https` | Publish port 8080 as HTTPS via Caddy using hostname `app.example.com` | | `app.example.com:8080/https` | Publish port 8080 as HTTPS via Caddy using hostname `app.example.com` |
| `127.0.0.1:5432:5432@host` | Bind TCP port 5432 to host port 5432 on loopback interface only | | `127.0.0.1:5432:5432@host` | Bind TCP port 5432 to host port 5432 on loopback interface only |
| `53:5353/udp@host` | Bind UDP port 5353 to host port 53 on all network interfaces | | `53:5353/udp@host` | Bind UDP port 5353 to host port 53 on all network interfaces |
| `192.168.76.0/24:5432:5432/tcp@host` | Bind TCP port 5432 to host port 5432 on every host IP contained in 192.168.76.0/24 |
:::warning :::warning
@@ -138,7 +141,7 @@ custom global configuration.
The following functions and variables are available: The following functions and variables are available:
| Template | Description | | Template | Description |
|---------------------------------------|-----------------------------------------------------------------------------------------------| | ------------------------------------- | --------------------------------------------------------------------------------------------- |
| `{{upstreams [service-name] [port]}}` | A space-separated list of healthy container IPs for the current or specified service and port | | `{{upstreams [service-name] [port]}}` | A space-separated list of healthy container IPs for the current or specified service and port |
| `{{.Name}}` | The name of the service the config belongs to | | `{{.Name}}` | The name of the service the config belongs to |
| `{{.Upstreams}}` | A map of all service names to their healthy container IPs | | `{{.Upstreams}}` | A map of all service names to their healthy container IPs |
@@ -149,38 +152,49 @@ changes.
**Examples:** **Examples:**
1. Current service upstreams, default port: 1. Current service upstreams, default port:
```caddyfile ```caddyfile
reverse_proxy {{upstreams}} reverse_proxy {{upstreams}}
``` ```
```caddyfile ```caddyfile
reverse_proxy 10.210.1.3 10.210.2.5 reverse_proxy 10.210.1.3 10.210.2.5
``` ```
2. Current service upstreams, port 8000: 2. Current service upstreams, port 8000:
```caddyfile ```caddyfile
reverse_proxy {{upstreams 8000}} reverse_proxy {{upstreams 8000}}
``` ```
```caddyfile ```caddyfile
reverse_proxy 10.210.1.3:8000 10.210.2.5:8000 reverse_proxy 10.210.1.3:8000 10.210.2.5:8000
``` ```
3. Current service upstreams with `https` scheme: 3. Current service upstreams with `https` scheme:
```caddyfile ```caddyfile
reverse_proxy {{- range $ip := index .Upstreams .Name}} https://{{$ip}}{{end}} reverse_proxy {{- range $ip := index .Upstreams .Name}} https://{{$ip}}{{end}}
``` ```
```caddyfile ```caddyfile
reverse_proxy https://10.210.1.3 https://10.210.2.5 reverse_proxy https://10.210.1.3 https://10.210.2.5
``` ```
4. `api` service upstreams, port 9000: 4. `api` service upstreams, port 9000:
```caddyfile ```caddyfile
handle_path /api/* { handle_path /api/* {
reverse_proxy {{upstreams "api" 9000}} reverse_proxy {{upstreams "api" 9000}}
} }
``` ```
```caddyfile ```caddyfile
+6 -4
View File
@@ -23,13 +23,15 @@ uc run IMAGE [COMMAND...] [flags]
-n, --name string Assign a name to the service. A random name is generated if not specified. -n, --name string Assign a name to the service. A random name is generated if not specified.
--privileged Give extended privileges to service containers. This is a security risk and should be used with caution. --privileged Give extended privileges to service containers. This is a security risk and should be used with caution.
-p, --publish strings Publish a service port to make it accessible outside the cluster. Can be specified multiple times. -p, --publish strings Publish a service port to make it accessible outside the cluster. Can be specified multiple times.
Format: [hostname:]container_port[/protocol] or [host_ip:]host_port:container_port[/protocol]@host Format: [hostname:]container_port[/protocol] or [host_ip|host_prefix:]host_port:container_port[/protocol]@host
Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified
and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname. and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname.
Examples: Examples:
-p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname -p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname
-p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname -p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname
-p 53:5353/udp@host Bind UDP port 5353 to host port 53 -p 53:5353/udp@host Bind UDP port 5353 to host port 53
-p 192.168.76.0/24:53:5353/udp@host Bind UDP port 5353 to host port 53 on every host IP address
contained in the prefix 192.168.76.0/24
--pull string Pull image from the registry before running service containers ('always', 'missing', 'never'). (default "missing") --pull string Pull image from the registry before running service containers ('always', 'missing', 'never'). (default "missing")
--replicas uint Number of containers to run for the service. Only valid for a replicated service. (default 1) --replicas uint Number of containers to run for the service. Only valid for a replicated service. (default 1)
--shm-size bytes Maximum amount of shared memory (mounted at /dev/shm) a service container can use. Value is a positive integer --shm-size bytes Maximum amount of shared memory (mounted at /dev/shm) a service container can use. Value is a positive integer
@@ -23,13 +23,15 @@ uc service run IMAGE [COMMAND...] [flags]
-n, --name string Assign a name to the service. A random name is generated if not specified. -n, --name string Assign a name to the service. A random name is generated if not specified.
--privileged Give extended privileges to service containers. This is a security risk and should be used with caution. --privileged Give extended privileges to service containers. This is a security risk and should be used with caution.
-p, --publish strings Publish a service port to make it accessible outside the cluster. Can be specified multiple times. -p, --publish strings Publish a service port to make it accessible outside the cluster. Can be specified multiple times.
Format: [hostname:]container_port[/protocol] or [host_ip:]host_port:container_port[/protocol]@host Format: [hostname:]container_port[/protocol] or [host_ip|host_prefix:]host_port:container_port[/protocol]@host
Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified
and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname. and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname.
Examples: Examples:
-p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname -p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname
-p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname -p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname
-p 53:5353/udp@host Bind UDP port 5353 to host port 53 -p 53:5353/udp@host Bind UDP port 5353 to host port 53
-p 192.168.76.0/24:53:5353/udp@host Bind UDP port 5353 to host port 53 on every host IP address
contained in the prefix 192.168.76.0/24
--pull string Pull image from the registry before running service containers ('always', 'missing', 'never'). (default "missing") --pull string Pull image from the registry before running service containers ('always', 'missing', 'never'). (default "missing")
--replicas uint Number of containers to run for the service. Only valid for a replicated service. (default 1) --replicas uint Number of containers to run for the service. Only valid for a replicated service. (default 1)
--shm-size bytes Maximum amount of shared memory (mounted at /dev/shm) a service container can use. Value is a positive integer --shm-size bytes Maximum amount of shared memory (mounted at /dev/shm) a service container can use. Value is a positive integer