How to Use `container exec` to Run Commands in Running Containers
The container exec command creates a new sandboxed process inside an already-running container by validating the container state, building a ProcessConfiguration, and invoking the daemon's createProcess RPC.
The container exec subcommand in the Apple Container project lets you execute additional commands inside containers that are already running. According to the source code in the apple/container repository, this functionality is implemented in Sources/ContainerCommands/Container/ContainerExec.swift as the Application.ContainerExec type. It provides functionality similar to Docker's docker exec, but built on Apple's sandbox-based container runtime.
How container exec Works Internally
The implementation follows a seven-step flow that bridges the CLI interface to the sandboxed process execution.
Step 1: Resolve the Target Container
The command first creates a ContainerClient to contact the local daemon. It fetches the container metadata using client.get(id: containerId) and validates that the container is actually running via ensureRunning(container:). If the container is stopped or doesn't exist, the command fails immediately.
Step 2: Build the Process Configuration
The first positional argument becomes the executable, while remaining arguments are passed as its arguments. The code starts with the container's base initProcess configuration from container.configuration.initProcess, then overrides specific fields: config.executable = executable and config.arguments = .... Flags like --tty, --env, --workdir, and --user are parsed in ProcessUtils.swift and merged into a ProcessConfiguration struct.
Step 3: Prepare I/O Streams
The ProcessIO.create(tty:interactive:detach:) method constructs the appropriate stdin, stdout, and stderr pipes. When --tty is specified, it allocates a pseudo-terminal; otherwise, it uses standard pipes. This handles the redirection between the host and the sandboxed process.
Step 4: Create the Remote Process
With the configuration assembled, the client calls client.createProcess(containerId: container.id, configuration: config, stdio: io.stdio). This RPC request asks the daemon to spawn a new sandboxed process inside the target container.
Step 5: Handle Detached Execution
If the --detach flag is supplied, the CLI starts the process, immediately closes its I/O handles, prints the container ID, and returns. The process continues running in the background within the container.
Step 6: Manage Foreground Execution
For non-detached runs, the code installs a SignalThreshold handler to protect against endless SIGINT or SIGTERM loops (terminating after three signals). The io.handleProcess(process:log:) method then drives the I/O streams until the process exits, capturing the exit code.
Step 7: Propagate Exit Status
Finally, the command throws ArgumentParser.ExitCode(exitCode) to ensure the container exec CLI exits with the same status code as the process running inside the container.
Common Usage Examples
The following examples demonstrate the most common patterns for running commands inside active containers, as documented in the project's docs/tutorials/start-here.md.
Run a Simple One-Off Command
Execute a single command and return the output:
container exec my-web-server ls /content
This runs ls /content inside the container named my-web-server and streams the results to your terminal.
Start an Interactive Shell
Allocate a pseudo-terminal and connect your host keyboard to a shell inside the container:
container exec --tty --interactive my-web-server sh
The --tty flag tells the shell it has a terminal attached, while --interactive streams your keystrokes to the process.
Run a Detached Background Process
Start a process that outlives your CLI session:
container exec --detach my-web-server sleep 3600
This begins a sleep process that runs for one hour, detaches the client immediately, and prints the container ID.
Set Custom Environment and Working Directory
Inject environment variables and change the working directory before execution:
container exec \
--env FOO=bar \
--workdir /app \
my-web-server ./run-my-script.sh
The FOO=bar environment variable is added to the process environment, and the command executes from /app instead of the default working directory.
Execute as a Non-Root User
Run commands with specific user privileges:
container exec \
--user alice \
my-web-server id -u -n
This executes id -u -n as user alice inside the container, provided that user exists in the container's user namespace.
Summary
container execcreates new processes inside already-running containers via theApplication.ContainerExectype inSources/ContainerCommands/Container/ContainerExec.swift.- The command validates the container is running via
ensureRunning(container:), builds aProcessConfiguration, and usesProcessIOto handle input/output streams. - Detached mode (
--detach) runs processes in the background, while foreground mode attaches your terminal and handles signals viaSignalThreshold. - Key flags include
--ttyfor pseudo-terminals,--interactivefor stdin attachment,--envfor environment variables,--workdirfor directory changes, and--userfor privilege switching. - All functionality relies on the
ContainerClientRPC interface defined inSources/ContainerAPIClient/ContainerClient.swift.
Frequently Asked Questions
What is the difference between container exec and container run?
container run creates and starts a new container from an image, while container exec starts a new process inside an existing running container. The exec command requires the target container to already be in a running state, which it verifies via ensureRunning(container:) before attempting to create the process.
How does container exec handle terminal signals like Ctrl+C?
The implementation installs a SignalThreshold handler that counts SIGINT and SIGTERM signals. After receiving three termination signals, the CLI exits forcefully to prevent infinite loops. Before that threshold, signals are propagated appropriately to the container process through the ProcessIO stream handling.
Can I execute commands as root if the container normally runs as a non-root user?
Yes. The --user flag allows you to specify any valid user in the container's user namespace, including root (uid 0), provided the container configuration permits it. The user is parsed in Sources/ContainerCommands/Container/ProcessUtils.swift and passed to the daemon as part of the ProcessConfiguration.
Why does container exec fail with "container is not running"?
The command explicitly checks the container state using client.get(id:containerId) followed by ensureRunning(container:). If the container is stopped, paused, or hasn't finished initializing, the RPC will fail. You must start the container with container run or container start before using container exec.
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 →