Understanding docker run --attach option

I’m a newbie with Docker and I’m pretty stuck at the how the --attach option works with docker run.

I would say that I’ve somehow understood the following command, as far as I understood with the -it Docker creates a pseudo-tty where the /bin/bash command is executed and the stdin and stdout of my local terminal is linked to the pseudo-tty.

$ docker run --rm -it ubuntu /bin/bash
root@d5e3551114ca:/# ls
bin  boot  dev  etc  home  lib  lib32  lib64  libx32  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

What I do not understand is the meaning of the following commands:

  • In this case I see no output on my local terminal, but in the docker logs I can see that the keystrokes are intercepted and executed
docker run --rm --attach stdin -i ubuntu /bin/bash
  • Here the container is started and stopped immediatelly
docker run --rm --attach stdin ubuntu /bin/bash
  • Here the container is started but keystrokes are not intercepted nor the output is shown
docker run --rm --attach stdin -t ubuntu /bin/bash
  • Here I can see the output but keystrokes are not intercepted
$ docker run --rm --attach stdout -t ubuntu /bin/bash
root@b47a46abdf34:/# ls

Afair --attach is used to attach stdin, stdout and stderr to a container, so it can be used within chained pipe commands.

I am not really sure if the default behavior changed and --attach is not necessarily required for this anymore, as stdin and stdout are available anyway:

echo "test" | docker run --rm alpine echo "container:  $(</dev/stdin)" | echo "outside $(</dev/stdin)"
# output: outside container: test

It doesn’t matter, if you add -i for interactive and/or -a stdin -a stdout. Though if you just add -a stdin without -a stdout, you will see the host’s stdout instead of the piped in value.

Update: whoever reads this: the example and drawn conclusion from it are wrong.

Thanks for the answer.
I was trying your commands and I ran the following:

echo "hello there!" | docker run --rm -a stdout alpine echo "container:" $(</dev/stdin) | echo "terminal:" $(</dev/stdin)

As I wasn’t attaching the stdin I was expecting to get back just terminal: container: instead I got terminal: container: hello there!, the same result I get back when I execute:

echo "hello there!" | docker run --rm -a stdin -a stdout alpine echo "container:" $(</dev/stdin) | echo "terminal:" $(</dev/stdin)

So, I’m still pretty confused, is the standard input always attached?

1 Like

I can’t say that I know everythng about these parameters, but extending @meyay’s answer here is what I think can help you to understand a little bit better what these parameters are for.

Docker has a client-server architecture. The Docker client is just communicating with the server and tells the server to run a container. The standard input, standard error and standard output streams are still on the server. Normally you wouldn’t see the output or error messages and you wouldn’t be able to run a command on the server that waits for an input since the container is not a child process of the client even if both are on the same machine. This is why Docker Desktop can work because the client can communicate with the server inside the virtual machine.

--attachcan be used to attach to these three streams so the standard input of your client could be forwarded to the standard input of the docker container on the (optionally) remote server, stdout an stderr of the server could be forwarded to your client’s stdout and stderr so you can see the output of the process you run in the container.

--attach is not compatible with --detach or -d which instructs the docker client not to attach to any streams., so the container can “run in the backround”. Well, it always runs in the background. The question is whether the client attaches to the standard streams through the Docker socket or not. For example you can attach to the standard streams, see the output and kill the client process from an other terminal. It will not kill the container.

By default, docker run will attach to stdout and stderr but not stdin.

docker run --name notstdin bash
ubuntu@docker2:~$ docker container inspect notstdin | grep -i std
        "Name": "/notstdin",
            "AttachStdin": false,
            "AttachStdout": true,
            "AttachStderr": true,
            "OpenStdin": false,
            "StdinOnce": false,

