How Container Integrates with Launchd for Service Management

Container integrates with Launchd by wrapping launchctl commands in a ServiceManager struct that registers XML property lists generated by the LaunchPlist model, enabling native macOS process lifecycle management.

The Apple Container project delegates all service supervision to macOS's native launchd system, avoiding custom process monitoring in favor of the platform's authoritative service manager. This architecture ensures that the Container API server, plugins, and helper processes benefit from Launchd's automatic restart, logging, and security sandboxing capabilities. The integration centers on two Swift components that bridge high-level container operations with low-level launchctl commands.

Core Integration Components

ServiceManager

The ServiceManager struct in Sources/ContainerPlugin/ServiceManager.swift serves as a thin Swift wrapper around the launchctl command-line tool. Rather than interfacing with Launchd directly via XPC, it abstracts common service lifecycle actions by spawning Process instances pointing at /bin/launchctl.

This wrapper implements five critical operations:

  • Bootstrap (register): Loads a property list into Launchd via launchctl bootstrap <domain> <plistPath>.
  • Bootout (deregister): Stops and unloads a service using launchctl bootout <label>.
  • Kickstart (kickstart): Restarts a running service with launchctl kickstart -k <label>.
  • Kill (kill): Sends signals to processes using launchctl kill <signal> <label>.
  • Enumeration (enumerate): Parses the output of launchctl list to return loaded service labels.

The struct also provides getDomainString(), which queries launchctl managername to determine the current session type—returning system, gui/<uid>, or user/<uid>—ensuring services load in the correct security context.

LaunchPlist

The LaunchPlist struct in Sources/ContainerPlugin/LaunchPlist.swift models the XML property list files that Launchd consumes. It conforms to Encodable and provides an encode() method that generates valid XML plist documents ready for the bootstrap command.

Key properties mapped to Launchd keys include:

  • Label: The unique service identifier.
  • ProgramArguments: Array of executable path and arguments.
  • EnvironmentVariables: Dictionary of environment values passed to the service.
  • RunAtLoad: Boolean triggering immediate startup after bootstrap.
  • MachServices: Dictionary enabling XPC communication between Container components.
  • LimitLoadToSessionType: Constraints on which user sessions may load the service.

The struct also supports debugging via the CONTAINER_DEBUG_LAUNCHD_LABEL environment variable, which sets the waitForDebugger flag in the generated plist, causing Launchd to pause service startup until a debugger attaches.

Service Lifecycle Workflow

Container manages services through a six-step workflow that maps directly to Launchd primitives:

  1. Determine Launchd Domain
    ServiceManager.getDomainString() (lines 24-36 in ServiceManager.swift) executes launchctl managername to detect whether the current context is System, Aqua, or Background, then returns the appropriate domain identifier for subsequent commands.

  2. Generate Property List
    When starting a service (e.g., via Sources/ContainerCommands/System/SystemStart.swift), the code constructs a LaunchPlist instance defining the service label, executable path, and runtime options like keepAlive or machServices. The encode() method writes this data to /var/run/container/launchd/<label>.plist.

  3. Register with Launchd
    ServiceManager.register(plistPath:) invokes launchctl bootstrap <domain> <plistPath>, instructing Launchd to load the configuration and begin monitoring the service.

  4. Control Service State
    Container sends lifecycle commands through ServiceManager:

    • Restart: kickstart(label:) calls launchctl kickstart -k <label>.
    • Stop: deregister(fullServiceLabel:) calls launchctl bootout <label>.
    • Signal: kill(fullServiceLabel:, signal:) calls launchctl kill <signal> <label>.
  5. Query Status
    ServiceManager.enumerate() runs launchctl list and parses the third column to return active service labels. isRegistered(fullServiceLabel:) checks specific label status via launchctl list <label>.

  6. Cleanup on Shutdown
    The scripts/ensure-container-stopped.sh script uses launchctl managername to discover the current domain before iterating through container services and executing bootout to ensure clean termination.

Practical Implementation Example

The following Swift example demonstrates registering a custom container service through Launchd:

import Foundation
import ContainerPlugin

// Configure the Launchd property list
let plist = LaunchPlist(
    label: "com.example.container.myservice",
    arguments: ["/usr/local/bin/myservice", "--port", "8080"],
    runAtLoad: true,
    keepAlive: true,
    machServices: ["com.example.container.myservice"]
)

// Write the plist to the Launchd directory
let plistData = try plist.encode()
let plistPath = "/var/run/container/launchd/com.example.container.myservice.plist"
try plistData.write(to: URL(fileURLWithPath: plistPath))

// Register with the appropriate Launchd domain
try ServiceManager.register(plistPath: plistPath)

// Restart the service later if needed
let domain = try ServiceManager.getDomainString()
try ServiceManager.kickstart(fullServiceLabel: "\(domain)/com.example.container.myservice")

High-level CLI commands in Sources/ContainerCommands/System/SystemStart.swift and SystemStop.swift wrap these operations, allowing users to execute container start and container stop without manually interacting with launchctl.

Summary

  • Container treats Launchd as the single source of truth for process lifecycle, using ServiceManager to wrap launchctl commands for bootstrap, bootout, andKill operations.
  • The LaunchPlist struct in Sources/ContainerPlugin/LaunchPlist.swift type-safely generates XML property lists with support for MachServices, environment variables, and debugging flags.
  • Domain detection via launchctl managername ensures services load in the correct security context (system, GUI, or user).
  • Services are registered by writing plists to /var/run/container/launchd/ and calling launchctl bootstrap, enabling automatic restart and monitoring without custom watchdog code.

Frequently Asked Questions

How does Container determine which Launchd domain to use for service registration?

Container calls launchctl managername through ServiceManager.getDomainString() to detect the current session type. This returns system for system-wide daemons, gui/<uid> for user sessions connected to the Aqua window server, or user/<uid> for background sessions, ensuring services run with appropriate permissions and visibility.

What is the difference between bootstrap and kickstart in Container's Launchd integration?

bootstrap (via ServiceManager.register) performs the initial registration of a property list with Launchd, creating the service entry and optionally starting it if RunAtLoad is true. kickstart (via ServiceManager.kickstart) forces an immediate restart of an already registered service, useful for reloading configurations without full deregistration.

Can Container services communicate via XPC, and how is this configured?

Yes, the LaunchPlist struct includes a machServices dictionary that populates the MachServices key in the generated plist. This enables XPC communication between the Container API server and its helper processes, with the service manager using these labels to route inter-process messages.

How can developers debug Container services that are managed by Launchd?

Set the CONTAINER_DEBUG_LAUNCHD_LABEL environment variable before starting the service. When present, LaunchPlist sets the waitForDebugger flag in the generated property list, causing Launchd to pause the service process immediately after startup until a debugger attaches via the specified Mach service label.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →