How to Execute a Command in a Running Container with Apple Container

Apple Container provides the container exec sub-command to run arbitrary processes inside live containers by leveraging the Container API to spawn sandboxed processes within the target container's namespace.

Apple Container's container exec command bridges the gap between the CLI and the underlying XPC-based runtime, allowing you to execute a command in a running container while preserving the same isolation guarantees as the original init process. This functionality is implemented in the open-source apple/container repository and reuses the same process-configuration plumbing as the run command, making it straightforward to debug, inspect, or extend active workloads.

The Execution Pipeline

When you invoke container exec, the CLI performs a seven-step runtime flow defined in Sources/ContainerCommands/Container/ContainerExec.swift:

  1. Container Lookup – A ContainerClient instance contacts the local container daemon and fetches container metadata via client.get(id:).

  2. State Validation – The ensureRunning(container:) method verifies the container is active; otherwise, it raises an error.

  3. Configuration Building – The CLI starts from the container's init process configuration (container.configuration.initProcess) and overrides the executable, arguments, and optional flags including TTY allocation, interactive stdin, environment variables, working directory, and user/group handling.

  4. I/O SetupProcessIO.create constructs the appropriate stdin/stdout/stderr pipes, allocating a pseudo-terminal when --tty is specified or attaching the host's stdin when --interactive is set.

  5. Process Spawnclient.createProcess transmits the configuration to the runtime, returning a Process handle.

  6. Detachment – If --detach (-d) is provided, the process starts in the background and the CLI prints the container ID without waiting.

  7. Synchronous Monitoring – Otherwise, the CLI forwards signals (SIGINT, SIGTERM) to the child process, monitors completion, and returns the exit code to the shell.

Implementation Details

The command definition and argument parsing reside in the ContainerExec struct (lines 23-45 of Sources/ContainerCommands/Container/ContainerExec.swift), while the execution logic lives in the run() method (lines 46-114). User and group overrides are parsed by Parser.user in Sources/ContainerPlugin/Parser.swift, and environment variable handling flows through Parser.allEnv. The ContainerClient class in Sources/ContainerAPIClient/ContainerClient.swift provides the daemon communication layer, and Sources/ContainerBuild/ProcessIO.swift manages stream allocation. User-facing documentation appears in docs/command-reference.md (lines 52-71).

Practical Examples

Run a one-off command inside a running container named "my-web-server":

container exec my-web-server ls /var/www

Open an interactive shell with TTY and stdin attached:

container exec -it my-web-server /bin/bash

Run a background command in detached mode:

container exec -d my-web-server /usr/bin/python3 /opt/app/background_worker.py

Override the user and group for the executed process:

container exec --user nobody --gid 1000 my-web-server cat /etc/passwd

Set environment variables and a custom working directory:

container exec -e DEBUG=1 -w /app my-web-server ./run-tests.sh

Summary

  • Use container exec to run arbitrary processes inside already-running Apple Containers without creating new instances.
  • Configuration inheritance leverages the container's init process settings while allowing overrides for executables, arguments, users, groups, and environments.
  • Interactive support via -i and -t flags allocates pseudo-terminals and attaches stdin for shell sessions.
  • Background execution using -d detaches the process and returns immediately with the container ID.
  • Isolation preservation ensures all executed commands maintain the same sandboxing guarantees as the original container process.

Frequently Asked Questions

How does container exec differ from container run?

container run creates a new container instance from an image, whereas container exec injects a process into an existing running container's namespace. The exec command uses ContainerClient.get(id:) to look up the live container and calls ensureRunning(container:) to validate state before spawning, ensuring you target active workloads rather than initializing fresh ones.

Can I run interactive commands like bash through container exec?

Yes. Pass the -i (interactive) and -t (TTY) flags to attach your terminal's stdin and allocate a pseudo-terminal. According to the implementation in ContainerExec.swift, this triggers ProcessIO.create to configure the appropriate streams, allowing you to execute container exec -it my-container /bin/bash for full shell access inside the sandbox.

What happens if the container is stopped when I try to exec?

The ensureRunning(container:) validation step checks the container state before process creation. If the target container is not running, the CLI raises an error and exits without attempting to create the process, preventing execution attempts against inactive containers.

How do I set environment variables when executing a command?

Use the -e flag followed by KEY=VALUE pairs, or specify an environment file with --env-file. The Parser.swift file handles these inputs through Parser.allEnv, merging them into the process configuration that overrides the container's base environment for that specific execution.

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 →