If you use --attach stdin it will only attach to stdinand nothing else`

docker run --name onlystdin --attach stdin bash
ubuntu@docker2:~$ docker container inspect onlystdin | grep -i std
        "Name": "/onlystdin",
            "AttachStdin": true,
            "AttachStdout": false,
            "AttachStderr": false,
            "OpenStdin": false,
            "StdinOnce": false,

This is what I don’t know what it is for, because it is not enough for the container to be able to read the standard input of the Docker client and you can’t even press CTRL+C to stop the client process.

Let’s say you want to run a container and want to be notified only about errors in the terminal. You can attach the standard error stream without the others.

docker run \
  --name onlystderr \
  --attach stderr \
  bash \
  -c 'echo "OUTPUT" > /dev/stdout; echo "ERROR" > /dev/stderr'
# result:
# ERROR

Let’s run: docker logs onlystderr

ERROR
OUTPUT

So the output is written to the logs, you just didn’t see it in real time.

If you inspect the container you will see that only stderr is true:

ubuntu@docker2:~$ docker container inspect onlystderr | grep -i std
            "echo \"OUTPUT\" > /dev/stdout; echo \"ERROR\" > /dev/stderr"
        "Name": "/onlystderr",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": true,
            "OpenStdin": false,
            "StdinOnce": false,
                "echo \"OUTPUT\" > /dev/stdout; echo \"ERROR\" > /dev/stderr"

You also used -i (--interactive) and -t (--tty). The interactive flag will make the client attach to all of the streams and you also set OpenStdin and StdinOnce to true. Now you run bash without any other foreground process to keep the container alive, but you will not be able to stop the container by pressing CTRL+C.

docker run --name interactive -i bash

The good news is you can actually use bash and see the output in the logs. You can also type “exit” and press ENTER to exit from the shell.

Let’s see the output of the inspect. Everything is enabled except tty:

ubuntu@docker2:~$ docker container inspect interactive | grep -i 'std\|tty'
            "AttachStdin": true,
            "AttachStdout": true,
            "AttachStderr": true,
            "Tty": false,
            "OpenStdin": true,
            "StdinOnce": true,

Using -t will get you a tty and you can see the bash prompt.

docker run --name interactivetty -it bash

Now Tty is enabled

ubuntu@docker2:~$ docker container inspect interactivetty | grep -i 'std\|tty'
        "Name": "/interactivetty",
            "AttachStdin": true,
            "AttachStdout": true,
            "AttachStderr": true,
            "Tty": true,
            "OpenStdin": true,
            "StdinOnce": true,

If you run the container in detached mode using -d you will have OpenStdin enabled but nothing else, so you are not attached to the standard streams but it is open on the server keeping the bash process alive.

docker run --name interactivedetach --detach -i bash
ubuntu@docker2:~$ docker container inspect interactivedetach | grep -i 'std'
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "OpenStdin": true,
            "StdinOnce": false,

What happens when you have an interactive terminal and also attach to standard error but nothing else?

docker run \
  --name interactivestderr \
  --attach stderr \
  -i \
  bash \
  -c 'echo "OUTPUT" > /dev/stdout; echo "ERROR" > /dev/stderr'
# result:
# ERROR

inspect

ubuntu@docker2:~$ docker container inspect interactivestderr | grep -i 'std'
            "echo \"OUTPUT\" > /dev/stdout; echo \"ERROR\" > /dev/stderr"
        "Name": "/interactivestderr",
            "AttachStdin": true,
            "AttachStdout": false,
            "AttachStderr": true,
            "OpenStdin": true,
            "StdinOnce": true,
                "echo \"OUTPUT\" > /dev/stdout; echo \"ERROR\" > /dev/stderr"

Here you can see that even if you don’t use --attach stdin but use -i, “AttachStdin” will be true, so the standard input will be kept open and you also attach to the standard input from client side.

updated with one more fact

One more important fact is that if you use -t to get a tty standard output and standard error streams will not be separated so even if you attach to stderr you will not see any output only if you attach to stdout.

ubuntu@docker2:~$ docker run --name ttystderrout -t --attach stderr bash -c 'echo "OUTPUT" > /dev/stdout; echo "ERROR" > /dev/stderr'
ubuntu@docker2:~$ docker run --name ttystderrout2 -t --attach stdout bash -c 'echo "OUTPUT" > /dev/stdout; echo "ERROR" > /dev/stderr'
OUTPUT
ERROR
2 Likes

Can you deepen this sentence? In particular “you wouldn’t be able to run a command on the server that waits for an input”. Maybe you meant “you wouldn’t be able to run a command on the container that waits for an input”?

In addition, how is it possible that the following command:

echo "hello there!" | docker run --rm -d -v $PWD/logs.txt:/tmp/logs.txt alpine /bin/sh -c "echo container: $(</dev/stdin) > /tmp/logs.txt"

prints hello there!" within the logs.txt file if I ran docker run -d?

My point was that the command is not running on your client. The container is running on the server so the command is running on the server too in a container. :slight_smile: Of course if you are not using Docker Desktop or any other remote docker daemon, your client is the server. The container is just isolation and resouce limits. The process “thinks” it is running on a separate machine but it is still running on the host isolated from the rest of the environment.

When I saw @meyay’s post I wasn’t sure how that could work so I tried the command and couldn’t prove what I tried so I assumed I just didn’t understand. Reading your example gave me another way to test it.

Since we use Linux and macOS which also supports the syntax of $(</dev/stdin), it is easy to miss that $(anycommand) is actually running on your host, not in a container. You were able to write “hello there” into the log file because

echo "hello there!" | anycommand

means you write something to the standard output which becomes the standard input of anycommand but that command is interpreted on the host. In this case the command was docker and the following command was interpreted on the host:

docker run --rm -d -v $PWD/logs.txt:/tmp/logs.txt alpine /bin/sh -c "echo container: $(</dev/stdin) > /tmp/logs.txt"

The subshell syntax ($(anycommand)) is interpreted between quotion marks. By the time that command is interpreted by the shell, you have something in the standard input so $(</dev/stdin) can read it. Eventually the following command will be executed:

docker run --rm -d -v $PWD/logs.txt:/tmp/logs.txt alpine /bin/sh -c "echo container: hello there! > /tmp/logs.txt"

If you want to make sure the command is executed in the container, you need to use apostrophes:

echo "hello there!" | docker run --rm -d -v $PWD/logs.txt:/tmp/logs.txt alpine /bin/sh -c 'echo container: $(</dev/stdin) > /tmp/logs.txt'

Now it will write only "container: " to the log file.
This command however would be able to write the log file again properly

echo "hello there!" | docker run --rm -i -v $PWD/logs.txt:/tmp/logs.txt alpine /bin/sh -c 'echo container: $(</dev/stdin) > /tmp/logs.txt'

If I change Metin’s example like this:

echo "test" | docker run --rm alpine echo 'container:  $(</dev/stdin)' | echo "outside $(</dev/stdin)"

you will get

outside container:  $(</dev/stdin)

This is because it just echos a string. There is no shell to interpret it since it is between apostrophes. Your example used /bin/sh -c and everything else was its argument. This is similar to using the eval keyword. So the next command will show that the container cannot read the stdin.

echo "test" | docker run --rm alpine /bin/sh -c 'echo "container:  $(</dev/stdin)"' | echo "outside $(</dev/stdin)"

Here the argument of sh is between apostrophes and it will execute the string it gets.

outside container:

Let’s use -i

echo "test" | docker run --rm -i alpine /bin/sh -c 'echo "container:  $(</dev/stdin)"' | echo "outside $(</dev/stdin)"

It will still not work.
So our next problem is that this </dev/stdin is supported by bash and the alpine linux doesn’t have it by default. Replace alpine with “ubuntu” and replace /dev/sh with /bin/bash and it will work

echo "test" | docker run -i --rm ubuntu /bin/bash -c 'echo "container:  $(</dev/stdin)"' | echo "outside $(</dev/stdin)"
outside container:  test

Or you can use alpine (or ubuntu with /bin/sh) and replace $(</dev/stdin) with $(cat -)

echo "test" | docker run -i --rm alpine /bin/sh -c 'echo "container:  $(cat -)"' | echo "outside $(</dev/stdin)"
outside container:  test

I think we don’t use standard inputs and docker containers often so it is still not obvious to us. This is why I actually enjoyed this playing with the standard streams :slight_smile:

1 Like

I indeed missed it.

The original example I had in mind was: echo "piped through the container" | docker run -i --rm alpine cat - | cat - but then modified it because I thought it might be to boring. But it seems this example would have been not sufficient as well, as it does not provide the intended behavior with -options.

Instead, I opted for the subcommand execution, which made my examples wrong.

Thanks for clarifying it :slight_smile:

Thank you, really a comprehensive answer!