Adds container network for macOS 26. (#243)

See discussion below for example. For multiple network interfaces in a
single container we'll want to integrate against a containerization that
includes apple/containerization#156.

The change bumps the containerization dependency to 0.2.0 and addresses
the breaking API changes.

```console
% container network
OVERVIEW: Manage container networks

USAGE: container network <subcommand>

OPTIONS:
  --version               Show the version.
  -h, --help              Show help information.

SUBCOMMANDS:
  create                  Create a new network
  delete, rm              Delete one or more networks
  list, ls                List networks
  inspect                 Display information about one or more networks

  See 'container help network <subcommand>' for detailed help.
```
This commit is contained in:
J Logan
2025-06-27 14:12:29 -07:00
committed by GitHub
parent 3fcd0dda67
commit 3b5c253059
22 changed files with 596 additions and 64 deletions
+1
View File
@@ -136,6 +136,7 @@ integration: init-block
@echo "Removing any existing containers"
@bin/container rm --all
@echo "Starting CLI integration tests"
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLINetwork
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIRunLifecycle
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIExecCommand
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIRunCommand
+5 -5
View File
@@ -1,5 +1,5 @@
{
"originHash" : "4ac93777a9a369fb7c46f1af4cd15c516926a8f4b23679f1ee2bc40b9a422313",
"originHash" : "cc0718ffe17715cc940f40d92b33532b6dd1767065ccc6039f7174402ba5930f",
"pins" : [
{
"identity" : "async-http-client",
@@ -15,8 +15,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/containerization.git",
"state" : {
"revision" : "4b05e5f2313e881ee048f7063b30e73070fbd1b1",
"version" : "0.1.1"
"revision" : "1a59de82b052a86edd9e2bd6f023d212a061b0b4",
"version" : "0.2.0"
}
},
{
@@ -240,8 +240,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-syntax.git",
"state" : {
"revision" : "0687f71944021d616d34d922343dcef086855920",
"version" : "600.0.1"
"revision" : "fd6373fad9cf3b3bc4e3c06d92f0cfd60007fa8e",
"version" : "602.0.0-prerelease-2025-06-26"
}
},
{
+6 -3
View File
@@ -26,7 +26,7 @@ if let path = ProcessInfo.processInfo.environment["CONTAINERIZATION_PATH"] {
scDependency = .package(path: path)
scVersion = "latest"
} else {
scVersion = "0.1.1"
scVersion = "0.2.0"
scDependency = .package(url: "https://github.com/apple/containerization.git", exact: Version(stringLiteral: scVersion))
}
@@ -295,10 +295,13 @@ let package = Package(
.testTarget(
name: "CLITests",
dependencies: [
.product(name: "ContainerizationOS", package: "containerization"),
.product(name: "AsyncHTTPClient", package: "async-http-client"),
.product(name: "Containerization", package: "containerization"),
"ContainerClient",
.product(name: "ContainerizationExtras", package: "containerization"),
.product(name: "ContainerizationOS", package: "containerization"),
"ContainerBuild",
"ContainerClient",
"ContainerNetworkService",
],
path: "Tests/CLITests"
),
+53 -41
View File
@@ -77,11 +77,8 @@ struct Application: AsyncParsableCommand {
]
),
CommandGroup(
name: "System",
subcommands: [
BuilderCommand.self,
SystemCommand.self,
]
name: "Other",
subcommands: Self.otherCommands()
),
],
// Hidden command to handle plugins on unrecognized input.
@@ -112,42 +109,6 @@ struct Application: AsyncParsableCommand {
return PluginLoader(pluginDirectories: pluginDirectories, pluginFactories: pluginFactories, defaultResourcePath: statePath, log: log)
}()
func validate() throws {
// Not really a "validation", but a cheat to run this before
// any of the commands do their business.
let debugEnvVar = ProcessInfo.processInfo.environment["CONTAINER_DEBUG"]
if self.global.debug || debugEnvVar != nil {
log.logLevel = .debug
}
// Ensure we're not running under Rosetta.
if try isTranslated() {
throw ValidationError(
"""
`container` is currently running under Rosetta Translation, which could be
caused by your terminal application. Please ensure this is turned off.
"""
)
}
}
private static func restoreCursorAtExit() {
let signalHandler: @convention(c) (Int32) -> Void = { signal in
let exitCode = ExitCode(signal + 128)
Application.exit(withError: exitCode)
}
// Termination by Ctrl+C.
signal(SIGINT, signalHandler)
// Termination using `kill`.
signal(SIGTERM, signalHandler)
// Normal and explicit exit.
atexit {
if let progressConfig = try? ProgressConfig() {
let progressBar = ProgressBar(config: progressConfig)
progressBar.resetCursor()
}
}
}
public static func main() async throws {
restoreCursorAtExit()
@@ -261,6 +222,57 @@ struct Application: AsyncParsableCommand {
return -1
}
}
func validate() throws {
// Not really a "validation", but a cheat to run this before
// any of the commands do their business.
let debugEnvVar = ProcessInfo.processInfo.environment["CONTAINER_DEBUG"]
if self.global.debug || debugEnvVar != nil {
log.logLevel = .debug
}
// Ensure we're not running under Rosetta.
if try isTranslated() {
throw ValidationError(
"""
`container` is currently running under Rosetta Translation, which could be
caused by your terminal application. Please ensure this is turned off.
"""
)
}
}
private static func otherCommands() -> [any ParsableCommand.Type] {
guard #available(macOS 26, *) else {
return [
BuilderCommand.self,
SystemCommand.self,
]
}
return [
BuilderCommand.self,
NetworkCommand.self,
SystemCommand.self,
]
}
private static func restoreCursorAtExit() {
let signalHandler: @convention(c) (Int32) -> Void = { signal in
let exitCode = ExitCode(signal + 128)
Application.exit(withError: exitCode)
}
// Termination by Ctrl+C.
signal(SIGINT, signalHandler)
// Termination using `kill`.
signal(SIGTERM, signalHandler)
// Normal and explicit exit.
atexit {
if let progressConfig = try? ProgressConfig() {
let progressBar = ProgressBar(config: progressConfig)
progressBar.resetCursor()
}
}
}
}
extension Application {
+1 -1
View File
@@ -45,7 +45,7 @@ extension Application {
if containerIDs.count > 0 && all {
throw ContainerizationError(
.invalidArgument,
message: "explicitly supplied container IDs conflicts with the --all flag"
message: "explicitly supplied container ID(s) conflict with the --all flag"
)
}
}
+33
View File
@@ -0,0 +1,33 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import ArgumentParser
extension Application {
struct NetworkCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "network",
abstract: "Manage container networks",
subcommands: [
NetworkCreate.self,
NetworkDelete.self,
NetworkList.self,
NetworkInspect.self,
],
aliases: ["n"]
)
}
}
+42
View File
@@ -0,0 +1,42 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import ArgumentParser
import ContainerClient
import ContainerNetworkService
import ContainerizationError
import Foundation
import TerminalProgress
extension Application {
struct NetworkCreate: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "create",
abstract: "Create a new network")
@Argument(help: "Network name")
var name: String
@OptionGroup
var global: Flags.Global
func run() async throws {
let config = NetworkConfiguration(id: self.name, mode: .nat)
let state = try await ClientNetwork.create(configuration: config)
print(state.id)
}
}
}
+116
View File
@@ -0,0 +1,116 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import ArgumentParser
import ContainerClient
import ContainerNetworkService
import ContainerizationError
import Foundation
extension Application {
struct NetworkDelete: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "delete",
abstract: "Delete one or more networks",
aliases: ["rm"])
@Flag(name: .shortAndLong, help: "Remove all networks")
var all = false
@OptionGroup
var global: Flags.Global
@Argument(help: "Network names")
var networkNames: [String] = []
func validate() throws {
if networkNames.count == 0 && !all {
throw ContainerizationError(.invalidArgument, message: "no networks specified and --all not supplied")
}
if networkNames.count > 0 && all {
throw ContainerizationError(
.invalidArgument,
message: "explicitly supplied network name(s) conflict with the --all flag"
)
}
}
mutating func run() async throws {
let uniqueNetworkNames = Set<String>(networkNames)
let networks: [NetworkState]
if all {
networks = try await ClientNetwork.list()
} else {
networks = try await ClientNetwork.list()
.filter { c in
uniqueNetworkNames.contains(c.id)
}
// If one of the networks requested isn't present lets throw. We don't need to do
// this for --all as --all should be perfectly usable with no networks to remove,
// otherwise it'd be quite clunky.
if networks.count != uniqueNetworkNames.count {
let missing = uniqueNetworkNames.filter { id in
!networks.contains { n in
n.id == id
}
}
throw ContainerizationError(
.notFound,
message: "failed to delete one or more networks: \(missing)"
)
}
}
if uniqueNetworkNames.contains(ClientNetwork.defaultNetworkName) {
throw ContainerizationError(
.invalidArgument,
message: "cannot delete the default network"
)
}
var failed = [String]()
try await withThrowingTaskGroup(of: NetworkState?.self) { group in
for network in networks {
group.addTask {
do {
// delete atomically disables the IP allocator, then deletes
// the allocator disable fails if any IPs are still in use
try await ClientNetwork.delete(id: network.id)
print(network.id)
return nil
} catch {
log.error("failed to delete network \(network.id): \(error)")
return network
}
}
}
for try await network in group {
guard let network else {
continue
}
failed.append(network.id)
}
}
if failed.count > 0 {
throw ContainerizationError(.internalError, message: "delete failed for one or more networks: \(failed)")
}
}
}
}
+44
View File
@@ -0,0 +1,44 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import ArgumentParser
import ContainerClient
import ContainerNetworkService
import Foundation
import SwiftProtobuf
extension Application {
struct NetworkInspect: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "inspect",
abstract: "Display information about one or more networks")
@OptionGroup
var global: Flags.Global
@Argument(help: "Networks to inspect")
var networks: [String]
func run() async throws {
let objects: [any Codable] = try await ClientNetwork.list().filter {
networks.contains($0.id)
}.map {
PrintableNetwork($0)
}
print(try objects.jsonArray())
}
}
}
+107
View File
@@ -0,0 +1,107 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import ArgumentParser
import ContainerClient
import ContainerNetworkService
import ContainerizationExtras
import Foundation
import SwiftProtobuf
extension Application {
struct NetworkList: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "list",
abstract: "List networks",
aliases: ["ls"])
@Flag(name: .shortAndLong, help: "Only output the network name")
var quiet = false
@Option(name: .long, help: "Format of the output")
var format: ListFormat = .table
@OptionGroup
var global: Flags.Global
func run() async throws {
let networks = try await ClientNetwork.list()
try printNetworks(networks: networks, format: format)
}
private func createHeader() -> [[String]] {
[["NETWORK", "STATE", "SUBNET"]]
}
private func printNetworks(networks: [NetworkState], format: ListFormat) throws {
if format == .json {
let printables = networks.map {
PrintableNetwork($0)
}
let data = try JSONEncoder().encode(printables)
print(String(data: data, encoding: .utf8)!)
return
}
if self.quiet {
networks.forEach {
print($0.id)
}
return
}
var rows = createHeader()
for network in networks {
rows.append(network.asRow)
}
let formatter = TableOutput(rows: rows)
print(formatter.format())
}
}
}
extension NetworkState {
var asRow: [String] {
switch self {
case .created(_):
return [self.id, self.state, "none"]
case .running(_, let status):
return [self.id, self.state, status.address]
}
}
}
struct PrintableNetwork: Codable {
let id: String
let state: String
let config: NetworkConfiguration
let status: NetworkStatus?
init(_ network: NetworkState) {
self.id = network.id
self.state = network.state
switch network {
case .created(let config):
self.config = config
self.status = nil
case .running(let config, let status):
self.config = config
self.status = status
}
}
}
+1 -1
View File
@@ -61,7 +61,7 @@ extension Application {
try await client.ping()
} catch let err as RegistryClient.Error {
switch err {
case .invalidStatus(url: _, .unauthorized), .invalidStatus(url: _, .forbidden):
case .invalidStatus(url: _, .unauthorized, _), .invalidStatus(url: _, .forbidden, _):
break
default:
throw err
+4
View File
@@ -15,6 +15,7 @@
//===----------------------------------------------------------------------===//
import ArgumentParser
import ContainerizationError
import Foundation
public struct Flags {
@@ -121,6 +122,9 @@ public struct Flags {
})
public var kernel: String?
@Option(name: [.customLong("network")], help: "Attach the container to a network")
public var networks: [String] = []
@Option(name: .customLong("cidfile"), help: "Write the container ID to the path provided")
public var cidfile = ""
+19 -5
View File
@@ -150,14 +150,28 @@ public struct Utility {
mounts.append(contentsOf: volumes)
config.mounts = mounts
let network = try await ClientNetwork.get(id: ClientNetwork.defaultNetworkName)
guard case .running(_, let networkStatus) = network else {
throw ContainerizationError(.invalidState, message: "default network is not running")
if management.networks.isEmpty {
config.networks = [ClientNetwork.defaultNetworkName]
} else {
// networks may only be specified for macOS 26+
guard #available(macOS 26, *) else {
throw ContainerizationError(.invalidArgument, message: "non-default network configuration requires macOS 26 or newer")
}
config.networks = management.networks
}
var networkStatuses: [NetworkStatus] = []
for networkName in config.networks {
let network: NetworkState = try await ClientNetwork.get(id: networkName)
guard case .running(_, let networkStatus) = network else {
throw ContainerizationError(.invalidState, message: "network \(networkName) is not running")
}
networkStatuses.append(networkStatus)
}
let nameservers: [String]
config.networks = [network.id]
if management.dnsNameservers.isEmpty {
let subnet = try CIDRAddress(networkStatus.address)
let subnet = try CIDRAddress(networkStatuses[0].address)
let nameserver = IPv4Address(fromValue: subnet.lower.value + 1).description
nameservers = [nameserver]
} else {
@@ -23,7 +23,8 @@ import Containerization
/// container to container networking, but it is the only approach that
/// works for macOS Sequoia.
struct IsolatedInterfaceStrategy: InterfaceStrategy {
public func toInterface(attachment: Attachment, additionalData: XPCMessage?) -> Interface {
NATInterface(address: attachment.address, gateway: attachment.gateway)
public func toInterface(attachment: Attachment, interfaceIndex: Int, additionalData: XPCMessage?) -> Interface {
let gateway = interfaceIndex == 0 ? attachment.gateway : nil
return NATInterface(address: attachment.address, gateway: gateway)
}
}
@@ -32,7 +32,7 @@ struct NonisolatedInterfaceStrategy: InterfaceStrategy {
self.log = log
}
public func toInterface(attachment: Attachment, additionalData: XPCMessage?) throws -> Interface {
public func toInterface(attachment: Attachment, interfaceIndex: Int, additionalData: XPCMessage?) throws -> Interface {
guard let additionalData else {
throw ContainerizationError(.invalidState, message: "network state does not contain custom network reference")
}
@@ -43,6 +43,7 @@ struct NonisolatedInterfaceStrategy: InterfaceStrategy {
}
log.info("creating NATNetworkInterface with network reference")
return NATNetworkInterface(address: attachment.address, gateway: attachment.gateway, reference: networkRef)
let gateway = interfaceIndex == 0 ? attachment.gateway : nil
return NATNetworkInterface(address: attachment.address, gateway: gateway, reference: networkRef)
}
}
@@ -170,7 +170,7 @@ extension ImagesService {
do {
return try await body(authentication)
} catch let err as RegistryClient.Error {
guard case .invalidStatus(_, let status) = err else {
guard case .invalidStatus(_, let status, _) = err else {
throw err
}
guard status == .unauthorized || status == .forbidden else {
@@ -25,9 +25,10 @@ public protocol InterfaceStrategy: Sendable {
/// - Parameters:
/// - attachment: General attachment information that is common
/// for all networks.
/// - interfaceIndex: The zero-based index of the interface.
/// - additionalData: If present, attachment information that is
/// specific for the network to which the container will attach.
///
/// - Returns: An XPC message with no parameters.
func toInterface(attachment: Attachment, additionalData: XPCMessage?) throws -> Interface
func toInterface(attachment: Attachment, interfaceIndex: Int, additionalData: XPCMessage?) throws -> Interface
}
@@ -114,7 +114,7 @@ public actor SandboxService {
let hostname = index == 0 ? fqdn : config.id
let (attachment, additionalData) = try await client.allocate(hostname: hostname)
attachments.append(attachment)
let interface = try self.interfaceStrategy.toInterface(attachment: attachment, additionalData: additionalData)
let interface = try self.interfaceStrategy.toInterface(attachment: attachment, interfaceIndex: index, additionalData: additionalData)
container.interfaces.append(interface)
}
@@ -0,0 +1,99 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
//
import AsyncHTTPClient
import ContainerClient
import ContainerizationExtras
import ContainerizationOS
import Foundation
import Testing
class TestCLINetwork: CLITest {
private static let retries = 10
private static let retryDelaySeconds = Int64(3)
@available(macOS 26, *)
@Test func testNetworkCreateAndUse() async throws {
do {
let name = Test.current!.name.trimmingCharacters(in: ["(", ")"])
let networkDeleteArgs = ["network", "delete", name]
_ = try? run(arguments: networkDeleteArgs)
let networkCreateArgs = ["network", "create", name]
let result = try run(arguments: networkCreateArgs)
if result.status != 0 {
throw CLIError.executionFailed("command failed: \(result.error)")
}
defer {
_ = try? run(arguments: networkDeleteArgs)
}
let port = UInt16.random(in: 50000..<60000)
try doLongRun(
name: name,
image: "docker.io/library/python:latest",
args: ["--network", name],
containerArgs: ["python3", "-m", "http.server", "--bind", "0.0.0.0", "\(port)"])
defer {
try? doStop(name: name)
}
let container = try inspectContainer(name)
#expect(container.networks.count > 0)
let cidrAddress = try CIDRAddress(container.networks[0].address)
let url = "http://\(cidrAddress.address):\(port)"
var request = HTTPClientRequest(url: url)
request.method = .GET
let client = getClient()
defer { _ = client.shutdown() }
var retriesRemaining = Self.retries
var success = false
while !success && retriesRemaining > 0 {
do {
let response = try await client.execute(request, timeout: .seconds(Self.retryDelaySeconds))
try #require(response.status == .ok)
success = true
} catch {
print("request to \(url) failed, error \(error)")
try await Task.sleep(for: .seconds(Self.retryDelaySeconds))
}
retriesRemaining -= 1
}
#expect(success, "Request to \(url) failed after \(Self.retries - retriesRemaining) retries")
try doStop(name: name)
} catch {
Issue.record("failed to run container \(error)")
return
}
}
private func getClient() -> HTTPClient {
var httpConfiguration = HTTPClient.Configuration()
let proxyConfig: HTTPClient.Configuration.Proxy? = {
let proxyEnv = ProcessInfo.processInfo.environment["HTTP_PROXY"]
guard let proxyEnv else {
return nil
}
guard let url = URL(string: proxyEnv), let host = url.host(), let port = url.port else {
return nil
}
return .server(host: host, port: port)
}()
httpConfiguration.proxy = proxyConfig
return HTTPClient(eventLoopGroupProvider: .singleton, configuration: httpConfiguration)
}
}
+2
View File
@@ -17,6 +17,7 @@
//
import ContainerClient
import ContainerNetworkService
import Containerization
import ContainerizationOS
import Foundation
@@ -250,6 +251,7 @@ class CLITest {
struct inspectOutput: Codable {
let status: String
let configuration: ContainerConfiguration
let networks: [ContainerNetworkService.Attachment]
}
func getContainerStatus(_ name: String) throws -> String {
+49 -1
View File
@@ -145,12 +145,60 @@ Use the `list` command with the `--format` option to display information for all
]
</pre>
## Create and use a separate isolated network
> [!NOTE]
> This feature is available on macOS 26 and later.
Running `container system start` creates a vmnet network named `default` to which your containers will attach unless you specify otherwise.
You can create a separate isolated network using `container network create`.
This command creates a network named `foo`:
```bash
container network create foo
```
The `foo` network, the default network, and any other networks you create are isolated from one another. A container on one network has no connectivity to containers on other networks.
Run `container network list` to see the networks that exist:
```console
% container network list
NETWORK STATE SUBNET
default running 192.168.64.0/24
foo running 192.168.65.0/24
%
```
Run a container that is attached to that network using the `--network` flag:
```console
container run -d --name my-web-server --network foo --rm web-test
```
Use `container ls` to see that the container is on the `foo` subnet:
```console
% container ls
ID IMAGE OS ARCH STATE ADDR
my-web-server web-test:latest linux arm64 running 192.168.65.2
```
You can delete networks that you create once no containers are attached:
```bash
container stop my-web-server
container network delete foo
```
## View container logs
The `container logs` command displays the output from your containerized application:
<pre>
% container run -d --dns-domain test --name my-web-server --rm registry.example.com/fido/web-test:latest
% container run -d --name my-web-server --rm registry.example.com/fido/web-test:latest
my-web-server
% curl http://my-web-server.test
&lt;!DOCTYPE html>&lt;html>&lt;head>&lt;title>Hello&lt;/title>&lt;/head>&lt;body>&lt;h1>Hello, world!&lt;/h1>&lt;/body>&lt;/html>
+4
View File
@@ -73,6 +73,10 @@ Currently, memory pages freed to the Linux operating system by processes running
The vmnet framework in macOS 15 can only provide networks where the attached containers are isolated from one another. Container-to-container communication over the virtual network is not possible.
#### Multiple networks
In macOS 15, all containers attach to the default vmnet network. The `container network` commands are not available on macOS 15, and using the `--network` option for `container run` or `container create` will result in an error.
#### Container IP addresses
In macOS 15, limitations in the vmnet framework mean that the container network can only be created when the first container starts. Since the network XPC helper provides IP addresses to containers, and the helper has to start before the first container, it is possible for the network helper and vmnet to disagree on the subnet address, resulting in containers that are completely cut off from the network.