docker exec and Interactive Shells
docker exec runs an additional command inside a container that is already running. It matters because most real troubleshooting happens after a container has started: checking files, reading environment variables, testing network tools, or opening a temporary shell without rebuilding the image. Used well, docker exec is a precise inspection tool; used carelessly, it can hide configuration problems by changing a live container by hand.
Overview: How docker exec Works
A Docker container is not a tiny virtual machine. It is one or more Linux processes isolated with namespaces, controlled with cgroups, and given a filesystem made from read-only image layers plus a thin writable container layer. When you start a container with docker run, Docker creates that filesystem view, applies runtime settings such as environment variables and mounts, and starts the container’s main process. For nginx, that main process is usually the web server; for postgres, it is the database server.
docker exec asks the Docker daemon to start another process inside the same running container. That new process joins the container’s namespaces: it sees the same container filesystem, process namespace, network interfaces, hostname, mounted volumes, and environment defaults. It does not create a new image layer, does not restart the container, and does not rerun the image’s ENTRYPOINT or CMD. It is simply another process launched into an existing container context.
This distinction explains the most important rule: docker exec works only on running containers. An image such as nginx:1.27-alpine is a template on disk, so there is nowhere to execute a process until a container exists. A stopped container has a filesystem and metadata, but it has no running process environment for exec to join. Use docker start to restart a stopped container, or create a new one with docker run.
Interactive shells are the most visible use of docker exec. The -i flag keeps standard input open, and -t allocates a pseudo-terminal so programs behave like they are attached to a real terminal. Together, -it lets you run sh or bash interactively. Minimal images often include sh but not bash, especially Alpine-based images, so docker exec -it container sh is the more portable first attempt.
Syntax
docker exec [OPTIONS] CONTAINER COMMAND [ARG...]
docker exec -it CONTAINER sh
docker exec -it CONTAINER bash
| Part | Meaning |
|---|---|
CONTAINER |
The running container name or ID, such as web-for-exec. This is not an image tag. |
COMMAND [ARG...] |
The process to start inside the container, plus its arguments. Docker does not automatically run this through a shell unless you explicitly run sh -c. |
-i, --interactive |
Keeps standard input open. Needed for typing into shells or commands that read input. |
-t, --tty |
Allocates a pseudo-terminal. Usually combined with -i for shells. |
-u, --user |
Runs the exec process as a user or UID/GID inside the container, such as -u 0 for root or -u 1000:1000. |
-e, --env |
Adds or overrides environment variables for the exec process only. It does not permanently change the container configuration. |
-w, --workdir |
Sets the working directory for the exec process, if that directory exists in the container. |
-d, --detach |
Starts the exec command in the background and returns immediately. |
Examples
Run a One-Off Command in a Running Container
docker run -d --name web-for-exec nginx:1.27-alpine
docker exec web-for-exec nginx -v
docker rm -f web-for-exec
Output:
nginx version: nginx/1.27.5
web-for-exec
The first command starts a detached nginx container with a stable name. The second command runs nginx -v inside that already-running container. Notice that the command is not run on your host; it uses the binary and libraries from the container filesystem. The final command force-removes the lab container so the example is repeatable.
Open an Interactive Shell
docker run -d --name alpine-lab alpine:3.20 sleep 1d
docker exec -it alpine-lab sh
docker rm -f alpine-lab
Output:
/ # pwd
/
/ # cat /etc/os-release | head -2
NAME="Alpine Linux"
ID=alpine
/ # exit
Here the container’s main process is sleep 1d, which keeps it alive for practice. docker exec -it alpine-lab sh starts a shell inside the container and attaches your terminal to it. When you type exit, only the shell process ends; the container keeps running until the main sleep process ends or you remove the container.
Debug Files and Environment Without Rebuilding
docker run -d --name api-debug -p 8080:80 nginx:1.27-alpine
docker exec api-debug sh -c "ls -1 /usr/share/nginx/html && printenv HOSTNAME"
docker exec -u 0 api-debug sh -c "touch /tmp/debug-marker && ls -l /tmp/debug-marker"
docker rm -f api-debug
Output:
50x.html
index.html
0f4a8d0f1a2b
-rw-r--r-- 1 root root 0 Aug 3 12:00 /tmp/debug-marker
api-debug
This example runs shell logic with sh -c because operators like && are interpreted by a shell, not by Docker. The process sees the same container hostname and files that the web server sees. The -u 0 flag runs the debug command as root, which is useful for inspection but should not become a habit for application containers.
How it Works Step by Step
- You run
docker execwith a container name or ID. The Docker client sends the request to the Docker daemon over its local API connection. - The daemon verifies that the target container exists and is running. If the main process has exited, Docker returns an error instead of starting a new command.
- Docker creates an exec instance with the requested command, user, working directory, environment overrides, and terminal settings.
- The container runtime starts the new process inside the container’s existing namespaces and cgroup. The process sees the same mounted volumes and writable layer as the main container process.
- If you used
-it, Docker connects your terminal to the process. If you did not use-t, output is plain streams, which is better for scripts and automation. - When the exec command exits, Docker reports its exit code to the client. The container itself keeps running as long as its original main process is still alive.
Changes made through docker exec land in the container’s writable layer or mounted volumes. Files written to a named volume survive container replacement; files written only to the container layer disappear when that container is removed. They also do not update the original image. If you need a repeatable change, put it in the Dockerfile, Compose file, or runtime configuration instead of typing it into a live shell.
Common Mistakes
Using an Image Name Instead of a Container
docker exec -it nginx:1.27-alpine sh
This is wrong because nginx:1.27-alpine is an image reference, not a running container. Create or identify a container first:
docker run -d --name shell-target nginx:1.27-alpine
docker exec -it shell-target sh
docker rm -f shell-target
Trying to Exec into a Stopped Container
docker run --name quick alpine:3.20 echo done
docker exec -it quick sh
The echo done command exits immediately, so the container is stopped by the time docker exec runs. Keep the container alive with a long-running main process, or restart it with a suitable command:
docker run -d --name quick alpine:3.20 sleep 1d
docker exec -it quick sh
docker rm -f quick
Assuming Manual Fixes Are Permanent
Installing tools, editing config files, or patching application code through an interactive shell is temporary operational state. It may appear to solve the problem on one container, but the change will vanish when the container is recreated and will not be present on another host. Treat docker exec changes as investigation notes, then move the real fix into source control, the image build, a mounted config file, or Compose configuration.
Forgetting That Minimal Images Are Minimal
Many production images intentionally omit bash, package managers, curl, editors, and debugging tools. This keeps images smaller and reduces attack surface. Try sh before bash, and prefer purpose-built debug containers or temporary diagnostic images when the production image is intentionally stripped down.
Best Practices
- Name containers in examples and local labs with
--name; it makesdocker execcommands readable and repeatable. - Use
docker exec command argswithout-tin scripts so output is easier to parse and exit codes are reliable. - Use
-itonly for human interactive sessions. - Use
sh -cwhen you need shell features such as pipes, redirects, variable expansion, or&&. - Prefer
shfor portability; usebashonly when you know the image includes it. - Do not rely on manual edits made inside a container. Rebuild the image or update configuration so the fix is reproducible.
- Avoid running exec sessions as root unless you need root for a specific inspection or repair task.
- Remember that
docker execis local to one container. In Compose projects, use the service container name fromdocker compose psor usedocker compose exec SERVICE COMMAND.
Practice Exercises
- Start an
nginx:1.27-alpinecontainer namedinspect-web. Usedocker execto print the first two lines of/etc/os-release, then remove the container. Hint: usesh -cfor the pipeline. - Run an
alpine:3.20container that stays alive for one day. Open an interactive shell, create a file in/tmp, exit, then use a non-interactivedocker execcommand to list that file. - Try opening
bashin an Alpine container. When it fails, explain whyshworks and what that tells you about minimal images.
Summary
docker execstarts a new process inside an existing running container.- It takes a container name or ID, not an image tag.
-itis the standard combination for interactive shells; non-interactive commands usually should not allocate a TTY.- The exec process shares the container’s filesystem, mounts, network namespace, environment defaults, and runtime isolation.
- Manual changes made through a shell are useful for debugging, but real fixes belong in images, configuration, or mounted data.
