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:
-
Container Lookup – A
ContainerClientinstance contacts the local container daemon and fetches container metadata viaclient.get(id:). -
State Validation – The
ensureRunning(container:)method verifies the container is active; otherwise, it raises an error. -
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. -
I/O Setup –
ProcessIO.createconstructs the appropriate stdin/stdout/stderr pipes, allocating a pseudo-terminal when--ttyis specified or attaching the host's stdin when--interactiveis set. -
Process Spawn –
client.createProcesstransmits the configuration to the runtime, returning aProcesshandle. -
Detachment – If
--detach(-d) is provided, the process starts in the background and the CLI prints the container ID without waiting. -
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 execto 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
-iand-tflags allocates pseudo-terminals and attaches stdin for shell sessions. - Background execution using
-ddetaches 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →