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

  1. You run docker exec with a container name or ID. The Docker client sends the request to the Docker daemon over its local API connection.
  2. 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.
  3. Docker creates an exec instance with the requested command, user, working directory, environment overrides, and terminal settings.
  4. 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.
  5. 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.
  6. 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 makes docker exec commands readable and repeatable.
  • Use docker exec command args without -t in scripts so output is easier to parse and exit codes are reliable.
  • Use -it only for human interactive sessions.
  • Use sh -c when you need shell features such as pipes, redirects, variable expansion, or &&.
  • Prefer sh for portability; use bash only 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 exec is local to one container. In Compose projects, use the service container name from docker compose ps or use docker compose exec SERVICE COMMAND.

Practice Exercises

  1. Start an nginx:1.27-alpine container named inspect-web. Use docker exec to print the first two lines of /etc/os-release, then remove the container. Hint: use sh -c for the pipeline.
  2. Run an alpine:3.20 container that stays alive for one day. Open an interactive shell, create a file in /tmp, exit, then use a non-interactive docker exec command to list that file.
  3. Try opening bash in an Alpine container. When it fails, explain why sh works and what that tells you about minimal images.

Summary

  • docker exec starts a new process inside an existing running container.
  • It takes a container name or ID, not an image tag.
  • -it is 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.