Skip to content
Go back

Multiple Actions with a Single docker exec Call

By SumGuy 5 min read
Multiple Actions with a Single docker exec Call

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 sh
cd /opt
wget https://example.com/file.tgz
tar xvf file.tgz
rm file.tgz
EOF

Step-by-Step Explanation:

Benefits:

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.

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:

Terminal window
docker exec mycontainer ls /bin/sh /bin/bash /bin/busybox 2>/dev/null

If 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:

Terminal window
cat <<EOF | docker exec --interactive mycontainer sh
set -e
cd /opt
wget https://example.com/file.tgz || { echo "download failed"; exit 1; }
tar xvf file.tgz
rm file.tgz
EOF
echo "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:

Terminal window
cat <<'EOF' | docker exec --interactive nginx-proxy sh
set -e
cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
cat > /etc/nginx/conf.d/upstream.conf <<INNER
upstream backend {
server app:3000;
}
INNER
nginx -t && nginx -s reload
EOF

The <<'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.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
Understanding the regreSSHion Vulnerability in OpenSSH
Next Post
Mastering xargs in Linux

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts