How to Publish a Container Port to a Specific Host IP Address in apple/container
Use the -p flag with the syntax [host-ip:]host-port:container-port[/protocol] to bind a container port to a specific IP address on the host, such as 127.0.0.1:8080:80 to restrict traffic to localhost.
The apple/container CLI allows you to expose services running inside containers to specific network interfaces on your host machine. By specifying a host IP address in the port mapping, you can control which network interfaces accept incoming traffic, improving security and enabling multi-interface deployments.
Port Mapping Syntax Explained
The complete syntax for publishing a port to a specific host IP follows this pattern:
[host-ip:]host-port:container-port[/protocol]
Each component serves a specific purpose in the network stack:
- host-ip – Optional IPv4 or IPv6 address assigned to a host interface. If omitted, the system defaults to the loopback address (
127.0.0.1for IPv4 or[::1]for IPv6). - host-port – The TCP or UDP port number on the host that will accept connections.
- container-port – The port number inside the container where the application is listening.
- protocol – Optional transport protocol, either
tcp(default) orudp.
When you invoke the container run command with this flag, the CLI parses the string in Sources/Services/ContainerAPIService/Client/Parser.swift via the Parser.publishPort(_:) method (lines 606-661), validates the input, and instantiates a PublishPort object that captures the host address, ports, and protocol.
CLI Usage Examples
Bind to IPv4 Loopback
Restrict access to the local machine only by binding to 127.0.0.1:
container run -d --rm -p 127.0.0.1:8080:8000 node:latest npx http-server -p 8000
This forwards traffic from host 127.0.0.1:8080 to port 8000 inside the container.
Bind to IPv6 Address
For IPv6 interfaces, enclose the address in brackets:
container run -d --rm -p '[::1]:8080:8000' node:latest npx http-server -p 8000
The runtime creates a socket forwarder bound to [::1]:8080 in Sources/Services/RuntimeLinux/Server/RuntimeService.swift (lines 876-907).
Publish UDP Ports
Specify the protocol explicitly for UDP services:
container run -d --rm -p 192.168.1.50:5353:53/udp alpine/dnsmasq
This maps host 192.168.1.50:5353 to container port 53 using UDP, which the parser identifies via the trailing /udp suffix and stores in the PublishPort.proto field.
Map Port Ranges
Publish consecutive ports by specifying ranges:
container run -d --rm -p 127.0.0.1:9000-9004:8000-8004 myapp:latest
The Parser.publishPort method in Parser.swift expands these ranges into individual PublishPort entries with a count field representing the range length.
How Port Publishing Works Under the Hood
Parsing the -p Flag
In Sources/Services/ContainerAPIService/Client/Flags.swift (lines 299-311), the -p/--publish flag is defined to collect string arguments from the command line. These strings are passed to Parser.publishPort(_:) in Parser.swift, which uses regular expressions to extract the host IP, host port, container port, and optional protocol. The parser validates that host ports do not overlap and throws an error if conflicting mappings are detected.
Configuration Storage
The parsed PublishPort objects are appended to ContainerConfiguration.publishedPorts, defined in Sources/ContainerResource/Container/ContainerConfiguration.swift (lines 27-30). This configuration structure serializes the port mappings and passes them to the runtime service when the container starts.
Runtime Socket Forwarding
When the container launches, RuntimeService.startSocketForwarders (implemented in Sources/Services/RuntimeLinux/Server/RuntimeService.swift, lines 876-907) iterates over each PublishPort. For each entry:
- Creates a
SocketAddressfor the host using the specified IP and port - Creates a corresponding
SocketAddressinside the VM using the container's internal IP (containerIPAddress) - Spawns a lightweight proxy that forwards traffic between the host socket and container socket
- Tears down forwarders automatically when the container stops
Programmatic Port Configuration in Swift
You can construct PublishPort instances directly in Swift code for programmatic container configuration:
import ContainerResource
let publish = PublishPort(
hostAddress: .ipv4("192.168.1.50"),
hostPort: 5353,
containerPort: 53,
proto: .udp,
count: 1
)
var config = ContainerConfiguration()
config.publishedPorts.append(publish)
Validate that ports do not overlap before starting the container:
if config.publishedPorts.hasOverlaps() {
throw ContainerizationError(
.invalidArgument,
message: "host ports for different publish port specs may not overlap"
)
}
The hasOverlaps() method is defined in Sources/ContainerResource/Container/PublishPort.swift (lines 54-100).
Summary
- Use the syntax
[host-ip:]host-port:container-port[/protocol]with the-pflag to bind to specific host interfaces. - Default behavior binds to loopback addresses (
127.0.0.1or[::1]) when no host IP is specified. - Implementation spans
Flags.swiftfor CLI input,Parser.swiftfor validation,ContainerConfiguration.swiftfor storage, andRuntimeService.swiftfor socket forwarding. - Protocol support includes TCP (default) and UDP via the
/udpsuffix. - Range mapping allows publishing consecutive ports using hyphenated syntax like
9000-9004:8000-8004.
Frequently Asked Questions
What is the default host IP if I don't specify one?
If you omit the host IP in the port mapping (e.g., -p 8080:80), the apple/container runtime defaults to the loopback address. For IPv4, this is 127.0.0.1; for IPv6, it is [::1]. This default is applied during the parsing phase in Parser.swift when the optional host IP component is missing from the input string.
Can I publish multiple ports to different host IP addresses?
Yes. You can specify multiple -p flags in a single container run command, each targeting a different host IP address. For example: -p 127.0.0.1:3000:3000 -p 192.168.1.50:8080:80. The RuntimeService creates independent socket forwarders for each IP/port combination, provided the host ports do not overlap.
How does the runtime handle port conflicts?
The CLI validation layer in Parser.publishPort(_:) checks for overlapping host ports before the container starts. If two port mappings specify the same host IP and port (or overlapping ranges), the parser throws an error. Additionally, the PublishPort model provides a hasOverlaps() method that you can call programmatically to validate configurations before submitting them to the runtime.
Is IPv6 supported for host IP binding?
Yes. IPv6 addresses are fully supported. When specifying an IPv6 address in the -p flag, you must enclose the address in brackets to distinguish the colons from the port separator, such as [::1]:8080:80 or [2001:db8::1]:443:443. The parser correctly identifies these as IPv6 addresses and passes them to the runtime socket forwarder.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →