Skip to content

Add container-compose plugin#3

Closed
mazdak wants to merge 7 commits into
pr2/container-host-entriesfrom
pr2/container-compose-plugin
Closed

Add container-compose plugin#3
mazdak wants to merge 7 commits into
pr2/container-host-entriesfrom
pr2/container-compose-plugin

Conversation

@mazdak
Copy link
Copy Markdown
Owner

@mazdak mazdak commented Mar 22, 2026

Problem

This branch started from a practical goal: run a real Docker Compose development stack on top of Apple's container tooling without asking the application repository to fork its compose files or invent container-specific workflow glue.

The immediate workload that drove the implementation was resq-fullstack, which relies on normal Compose features such as multi-service orchestration, depends_on, health checks, env_file, profile selection, bind mounts, named volumes, extra_hosts, and image build-or-reuse behavior.

container did not have a Compose-compatible plugin for that workflow, so bringing up a realistic stack required a new plugin layer that can translate Compose concepts into the existing container runtime model.

Scope

This PR adds a first-party container compose plugin that implements the Compose-style orchestration layer in Plugins/container-compose.

It is stacked on top of #1 because Compose extra_hosts needs the core hosts plumbing introduced there.

What this changes

  • add the Plugins/container-compose package, plugin config, CLI entrypoints, and debug executable
  • add compose parsing, file merging, include support, env interpolation, project conversion, orchestration, and package-local test coverage
  • implement Compose extra_hosts, health checks, dependency ordering, project naming/path resolution, volume copy-up behavior, and image reuse in the plugin layer
  • resolve the Compose host-gateway sentinel in the plugin before populating core config.hosts
  • preserve Docker-compatible entrypoint: '' behavior
  • raise the plugin's default per-service memory fallback to 6 GiB when Compose does not specify a memory limit, which keeps the resq-fullstack frontend alive on this runtime
  • normalize plugin file headers to the repository's standard copyright header

Validation

  • swift test --package-path Plugins/container-compose
  • swift test --package-path Plugins/container-compose --filter ExtraHostsTests/testResolvedComposeHostAddressMapsHostGatewaySentinel

@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Mar 22, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2931d8d78c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +965 to +969
let imageName = service.effectiveImageName(projectName: project.name)
try await ensureImageAvailable(serviceName: serviceName, service: service, imageName: imageName, policy: pullPolicy)

// Create and start new container
try await createAndStartNewContainer(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Skip creation when reusing an existing container

handleExistingContainer() can decide to reuse the runtime container for both --no-recreate and the "config unchanged" path, but createAndStartContainer() always falls through into createAndStartNewContainer() anyway. In practice, a second container compose up (or any run with --no-recreate) still tries to create the same container ID again, so reuse never works and the command will fail instead of leaving the existing service running.

Useful? React with 👍 / 👎.

Comment on lines +62 to +64
// Add default network if no networks specified
if networks.isEmpty {
networks["default"] = Network(name: "default", driver: "bridge", external: false)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the runtime default network when Compose omits networks

Synthesizing a project-level default network here turns every compose file without an explicit networks: section into a custom-network project. up() then hits ensureComposeNetworks()/mapServiceNetworkIds(), both of which reject non-default networking on macOS versions earlier than 26, so even a minimal file like services: {web: {image: alpine}} now fails on supported macOS 15 instead of using ClientNetwork.defaultNetworkName.

Useful? React with 👍 / 👎.

Comment on lines +81 to +82
// Wait for process completion
let result = try await process.wait()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Apply health-check timeouts to probe processes

HealthCheck.timeout is parsed into the model, but the runner ignores it and waits indefinitely for process.wait(). Any probe that hangs instead of exiting—e.g. a curl to a blackholed address—will block depends_on.condition: service_healthy, compose up --wait, and compose health forever rather than failing the check after the configured timeout.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ed178323f1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1103 to +1107
process: ProcessConfiguration(
executable: service.command?.first ?? "/bin/sh",
arguments: Array((service.command ?? ["/bin/sh", "-c"]).dropFirst()),
environment: service.environment.map { "\($0.key)=\($0.value)" },
workingDirectory: service.workingDir ?? "/"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Hash reused containers from the real image process config

handleExistingContainer() derives expectedHash from service.command/workingDir only, while container creation later resolves image ENTRYPOINT, CMD, and image WORKDIR before writing the com.apple.container.compose.config-hash label. For any service that relies on image defaults (for example image: postgres with no command:), the stored hash and the next-run hash can never match, so the "config unchanged" path recreates the container on every compose up even when nothing changed.

Useful? React with 👍 / 👎.

Comment on lines +1688 to +1692
if let configuredImage = service.image,
(try? await ClientImage.get(reference: configuredImage)) != nil
{
log.info("Using existing image '\(imageName)' for service '\(serviceName)'")
buildCache[cacheKey] = imageName
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Still build services that declare both build: and image:

This short-circuits as soon as the tagged image already exists locally, so a service like the new build + image test case will skip its Dockerfile build and run whatever is already tagged under image: instead. In the common case where that tag is a pulled base image (for example node:18-alpine), compose up starts the wrong filesystem and command instead of the application image built from the compose source.

Useful? React with 👍 / 👎.

Comment on lines +662 to +665
return Volume(
name: name,
driver: volume.driver ?? "local",
external: external
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve top-level volume name: when resolving mounts

ComposeVolume.name is parsed but discarded here, so later ensureVolume()/down --volumes operate on the logical key (data) instead of the actual resource name (prod-data, shared-data, etc.). Any compose file that aliases a named or external volume will either fail to find the existing volume or create/delete the wrong one.

Useful? React with 👍 / 👎.

Comment on lines +1404 to +1405
if let cpus = service.cpus {
config.resources.cpus = Int(cpus) ?? 4
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Don't silently turn non-integer CPU limits into 4 cores

Compose CPU limits are strings and commonly use fractional values like "0.5"; here every non-integer falls through to the default of 4. That means a service asking for half a core, or even a typo in cpus, gets the platform default allocation instead of an error or an intentional conversion, which can materially over-provision workloads.

Useful? React with 👍 / 👎.

@mazdak mazdak closed this Mar 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant