Fix: Add explicit host entries to container configuration - #1340

Open
mazdak wants to merge 4 commits into
apple:mainfrom
mazdak:pr2/container-host-entries
Open

Fix: Add explicit host entries to container configuration#1340
mazdak wants to merge 4 commits into
apple:mainfrom
mazdak:pr2/container-host-entries

Conversation

@mazdak

@mazdakmazdak commented Mar 22, 2026

Copy link
Copy Markdown
Contributor

Type of Change

  • Bug fix

Motivation and Context

While building a Docker Compose-like plugin for container and validating it against our real Docker Compose workload, we hit a core limitation: there was no way for callers to ask the runtime to append explicit entries to a container's /etc/hosts.

That showed up most clearly with Compose extra_hosts, especially the common host.docker.internal pattern. The plugin could parse those mappings, but there was no core field to carry them into the sandbox, so the runtime always generated only the default localhost/container-name entries.

In practice, containers that depended on host aliases still failed name resolution even though the compose file specified them.

Why this belongs in core

This is not something the plugin can fake safely. /etc/hosts is generated in the sandbox layer, so callers need a first-class way to provide additional host entries to the runtime.

What this changes

  • add ContainerConfiguration.HostEntry
  • add ContainerConfiguration.hosts
  • preserve those entries through configuration encoding/decoding
  • extend sandbox host generation to append caller-provided host entries after the default localhost and primary hostname entries
  • add focused tests for round-tripping and host resolution behavior

Testing

  • Tested locally
  • Added/updated tests

var hosts = [ContainerConfiguration.HostEntry(ipAddress: "127.0.0.1", hostnames: ["localhost"])]

if let primaryAddress {
let ip = String(primaryAddress.split(separator: "/")[0])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we splitting by / here?

public struct IPv4Address {
@inlinable
public var description: String {
"\(bytes[0]).\(bytes[1]).\(bytes[2]).\(bytes[3])"
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are splitting by / here because the value coming in as primaryAddress is not a plain IP address — it is a CIDR notation string (e.g. "192.168.1.45/24").
The /24 part is the subnet mask. We only want the actual IP address (192.168.1.45), so we split on the / and take the first part.
Cleaned-up & safer version of that function:

extension SandboxService {
static func resolvedHosts(
hostname: String,
primaryAddress: String?,
extraHosts: [ContainerConfiguration.HostEntry]
) -> [ContainerConfiguration.HostEntry] {

 var hosts: [ContainerConfiguration.HostEntry] = [
ContainerConfiguration.HostEntry(ipAddress: "127.0.0.1", hostnames: ["localhost"])
]
if let primaryAddress {
// Split off the CIDR suffix if present (e.g. "192.168.1.45/24" → "192.168.1.45")
let ipOnly = primaryAddress.split(separator: "/").first.map(String.init) ?? primaryAddress
hosts.append(
ContainerConfiguration.HostEntry(
ipAddress: ipOnly,
hostnames: [hostname]
)
)
}
// Add any extra hosts passed in
hosts.append(contentsOf: extraHosts)
return hosts
}

}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we splitting by / here?

public struct IPv4Address {
@inlinable
public var description: String {
"\(bytes[0]).\(bytes[1]).\(bytes[2]).\(bytes[3])"
}
}

Also with the way I am running low latency it must be a steady string no breaking if possible.! I can show you diagrams or math that proves it.

@JaewonHur

Copy link
Copy Markdown
Contributor

It'd be good to have a follow up PR that wires this to CLI (e.g., --add-host [name:ip] in Docker).

@JaewonHur

Copy link
Copy Markdown
Contributor

@mazdak Hi! Could you do make fmt and push again? Sorry for late! We are going to merge this.

@mazdak

Copy link
Copy Markdown
ContributorAuthor

@mazdak Hi! Could you do make fmt and push again? Sorry for late! We are going to merge this.

Apologies for the late reply. Done.

@mazdak
mazdak requested a review from JaewonHurJune 2, 2026 00:12
@stephenlclarke

Copy link
Copy Markdown

Thanks for implementing this as a data-shape-only PR. I reviewed the patch from the downstream stephenlclarke/container-compose side, where the direct need is Compose depends_on.condition: service_healthy.

This looks like the right first slice to me:

  • HealthStatus and optional ContainerSnapshot.health give API clients a stable type without forcing the observer design into this PR.
  • Keeping the value nil until a runtime observer exists is a useful compatibility boundary for older servers/newer clients and for current daemon behavior.
  • The split keeps Compose policy in stephenlclarke/container-compose, which is where service dependency waiting, selected services, and user-facing Compose errors should live.

I would suggest adding a small Codable compatibility test before merge. This PR is adding a new API field, even though the daemon does not populate it yet, so the important thing to protect is the wire/API shape:

  • old ContainerSnapshot JSON without health should still decode successfully;
  • all HealthStatus values should encode and decode correctly;
  • existing code that creates a ContainerSnapshot should still default health to nil.

I would also make the meaning of nil versus .none very clear. nil should mean "health information is not available from this daemon/snapshot." .none should mean "health information is available, and the container has no healthcheck or no result yet." That difference will matter for downstream tools like stephenlclarke/container-compose.

For follow-up work, I would keep the healthcheck pieces split into small PRs:

  1. add the health field and HealthStatus type (this PR);
  2. add a way to configure a container healthcheck;
  3. add the daemon code that runs the healthcheck and updates ContainerSnapshot.health;
  4. add container create / container run flags for healthchecks;
  5. add support for reading Dockerfile HEALTHCHECK settings from images.

I do not think those need to be included in this PR. This PR is easier to review if it only adds the shared API field. That gives stephenlclarke/container-compose and other clients something stable to build against, while keeping Compose-specific behaviour out of apple/container.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@mazdak@JaewonHur@stephenlclarke@vxnkjyffmq-code@katiewasnothere
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fix: Add explicit host entries to container configuration - #1340

Open
mazdak wants to merge 4 commits into
apple:mainfrom
mazdak:pr2/container-host-entries
Open

Fix: Add explicit host entries to container configuration#1340
mazdak wants to merge 4 commits into
apple:mainfrom
mazdak:pr2/container-host-entries

Conversation

@mazdak

@mazdakmazdak commented Mar 22, 2026

Copy link
Copy Markdown
Contributor

Type of Change

  • Bug fix

Motivation and Context

While building a Docker Compose-like plugin for container and validating it against our real Docker Compose workload, we hit a core limitation: there was no way for callers to ask the runtime to append explicit entries to a container's /etc/hosts.

That showed up most clearly with Compose extra_hosts, especially the common host.docker.internal pattern. The plugin could parse those mappings, but there was no core field to carry them into the sandbox, so the runtime always generated only the default localhost/container-name entries.

In practice, containers that depended on host aliases still failed name resolution even though the compose file specified them.

Why this belongs in core

This is not something the plugin can fake safely. /etc/hosts is generated in the sandbox layer, so callers need a first-class way to provide additional host entries to the runtime.

What this changes

  • add ContainerConfiguration.HostEntry
  • add ContainerConfiguration.hosts
  • preserve those entries through configuration encoding/decoding
  • extend sandbox host generation to append caller-provided host entries after the default localhost and primary hostname entries
  • add focused tests for round-tripping and host resolution behavior

Testing

  • Tested locally
  • Added/updated tests

var hosts = [ContainerConfiguration.HostEntry(ipAddress: "127.0.0.1", hostnames: ["localhost"])]

if let primaryAddress {
let ip = String(primaryAddress.split(separator: "/")[0])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we splitting by / here?

public struct IPv4Address {
@inlinable
public var description: String {
"\(bytes[0]).\(bytes[1]).\(bytes[2]).\(bytes[3])"
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are splitting by / here because the value coming in as primaryAddress is not a plain IP address — it is a CIDR notation string (e.g. "192.168.1.45/24").
The /24 part is the subnet mask. We only want the actual IP address (192.168.1.45), so we split on the / and take the first part.
Cleaned-up & safer version of that function:

extension SandboxService {
static func resolvedHosts(
hostname: String,
primaryAddress: String?,
extraHosts: [ContainerConfiguration.HostEntry]
) -> [ContainerConfiguration.HostEntry] {

 var hosts: [ContainerConfiguration.HostEntry] = [
ContainerConfiguration.HostEntry(ipAddress: "127.0.0.1", hostnames: ["localhost"])
]
if let primaryAddress {
// Split off the CIDR suffix if present (e.g. "192.168.1.45/24" → "192.168.1.45")
let ipOnly = primaryAddress.split(separator: "/").first.map(String.init) ?? primaryAddress
hosts.append(
ContainerConfiguration.HostEntry(
ipAddress: ipOnly,
hostnames: [hostname]
)
)
}
// Add any extra hosts passed in
hosts.append(contentsOf: extraHosts)
return hosts
}

}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we splitting by / here?

public struct IPv4Address {
@inlinable
public var description: String {
"\(bytes[0]).\(bytes[1]).\(bytes[2]).\(bytes[3])"
}
}

Also with the way I am running low latency it must be a steady string no breaking if possible.! I can show you diagrams or math that proves it.

@JaewonHur

Copy link
Copy Markdown
Contributor

It'd be good to have a follow up PR that wires this to CLI (e.g., --add-host [name:ip] in Docker).

@JaewonHur

Copy link
Copy Markdown
Contributor

@mazdak Hi! Could you do make fmt and push again? Sorry for late! We are going to merge this.

@mazdak

Copy link
Copy Markdown
ContributorAuthor

@mazdak Hi! Could you do make fmt and push again? Sorry for late! We are going to merge this.

Apologies for the late reply. Done.

@mazdak
mazdak requested a review from JaewonHurJune 2, 2026 00:12
@stephenlclarke

Copy link
Copy Markdown

Thanks for implementing this as a data-shape-only PR. I reviewed the patch from the downstream stephenlclarke/container-compose side, where the direct need is Compose depends_on.condition: service_healthy.

This looks like the right first slice to me:

  • HealthStatus and optional ContainerSnapshot.health give API clients a stable type without forcing the observer design into this PR.
  • Keeping the value nil until a runtime observer exists is a useful compatibility boundary for older servers/newer clients and for current daemon behavior.
  • The split keeps Compose policy in stephenlclarke/container-compose, which is where service dependency waiting, selected services, and user-facing Compose errors should live.

I would suggest adding a small Codable compatibility test before merge. This PR is adding a new API field, even though the daemon does not populate it yet, so the important thing to protect is the wire/API shape:

  • old ContainerSnapshot JSON without health should still decode successfully;
  • all HealthStatus values should encode and decode correctly;
  • existing code that creates a ContainerSnapshot should still default health to nil.

I would also make the meaning of nil versus .none very clear. nil should mean "health information is not available from this daemon/snapshot." .none should mean "health information is available, and the container has no healthcheck or no result yet." That difference will matter for downstream tools like stephenlclarke/container-compose.

For follow-up work, I would keep the healthcheck pieces split into small PRs:

  1. add the health field and HealthStatus type (this PR);
  2. add a way to configure a container healthcheck;
  3. add the daemon code that runs the healthcheck and updates ContainerSnapshot.health;
  4. add container create / container run flags for healthchecks;
  5. add support for reading Dockerfile HEALTHCHECK settings from images.

I do not think those need to be included in this PR. This PR is easier to review if it only adds the shared API field. That gives stephenlclarke/container-compose and other clients something stable to build against, while keeping Compose-specific behaviour out of apple/container.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@mazdak@JaewonHur@stephenlclarke@vxnkjyffmq-code@katiewasnothere
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix: Add explicit host entries to container configuration - #1340

Open
mazdak wants to merge 4 commits into
apple:mainfrom
mazdak:pr2/container-host-entries
Open

Fix: Add explicit host entries to container configuration#1340
mazdak wants to merge 4 commits into
apple:mainfrom
mazdak:pr2/container-host-entries

Conversation

@mazdak

@mazdakmazdak commented Mar 22, 2026

Copy link
Copy Markdown
Contributor

Type of Change

  • Bug fix

Motivation and Context

While building a Docker Compose-like plugin for container and validating it against our real Docker Compose workload, we hit a core limitation: there was no way for callers to ask the runtime to append explicit entries to a container's /etc/hosts.

That showed up most clearly with Compose extra_hosts, especially the common host.docker.internal pattern. The plugin could parse those mappings, but there was no core field to carry them into the sandbox, so the runtime always generated only the default localhost/container-name entries.

In practice, containers that depended on host aliases still failed name resolution even though the compose file specified them.

Why this belongs in core

This is not something the plugin can fake safely. /etc/hosts is generated in the sandbox layer, so callers need a first-class way to provide additional host entries to the runtime.

What this changes

  • add ContainerConfiguration.HostEntry
  • add ContainerConfiguration.hosts
  • preserve those entries through configuration encoding/decoding
  • extend sandbox host generation to append caller-provided host entries after the default localhost and primary hostname entries
  • add focused tests for round-tripping and host resolution behavior

Testing

  • Tested locally
  • Added/updated tests

var hosts = [ContainerConfiguration.HostEntry(ipAddress: "127.0.0.1", hostnames: ["localhost"])]

if let primaryAddress {
let ip = String(primaryAddress.split(separator: "/")[0])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we splitting by / here?

public struct IPv4Address {
@inlinable
public var description: String {
"\(bytes[0]).\(bytes[1]).\(bytes[2]).\(bytes[3])"
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are splitting by / here because the value coming in as primaryAddress is not a plain IP address — it is a CIDR notation string (e.g. "192.168.1.45/24").
The /24 part is the subnet mask. We only want the actual IP address (192.168.1.45), so we split on the / and take the first part.
Cleaned-up & safer version of that function:

extension SandboxService {
static func resolvedHosts(
hostname: String,
primaryAddress: String?,
extraHosts: [ContainerConfiguration.HostEntry]
) -> [ContainerConfiguration.HostEntry] {

 var hosts: [ContainerConfiguration.HostEntry] = [
ContainerConfiguration.HostEntry(ipAddress: "127.0.0.1", hostnames: ["localhost"])
]
if let primaryAddress {
// Split off the CIDR suffix if present (e.g. "192.168.1.45/24" → "192.168.1.45")
let ipOnly = primaryAddress.split(separator: "/").first.map(String.init) ?? primaryAddress
hosts.append(
ContainerConfiguration.HostEntry(
ipAddress: ipOnly,
hostnames: [hostname]
)
)
}
// Add any extra hosts passed in
hosts.append(contentsOf: extraHosts)
return hosts
}

}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we splitting by / here?

public struct IPv4Address {
@inlinable
public var description: String {
"\(bytes[0]).\(bytes[1]).\(bytes[2]).\(bytes[3])"
}
}

Also with the way I am running low latency it must be a steady string no breaking if possible.! I can show you diagrams or math that proves it.

@JaewonHur

Copy link
Copy Markdown
Contributor

It'd be good to have a follow up PR that wires this to CLI (e.g., --add-host [name:ip] in Docker).

@JaewonHur

Copy link
Copy Markdown
Contributor

@mazdak Hi! Could you do make fmt and push again? Sorry for late! We are going to merge this.

@mazdak

Copy link
Copy Markdown
ContributorAuthor

@mazdak Hi! Could you do make fmt and push again? Sorry for late! We are going to merge this.

Apologies for the late reply. Done.

@mazdak
mazdak requested a review from JaewonHurJune 2, 2026 00:12
@stephenlclarke

Copy link
Copy Markdown

Thanks for implementing this as a data-shape-only PR. I reviewed the patch from the downstream stephenlclarke/container-compose side, where the direct need is Compose depends_on.condition: service_healthy.

This looks like the right first slice to me:

  • HealthStatus and optional ContainerSnapshot.health give API clients a stable type without forcing the observer design into this PR.
  • Keeping the value nil until a runtime observer exists is a useful compatibility boundary for older servers/newer clients and for current daemon behavior.
  • The split keeps Compose policy in stephenlclarke/container-compose, which is where service dependency waiting, selected services, and user-facing Compose errors should live.

I would suggest adding a small Codable compatibility test before merge. This PR is adding a new API field, even though the daemon does not populate it yet, so the important thing to protect is the wire/API shape:

  • old ContainerSnapshot JSON without health should still decode successfully;
  • all HealthStatus values should encode and decode correctly;
  • existing code that creates a ContainerSnapshot should still default health to nil.

I would also make the meaning of nil versus .none very clear. nil should mean "health information is not available from this daemon/snapshot." .none should mean "health information is available, and the container has no healthcheck or no result yet." That difference will matter for downstream tools like stephenlclarke/container-compose.

For follow-up work, I would keep the healthcheck pieces split into small PRs:

  1. add the health field and HealthStatus type (this PR);
  2. add a way to configure a container healthcheck;
  3. add the daemon code that runs the healthcheck and updates ContainerSnapshot.health;
  4. add container create / container run flags for healthchecks;
  5. add support for reading Dockerfile HEALTHCHECK settings from images.

I do not think those need to be included in this PR. This PR is easier to review if it only adds the shared API field. That gives stephenlclarke/container-compose and other clients something stable to build against, while keeping Compose-specific behaviour out of apple/container.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@mazdak@JaewonHur@stephenlclarke@vxnkjyffmq-code@katiewasnothere
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix: Add explicit host entries to container configuration - #1340

Open
mazdak wants to merge 4 commits into
apple:mainfrom
mazdak:pr2/container-host-entries
Open

Fix: Add explicit host entries to container configuration#1340
mazdak wants to merge 4 commits into
apple:mainfrom
mazdak:pr2/container-host-entries

Conversation

@mazdak

@mazdakmazdak commented Mar 22, 2026

Copy link
Copy Markdown
Contributor

Type of Change

  • Bug fix

Motivation and Context

While building a Docker Compose-like plugin for container and validating it against our real Docker Compose workload, we hit a core limitation: there was no way for callers to ask the runtime to append explicit entries to a container's /etc/hosts.

That showed up most clearly with Compose extra_hosts, especially the common host.docker.internal pattern. The plugin could parse those mappings, but there was no core field to carry them into the sandbox, so the runtime always generated only the default localhost/container-name entries.

In practice, containers that depended on host aliases still failed name resolution even though the compose file specified them.

Why this belongs in core

This is not something the plugin can fake safely. /etc/hosts is generated in the sandbox layer, so callers need a first-class way to provide additional host entries to the runtime.

What this changes

  • add ContainerConfiguration.HostEntry
  • add ContainerConfiguration.hosts
  • preserve those entries through configuration encoding/decoding
  • extend sandbox host generation to append caller-provided host entries after the default localhost and primary hostname entries
  • add focused tests for round-tripping and host resolution behavior

Testing

  • Tested locally
  • Added/updated tests

var hosts = [ContainerConfiguration.HostEntry(ipAddress: "127.0.0.1", hostnames: ["localhost"])]

if let primaryAddress {
let ip = String(primaryAddress.split(separator: "/")[0])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we splitting by / here?

public struct IPv4Address {
@inlinable
public var description: String {
"\(bytes[0]).\(bytes[1]).\(bytes[2]).\(bytes[3])"
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are splitting by / here because the value coming in as primaryAddress is not a plain IP address — it is a CIDR notation string (e.g. "192.168.1.45/24").
The /24 part is the subnet mask. We only want the actual IP address (192.168.1.45), so we split on the / and take the first part.
Cleaned-up & safer version of that function:

extension SandboxService {
static func resolvedHosts(
hostname: String,
primaryAddress: String?,
extraHosts: [ContainerConfiguration.HostEntry]
) -> [ContainerConfiguration.HostEntry] {

 var hosts: [ContainerConfiguration.HostEntry] = [
ContainerConfiguration.HostEntry(ipAddress: "127.0.0.1", hostnames: ["localhost"])
]
if let primaryAddress {
// Split off the CIDR suffix if present (e.g. "192.168.1.45/24" → "192.168.1.45")
let ipOnly = primaryAddress.split(separator: "/").first.map(String.init) ?? primaryAddress
hosts.append(
ContainerConfiguration.HostEntry(
ipAddress: ipOnly,
hostnames: [hostname]
)
)
}
// Add any extra hosts passed in
hosts.append(contentsOf: extraHosts)
return hosts
}

}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we splitting by / here?

public struct IPv4Address {
@inlinable
public var description: String {
"\(bytes[0]).\(bytes[1]).\(bytes[2]).\(bytes[3])"
}
}

Also with the way I am running low latency it must be a steady string no breaking if possible.! I can show you diagrams or math that proves it.

@JaewonHur

Copy link
Copy Markdown
Contributor

It'd be good to have a follow up PR that wires this to CLI (e.g., --add-host [name:ip] in Docker).

@JaewonHur

Copy link
Copy Markdown
Contributor

@mazdak Hi! Could you do make fmt and push again? Sorry for late! We are going to merge this.

@mazdak

Copy link
Copy Markdown
ContributorAuthor

@mazdak Hi! Could you do make fmt and push again? Sorry for late! We are going to merge this.

Apologies for the late reply. Done.

@mazdak
mazdak requested a review from JaewonHurJune 2, 2026 00:12
@stephenlclarke

Copy link
Copy Markdown

Thanks for implementing this as a data-shape-only PR. I reviewed the patch from the downstream stephenlclarke/container-compose side, where the direct need is Compose depends_on.condition: service_healthy.

This looks like the right first slice to me:

  • HealthStatus and optional ContainerSnapshot.health give API clients a stable type without forcing the observer design into this PR.
  • Keeping the value nil until a runtime observer exists is a useful compatibility boundary for older servers/newer clients and for current daemon behavior.
  • The split keeps Compose policy in stephenlclarke/container-compose, which is where service dependency waiting, selected services, and user-facing Compose errors should live.

I would suggest adding a small Codable compatibility test before merge. This PR is adding a new API field, even though the daemon does not populate it yet, so the important thing to protect is the wire/API shape:

  • old ContainerSnapshot JSON without health should still decode successfully;
  • all HealthStatus values should encode and decode correctly;
  • existing code that creates a ContainerSnapshot should still default health to nil.

I would also make the meaning of nil versus .none very clear. nil should mean "health information is not available from this daemon/snapshot." .none should mean "health information is available, and the container has no healthcheck or no result yet." That difference will matter for downstream tools like stephenlclarke/container-compose.

For follow-up work, I would keep the healthcheck pieces split into small PRs:

  1. add the health field and HealthStatus type (this PR);
  2. add a way to configure a container healthcheck;
  3. add the daemon code that runs the healthcheck and updates ContainerSnapshot.health;
  4. add container create / container run flags for healthchecks;
  5. add support for reading Dockerfile HEALTHCHECK settings from images.

I do not think those need to be included in this PR. This PR is easier to review if it only adds the shared API field. That gives stephenlclarke/container-compose and other clients something stable to build against, while keeping Compose-specific behaviour out of apple/container.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@mazdak@JaewonHur@stephenlclarke@vxnkjyffmq-code@katiewasnothere
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fix: Add explicit host entries to container configuration - #1340

Open
mazdak wants to merge 4 commits into
apple:mainfrom
mazdak:pr2/container-host-entries
Open

Fix: Add explicit host entries to container configuration#1340
mazdak wants to merge 4 commits into
apple:mainfrom
mazdak:pr2/container-host-entries

Conversation

@mazdak

@mazdakmazdak commented Mar 22, 2026

Copy link
Copy Markdown
Contributor

Type of Change

  • Bug fix

Motivation and Context

While building a Docker Compose-like plugin for container and validating it against our real Docker Compose workload, we hit a core limitation: there was no way for callers to ask the runtime to append explicit entries to a container's /etc/hosts.

That showed up most clearly with Compose extra_hosts, especially the common host.docker.internal pattern. The plugin could parse those mappings, but there was no core field to carry them into the sandbox, so the runtime always generated only the default localhost/container-name entries.

In practice, containers that depended on host aliases still failed name resolution even though the compose file specified them.

Why this belongs in core

This is not something the plugin can fake safely. /etc/hosts is generated in the sandbox layer, so callers need a first-class way to provide additional host entries to the runtime.

What this changes

  • add ContainerConfiguration.HostEntry
  • add ContainerConfiguration.hosts
  • preserve those entries through configuration encoding/decoding
  • extend sandbox host generation to append caller-provided host entries after the default localhost and primary hostname entries
  • add focused tests for round-tripping and host resolution behavior

Testing

  • Tested locally
  • Added/updated tests

var hosts = [ContainerConfiguration.HostEntry(ipAddress: "127.0.0.1", hostnames: ["localhost"])]

if let primaryAddress {
let ip = String(primaryAddress.split(separator: "/")[0])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we splitting by / here?

public struct IPv4Address {
@inlinable
public var description: String {
"\(bytes[0]).\(bytes[1]).\(bytes[2]).\(bytes[3])"
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are splitting by / here because the value coming in as primaryAddress is not a plain IP address — it is a CIDR notation string (e.g. "192.168.1.45/24").
The /24 part is the subnet mask. We only want the actual IP address (192.168.1.45), so we split on the / and take the first part.
Cleaned-up & safer version of that function:

extension SandboxService {
static func resolvedHosts(
hostname: String,
primaryAddress: String?,
extraHosts: [ContainerConfiguration.HostEntry]
) -> [ContainerConfiguration.HostEntry] {

 var hosts: [ContainerConfiguration.HostEntry] = [
ContainerConfiguration.HostEntry(ipAddress: "127.0.0.1", hostnames: ["localhost"])
]
if let primaryAddress {
// Split off the CIDR suffix if present (e.g. "192.168.1.45/24" → "192.168.1.45")
let ipOnly = primaryAddress.split(separator: "/").first.map(String.init) ?? primaryAddress
hosts.append(
ContainerConfiguration.HostEntry(
ipAddress: ipOnly,
hostnames: [hostname]
)
)
}
// Add any extra hosts passed in
hosts.append(contentsOf: extraHosts)
return hosts
}

}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we splitting by / here?

public struct IPv4Address {
@inlinable
public var description: String {
"\(bytes[0]).\(bytes[1]).\(bytes[2]).\(bytes[3])"
}
}

Also with the way I am running low latency it must be a steady string no breaking if possible.! I can show you diagrams or math that proves it.

@JaewonHur

Copy link
Copy Markdown
Contributor

It'd be good to have a follow up PR that wires this to CLI (e.g., --add-host [name:ip] in Docker).

@JaewonHur

Copy link
Copy Markdown
Contributor

@mazdak Hi! Could you do make fmt and push again? Sorry for late! We are going to merge this.

@mazdak

Copy link
Copy Markdown
ContributorAuthor

@mazdak Hi! Could you do make fmt and push again? Sorry for late! We are going to merge this.

Apologies for the late reply. Done.

@mazdak
mazdak requested a review from JaewonHurJune 2, 2026 00:12
@stephenlclarke

Copy link
Copy Markdown

Thanks for implementing this as a data-shape-only PR. I reviewed the patch from the downstream stephenlclarke/container-compose side, where the direct need is Compose depends_on.condition: service_healthy.

This looks like the right first slice to me:

  • HealthStatus and optional ContainerSnapshot.health give API clients a stable type without forcing the observer design into this PR.
  • Keeping the value nil until a runtime observer exists is a useful compatibility boundary for older servers/newer clients and for current daemon behavior.
  • The split keeps Compose policy in stephenlclarke/container-compose, which is where service dependency waiting, selected services, and user-facing Compose errors should live.

I would suggest adding a small Codable compatibility test before merge. This PR is adding a new API field, even though the daemon does not populate it yet, so the important thing to protect is the wire/API shape:

  • old ContainerSnapshot JSON without health should still decode successfully;
  • all HealthStatus values should encode and decode correctly;
  • existing code that creates a ContainerSnapshot should still default health to nil.

I would also make the meaning of nil versus .none very clear. nil should mean "health information is not available from this daemon/snapshot." .none should mean "health information is available, and the container has no healthcheck or no result yet." That difference will matter for downstream tools like stephenlclarke/container-compose.

For follow-up work, I would keep the healthcheck pieces split into small PRs:

  1. add the health field and HealthStatus type (this PR);
  2. add a way to configure a container healthcheck;
  3. add the daemon code that runs the healthcheck and updates ContainerSnapshot.health;
  4. add container create / container run flags for healthchecks;
  5. add support for reading Dockerfile HEALTHCHECK settings from images.

I do not think those need to be included in this PR. This PR is easier to review if it only adds the shared API field. That gives stephenlclarke/container-compose and other clients something stable to build against, while keeping Compose-specific behaviour out of apple/container.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@mazdak@JaewonHur@stephenlclarke@vxnkjyffmq-code@katiewasnothere
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix: Add explicit host entries to container configuration - #1340

Open
mazdak wants to merge 4 commits into
apple:mainfrom
mazdak:pr2/container-host-entries
Open

Fix: Add explicit host entries to container configuration#1340
mazdak wants to merge 4 commits into
apple:mainfrom
mazdak:pr2/container-host-entries

Conversation

@mazdak

@mazdakmazdak commented Mar 22, 2026

Copy link
Copy Markdown
Contributor

Type of Change

  • Bug fix

Motivation and Context

While building a Docker Compose-like plugin for container and validating it against our real Docker Compose workload, we hit a core limitation: there was no way for callers to ask the runtime to append explicit entries to a container's /etc/hosts.

That showed up most clearly with Compose extra_hosts, especially the common host.docker.internal pattern. The plugin could parse those mappings, but there was no core field to carry them into the sandbox, so the runtime always generated only the default localhost/container-name entries.

In practice, containers that depended on host aliases still failed name resolution even though the compose file specified them.

Why this belongs in core

This is not something the plugin can fake safely. /etc/hosts is generated in the sandbox layer, so callers need a first-class way to provide additional host entries to the runtime.

What this changes

  • add ContainerConfiguration.HostEntry
  • add ContainerConfiguration.hosts
  • preserve those entries through configuration encoding/decoding
  • extend sandbox host generation to append caller-provided host entries after the default localhost and primary hostname entries
  • add focused tests for round-tripping and host resolution behavior

Testing

  • Tested locally
  • Added/updated tests

var hosts = [ContainerConfiguration.HostEntry(ipAddress: "127.0.0.1", hostnames: ["localhost"])]

if let primaryAddress {
let ip = String(primaryAddress.split(separator: "/")[0])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we splitting by / here?

public struct IPv4Address {
@inlinable
public var description: String {
"\(bytes[0]).\(bytes[1]).\(bytes[2]).\(bytes[3])"
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are splitting by / here because the value coming in as primaryAddress is not a plain IP address — it is a CIDR notation string (e.g. "192.168.1.45/24").
The /24 part is the subnet mask. We only want the actual IP address (192.168.1.45), so we split on the / and take the first part.
Cleaned-up & safer version of that function:

extension SandboxService {
static func resolvedHosts(
hostname: String,
primaryAddress: String?,
extraHosts: [ContainerConfiguration.HostEntry]
) -> [ContainerConfiguration.HostEntry] {

 var hosts: [ContainerConfiguration.HostEntry] = [
ContainerConfiguration.HostEntry(ipAddress: "127.0.0.1", hostnames: ["localhost"])
]
if let primaryAddress {
// Split off the CIDR suffix if present (e.g. "192.168.1.45/24" → "192.168.1.45")
let ipOnly = primaryAddress.split(separator: "/").first.map(String.init) ?? primaryAddress
hosts.append(
ContainerConfiguration.HostEntry(
ipAddress: ipOnly,
hostnames: [hostname]
)
)
}
// Add any extra hosts passed in
hosts.append(contentsOf: extraHosts)
return hosts
}

}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we splitting by / here?

public struct IPv4Address {
@inlinable
public var description: String {
"\(bytes[0]).\(bytes[1]).\(bytes[2]).\(bytes[3])"
}
}

Also with the way I am running low latency it must be a steady string no breaking if possible.! I can show you diagrams or math that proves it.

@JaewonHur

Copy link
Copy Markdown
Contributor

It'd be good to have a follow up PR that wires this to CLI (e.g., --add-host [name:ip] in Docker).

@JaewonHur

Copy link
Copy Markdown
Contributor

@mazdak Hi! Could you do make fmt and push again? Sorry for late! We are going to merge this.

@mazdak

Copy link
Copy Markdown
ContributorAuthor

@mazdak Hi! Could you do make fmt and push again? Sorry for late! We are going to merge this.

Apologies for the late reply. Done.

@mazdak
mazdak requested a review from JaewonHurJune 2, 2026 00:12
@stephenlclarke

Copy link
Copy Markdown

Thanks for implementing this as a data-shape-only PR. I reviewed the patch from the downstream stephenlclarke/container-compose side, where the direct need is Compose depends_on.condition: service_healthy.

This looks like the right first slice to me:

  • HealthStatus and optional ContainerSnapshot.health give API clients a stable type without forcing the observer design into this PR.
  • Keeping the value nil until a runtime observer exists is a useful compatibility boundary for older servers/newer clients and for current daemon behavior.
  • The split keeps Compose policy in stephenlclarke/container-compose, which is where service dependency waiting, selected services, and user-facing Compose errors should live.

I would suggest adding a small Codable compatibility test before merge. This PR is adding a new API field, even though the daemon does not populate it yet, so the important thing to protect is the wire/API shape:

  • old ContainerSnapshot JSON without health should still decode successfully;
  • all HealthStatus values should encode and decode correctly;
  • existing code that creates a ContainerSnapshot should still default health to nil.

I would also make the meaning of nil versus .none very clear. nil should mean "health information is not available from this daemon/snapshot." .none should mean "health information is available, and the container has no healthcheck or no result yet." That difference will matter for downstream tools like stephenlclarke/container-compose.

For follow-up work, I would keep the healthcheck pieces split into small PRs:

  1. add the health field and HealthStatus type (this PR);
  2. add a way to configure a container healthcheck;
  3. add the daemon code that runs the healthcheck and updates ContainerSnapshot.health;
  4. add container create / container run flags for healthchecks;
  5. add support for reading Dockerfile HEALTHCHECK settings from images.

I do not think those need to be included in this PR. This PR is easier to review if it only adds the shared API field. That gives stephenlclarke/container-compose and other clients something stable to build against, while keeping Compose-specific behaviour out of apple/container.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@mazdak@JaewonHur@stephenlclarke@vxnkjyffmq-code@katiewasnothere
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix: Add explicit host entries to container configuration - #1340

Open
mazdak wants to merge 4 commits into
apple:mainfrom
mazdak:pr2/container-host-entries
Open

Fix: Add explicit host entries to container configuration#1340
mazdak wants to merge 4 commits into
apple:mainfrom
mazdak:pr2/container-host-entries

Conversation

@mazdak

@mazdakmazdak commented Mar 22, 2026

Copy link
Copy Markdown
Contributor

Type of Change

  • Bug fix

Motivation and Context

While building a Docker Compose-like plugin for container and validating it against our real Docker Compose workload, we hit a core limitation: there was no way for callers to ask the runtime to append explicit entries to a container's /etc/hosts.

That showed up most clearly with Compose extra_hosts, especially the common host.docker.internal pattern. The plugin could parse those mappings, but there was no core field to carry them into the sandbox, so the runtime always generated only the default localhost/container-name entries.

In practice, containers that depended on host aliases still failed name resolution even though the compose file specified them.

Why this belongs in core

This is not something the plugin can fake safely. /etc/hosts is generated in the sandbox layer, so callers need a first-class way to provide additional host entries to the runtime.

What this changes

  • add ContainerConfiguration.HostEntry
  • add ContainerConfiguration.hosts
  • preserve those entries through configuration encoding/decoding
  • extend sandbox host generation to append caller-provided host entries after the default localhost and primary hostname entries
  • add focused tests for round-tripping and host resolution behavior

Testing

  • Tested locally
  • Added/updated tests

var hosts = [ContainerConfiguration.HostEntry(ipAddress: "127.0.0.1", hostnames: ["localhost"])]

if let primaryAddress {
let ip = String(primaryAddress.split(separator: "/")[0])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we splitting by / here?

public struct IPv4Address {
@inlinable
public var description: String {
"\(bytes[0]).\(bytes[1]).\(bytes[2]).\(bytes[3])"
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are splitting by / here because the value coming in as primaryAddress is not a plain IP address — it is a CIDR notation string (e.g. "192.168.1.45/24").
The /24 part is the subnet mask. We only want the actual IP address (192.168.1.45), so we split on the / and take the first part.
Cleaned-up & safer version of that function:

extension SandboxService {
static func resolvedHosts(
hostname: String,
primaryAddress: String?,
extraHosts: [ContainerConfiguration.HostEntry]
) -> [ContainerConfiguration.HostEntry] {

 var hosts: [ContainerConfiguration.HostEntry] = [
ContainerConfiguration.HostEntry(ipAddress: "127.0.0.1", hostnames: ["localhost"])
]
if let primaryAddress {
// Split off the CIDR suffix if present (e.g. "192.168.1.45/24" → "192.168.1.45")
let ipOnly = primaryAddress.split(separator: "/").first.map(String.init) ?? primaryAddress
hosts.append(
ContainerConfiguration.HostEntry(
ipAddress: ipOnly,
hostnames: [hostname]
)
)
}
// Add any extra hosts passed in
hosts.append(contentsOf: extraHosts)
return hosts
}

}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we splitting by / here?

public struct IPv4Address {
@inlinable
public var description: String {
"\(bytes[0]).\(bytes[1]).\(bytes[2]).\(bytes[3])"
}
}

Also with the way I am running low latency it must be a steady string no breaking if possible.! I can show you diagrams or math that proves it.

@JaewonHur

Copy link
Copy Markdown
Contributor

It'd be good to have a follow up PR that wires this to CLI (e.g., --add-host [name:ip] in Docker).

@JaewonHur

Copy link
Copy Markdown
Contributor

@mazdak Hi! Could you do make fmt and push again? Sorry for late! We are going to merge this.

@mazdak

Copy link
Copy Markdown
ContributorAuthor

@mazdak Hi! Could you do make fmt and push again? Sorry for late! We are going to merge this.

Apologies for the late reply. Done.

@mazdak
mazdak requested a review from JaewonHurJune 2, 2026 00:12
@stephenlclarke

Copy link
Copy Markdown

Thanks for implementing this as a data-shape-only PR. I reviewed the patch from the downstream stephenlclarke/container-compose side, where the direct need is Compose depends_on.condition: service_healthy.

This looks like the right first slice to me:

  • HealthStatus and optional ContainerSnapshot.health give API clients a stable type without forcing the observer design into this PR.
  • Keeping the value nil until a runtime observer exists is a useful compatibility boundary for older servers/newer clients and for current daemon behavior.
  • The split keeps Compose policy in stephenlclarke/container-compose, which is where service dependency waiting, selected services, and user-facing Compose errors should live.

I would suggest adding a small Codable compatibility test before merge. This PR is adding a new API field, even though the daemon does not populate it yet, so the important thing to protect is the wire/API shape:

  • old ContainerSnapshot JSON without health should still decode successfully;
  • all HealthStatus values should encode and decode correctly;
  • existing code that creates a ContainerSnapshot should still default health to nil.

I would also make the meaning of nil versus .none very clear. nil should mean "health information is not available from this daemon/snapshot." .none should mean "health information is available, and the container has no healthcheck or no result yet." That difference will matter for downstream tools like stephenlclarke/container-compose.

For follow-up work, I would keep the healthcheck pieces split into small PRs:

  1. add the health field and HealthStatus type (this PR);
  2. add a way to configure a container healthcheck;
  3. add the daemon code that runs the healthcheck and updates ContainerSnapshot.health;
  4. add container create / container run flags for healthchecks;
  5. add support for reading Dockerfile HEALTHCHECK settings from images.

I do not think those need to be included in this PR. This PR is easier to review if it only adds the shared API field. That gives stephenlclarke/container-compose and other clients something stable to build against, while keeping Compose-specific behaviour out of apple/container.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@mazdak@JaewonHur@stephenlclarke@vxnkjyffmq-code@katiewasnothere
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fix: Add explicit host entries to container configuration - #1340

Open
mazdak wants to merge 4 commits into
apple:mainfrom
mazdak:pr2/container-host-entries
Open

Fix: Add explicit host entries to container configuration#1340
mazdak wants to merge 4 commits into
apple:mainfrom
mazdak:pr2/container-host-entries

Conversation

@mazdak

@mazdakmazdak commented Mar 22, 2026

Copy link
Copy Markdown
Contributor

Type of Change

  • Bug fix

Motivation and Context

While building a Docker Compose-like plugin for container and validating it against our real Docker Compose workload, we hit a core limitation: there was no way for callers to ask the runtime to append explicit entries to a container's /etc/hosts.

That showed up most clearly with Compose extra_hosts, especially the common host.docker.internal pattern. The plugin could parse those mappings, but there was no core field to carry them into the sandbox, so the runtime always generated only the default localhost/container-name entries.

In practice, containers that depended on host aliases still failed name resolution even though the compose file specified them.

Why this belongs in core

This is not something the plugin can fake safely. /etc/hosts is generated in the sandbox layer, so callers need a first-class way to provide additional host entries to the runtime.

What this changes

  • add ContainerConfiguration.HostEntry
  • add ContainerConfiguration.hosts
  • preserve those entries through configuration encoding/decoding
  • extend sandbox host generation to append caller-provided host entries after the default localhost and primary hostname entries
  • add focused tests for round-tripping and host resolution behavior

Testing

  • Tested locally
  • Added/updated tests

var hosts = [ContainerConfiguration.HostEntry(ipAddress: "127.0.0.1", hostnames: ["localhost"])]

if let primaryAddress {
let ip = String(primaryAddress.split(separator: "/")[0])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we splitting by / here?

public struct IPv4Address {
@inlinable
public var description: String {
"\(bytes[0]).\(bytes[1]).\(bytes[2]).\(bytes[3])"
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are splitting by / here because the value coming in as primaryAddress is not a plain IP address — it is a CIDR notation string (e.g. "192.168.1.45/24").
The /24 part is the subnet mask. We only want the actual IP address (192.168.1.45), so we split on the / and take the first part.
Cleaned-up & safer version of that function:

extension SandboxService {
static func resolvedHosts(
hostname: String,
primaryAddress: String?,
extraHosts: [ContainerConfiguration.HostEntry]
) -> [ContainerConfiguration.HostEntry] {

 var hosts: [ContainerConfiguration.HostEntry] = [
ContainerConfiguration.HostEntry(ipAddress: "127.0.0.1", hostnames: ["localhost"])
]
if let primaryAddress {
// Split off the CIDR suffix if present (e.g. "192.168.1.45/24" → "192.168.1.45")
let ipOnly = primaryAddress.split(separator: "/").first.map(String.init) ?? primaryAddress
hosts.append(
ContainerConfiguration.HostEntry(
ipAddress: ipOnly,
hostnames: [hostname]
)
)
}
// Add any extra hosts passed in
hosts.append(contentsOf: extraHosts)
return hosts
}

}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we splitting by / here?

public struct IPv4Address {
@inlinable
public var description: String {
"\(bytes[0]).\(bytes[1]).\(bytes[2]).\(bytes[3])"
}
}

Also with the way I am running low latency it must be a steady string no breaking if possible.! I can show you diagrams or math that proves it.

@JaewonHur

Copy link
Copy Markdown
Contributor

It'd be good to have a follow up PR that wires this to CLI (e.g., --add-host [name:ip] in Docker).

@JaewonHur

Copy link
Copy Markdown
Contributor

@mazdak Hi! Could you do make fmt and push again? Sorry for late! We are going to merge this.

@mazdak

Copy link
Copy Markdown
ContributorAuthor

@mazdak Hi! Could you do make fmt and push again? Sorry for late! We are going to merge this.

Apologies for the late reply. Done.

@mazdak
mazdak requested a review from JaewonHurJune 2, 2026 00:12
@stephenlclarke

Copy link
Copy Markdown

Thanks for implementing this as a data-shape-only PR. I reviewed the patch from the downstream stephenlclarke/container-compose side, where the direct need is Compose depends_on.condition: service_healthy.

This looks like the right first slice to me:

  • HealthStatus and optional ContainerSnapshot.health give API clients a stable type without forcing the observer design into this PR.
  • Keeping the value nil until a runtime observer exists is a useful compatibility boundary for older servers/newer clients and for current daemon behavior.
  • The split keeps Compose policy in stephenlclarke/container-compose, which is where service dependency waiting, selected services, and user-facing Compose errors should live.

I would suggest adding a small Codable compatibility test before merge. This PR is adding a new API field, even though the daemon does not populate it yet, so the important thing to protect is the wire/API shape:

  • old ContainerSnapshot JSON without health should still decode successfully;
  • all HealthStatus values should encode and decode correctly;
  • existing code that creates a ContainerSnapshot should still default health to nil.

I would also make the meaning of nil versus .none very clear. nil should mean "health information is not available from this daemon/snapshot." .none should mean "health information is available, and the container has no healthcheck or no result yet." That difference will matter for downstream tools like stephenlclarke/container-compose.

For follow-up work, I would keep the healthcheck pieces split into small PRs:

  1. add the health field and HealthStatus type (this PR);
  2. add a way to configure a container healthcheck;
  3. add the daemon code that runs the healthcheck and updates ContainerSnapshot.health;
  4. add container create / container run flags for healthchecks;
  5. add support for reading Dockerfile HEALTHCHECK settings from images.

I do not think those need to be included in this PR. This PR is easier to review if it only adds the shared API field. That gives stephenlclarke/container-compose and other clients something stable to build against, while keeping Compose-specific behaviour out of apple/container.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@mazdak@JaewonHur@stephenlclarke@vxnkjyffmq-code@katiewasnothere