Exec vs Attach: Interacting with Containers

Once a container is running, it feels like a black box. How do you see what’s happening inside? How do you run a database migration or inspect a configuration file?

You have two primary tools: attach and exec. They sound similar, but they operate on fundamentally different principles.

1. The Difference: PID 1 vs New Process

graph TD
  User((User))

  subgraph Host [Host Machine]
      DockerCLI[Docker CLI]

      subgraph Container [Container Boundary (PID Namespace)]
          PID1[PID 1: App]
          PID2[PID 2: /bin/bash]
      end
  end

  User -->|docker attach| DockerCLI
  DockerCLI -.->|Stream Stdout/Stdin| PID1

  User -->|docker exec| DockerCLI
  DockerCLI -->|Spawn New Process| PID2

1. docker attach (The Main Line)

  • Target: Connects your terminal’s standard input/output/error to the primary process (PID 1).
  • Behavior: You are “becoming” the screen and keyboard for the running app.
  • Danger: If you hit Ctrl+C, you send SIGINT to PID 1, killing the container.
  • Escape: Use Ctrl+P then Ctrl+Q to detach without killing.

2. docker exec (The Sidecar)

  • Target: Creates a new process inside the existing container’s namespaces.
  • Behavior: It’s like SSH-ing into a server. You get a fresh shell (e.g., /bin/bash).
  • Safety: If you hit Ctrl+C, you only kill your shell process (e.g., PID 25), not the main app (PID 1).

2. Interactive: Namespace Explorer

Visualize how attach and exec interact with the container’s process space.

Actions
Container Namespace
PID 1
PID 45
> Container running...

3. Code Example: The “Attachable” App

To practice docker attach, you need an app that interacts with Standard Input (Stdin) and Standard Output (Stdout).

This Java app echoes whatever you type. Perfect for testing attach.

import java.util.Scanner;

public class EchoChamber {
    public static void main(String[] args) {
        System.out.println("Echo Chamber Started. Type something:");

        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()) {
            String line = scanner.nextLine();
            System.out.println("You said: " + line);
            if ("exit".equalsIgnoreCase(line)) {
                break;
            }
        }
    }
}

Try it:

  1. Run: docker run -it --name echo my-java-app
  2. Detach: Ctrl+P, Ctrl+Q
  3. Attach again: docker attach echo

This Go app logs a counter every second but also listens for user input.

package main

import (
    "bufio"
    "fmt"
    "os"
    "time"
)

func main() {
    // Background ticker
    go func() {
        i := 0
        for {
            time.Sleep(2 * time.Second)
            fmt.Printf("[Log] Ticker %d\n", i)
            i++
        }
    }()

    fmt.Println("Interactive Shell ready. Type commands:")
    scanner := bufio.NewScanner(os.Stdin)
    for scanner.Scan() {
        text := scanner.Text()
        fmt.Printf("Received: %s\n", text)
    }
}

Debug Tip: If you docker attach to this, you will see the “[Log] Ticker” messages interleaved with your typing. This is why docker logs is usually preferred for viewing output, and docker attach is reserved for interactive sessions.