How to Execute Multiple Commands in a Docker Container with a Single Command
Docker provides a powerful and flexible platform for developers to isolate applications in containers, making them portable and easy to manage. A common scenario that developers face is needing to execute multiple commands within a Docker container without initiating multiple separate docker exec calls. In this post, we’ll explore a streamlined approach to achieve this in a single invocation.
The Challenge: Running Multiple Commands Sequentially
Typically, executing commands in a Docker container is done through the docker exec command followed by the container name and the command you wish to execute. The challenge arises when you need to perform several commands that depend on each other. Repeatedly running docker exec for each command can be inefficient and cumbersome, especially in situations involving dependency or order of execution.
The Solution: Single Command Execution
To run multiple commands inside a Docker container with one command, we can employ a shell script technique using a here-document (heredoc). This method involves passing multiple commands all at once to a container’s shell. Here’s how you can do it:
cat <<EOF | docker exec --interactive alpine shcd /optwget https://example.com/file.tgztar xvf file.tgzrm file.tgzEOFStep-by-Step Explanation:
-
Here-document (EOF): This is a type of redirection that allows you to provide several lines of input to a command sequentially. The
cat <<EOF |starts the input, and it ends with a standaloneEOF. Everything between these markers is treated as input to thecatcommand. -
Pipe to Docker Exec: The output of the
catcommand (your script) is piped intodocker exec. The--interactiveoption keeps STDIN open even if not attached, allowing the piped commands to execute as if they were being typed directly into an interactive session. -
Container and Shell: The
alpine shspecifies which container and which shell will run the commands. In this case, it’s an Alpine Linux container using theshshell. -
Commands to Execute: Inside the
heredoc, we move into the opt directory, download a file, unpack it, and then remove the archive. This sequence demonstrates the capability to perform complex operations in a single sent command sequence.
Benefits:
-
Efficiency: Reduces the overhead of multiple
docker execcalls. -
Atomicity: Ensures that all commands are executed in a single session, which is particularly useful for dependent commands.
-
Simplicity: Simplifies scripts and makes automation more straightforward.
Using this technique effectively allows Docker developers and system administrators to manage containerized applications with greater flexibility and efficiency. Whether you’re automating deployments, performing maintenance, or scripting routine tasks, combining commands with this approach can save time and reduce complexity. This method is particularly valuable in build scripts or during the development phase, where such tasks are frequent.
Related Reading
- Executing Commands with Asterisks in Docker
- Copying Files Between Docker Containers and Host Machines
- How to Transfer docker Images Without a Repository
- Docker Compose useful commands
- Access Docker socket via TCP
What Actually Breaks This
The heredoc approach is clean until it isn’t. Here’s where it falls apart in practice.
The shell doesn’t exist. Distroless images and some minimal containers don’t ship sh or bash. If your docker exec ... sh returns OCI runtime exec failed: exec: "sh": executable file not found, you’re in one of those. Check what’s available first:
docker exec mycontainer ls /bin/sh /bin/bash /bin/busybox 2>/dev/nullIf nothing comes back, you’re either stuck with docker cp + a script file, or you need to rethink the image entirely (which, honestly, you probably should).
Exit codes disappear. This one bites you in CI. When you pipe through cat, the pipeline’s exit code is the exit code of the last command — docker exec itself. If wget fails inside the heredoc, your script happily exits 0 and your deploy pipeline celebrates a broken container. Use sh -c with set -e inside the heredoc to bail on first failure:
cat <<EOF | docker exec --interactive mycontainer shset -ecd /optwget https://example.com/file.tgz || { echo "download failed"; exit 1; }tar xvf file.tgzrm file.tgzEOFecho "Exit: $?"Environment variables don’t expand the way you expect. If you write $MY_VAR inside the heredoc, it expands on the host before it ever reaches the container. That’s occasionally what you want — usually it’s not. To pass a literal $HOME into the container’s shell, either quote the delimiter (<<'EOF') or escape it (\$HOME).
A More Realistic Example
Here’s what this actually looks like when you’re doing something useful — rotating a config file and reloading a service without restarting the container:
cat <<'EOF' | docker exec --interactive nginx-proxy shset -ecp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bakcat > /etc/nginx/conf.d/upstream.conf <<INNERupstream backend { server app:3000;}INNERnginx -t && nginx -s reloadEOFThe <<'EOF' (quoted) means nothing inside gets expanded by your host shell — the inner <<INNER heredoc runs entirely inside the container. Yes, nested heredocs work. Yes, it looks weird. Your 2 AM self will figure it out.
One more thing: if you’re doing this in a script that runs regularly — a cron job, a deploy hook, anything automated — swap --interactive for just piping stdin directly. The --interactive flag is fine from a terminal but can hang in non-interactive contexts depending on the shell and Docker version. Explicit pipe without -i is cleaner when there’s no human on the other end.