What does "the input device is not a TTY" exactly mean in "docker run" output?

This is a command that works:

$ echo 'hi there' | docker run -i ubuntu cat
hi there

This is a command that responds with an error message:

$ echo 'hi there' | docker run -it ubuntu cat
the input device is not a TTY

I would like to figure out exactly what happens here. Not just "remove -t and it'll be fixed".

I know that docker run's -t option stands for "Allocate a pseudo-TTY", and I have read historical overviews of what TTY stands for, but it didn't help me understand what kind of a contract is violated here.

Late answer, but might help someone

docker run/exec -i will connect the STDIN of the command inside the container to the STDIN of the docker run/exec itself.

So

  • docker run -i alpine cat gives you an empty line waiting for input. Type "hello" you get an echo "hello". The container will not exit until you send CTRL+D because the main process cat is waiting for input from the infinite stream that is the terminal input of the docker run.
  • On the other hand echo "hello" | docker run -i alpine cat will print "hello" and exit immediately because cat notices that the input stream has ended and terminates itself.

If you try docker ps after you exit either of the above, you will not find any running containers. In both cases, cat itself has terminated, thus docker has terminated the container.

Now for "-t", this tells the main process inside docker that its input is a terminal device.

So

  • docker run -t alpine cat will give you an empty line, but if you try to type "hello", you will not get any echo. This is because while cat is connected to a terminal input, this input is not connected to your input. The "hello" that you typed did not reach the input of cat. cat is waiting for input that never arrives.
  • echo "hello" | docker run -t alpine cat will also give you an empty line and will not exit the container on CTRL-D but you will not get an echo "hello" because you didn't pass -i

If you send CTRL+C, you get your shell back, but if you try docker ps now, you see the cat container still running. This is because cat is still waiting on an input stream that was never closed. I have not found any useful use for the -t alone without being combined with -i.

Now, for -it together. This tells cat that its input is a terminal and in the same time connect this terminal to the input of docker run which is a terminal. docker run/exec will make sure that its own input is in fact a tty before passing it to cat. This is why you will get a input device is not a TTY if you try echo "hello" | docker run -it alpine cat because in this case, the input of docker run itself is the pipe from the previous echo and not the terminal where docker run is executed

Finally, why would you need to pass -t if -i will do the trick of connecting your input to cat's input? This is because commands treat the input differently if it's a terminal. This is also best illustrated by example

  • docker run -e MYSQL_ROOT_PASSWORD=123 -i mariadb mysql -uroot -p will give you a password prompt. If you type the password, the characters are printed visibly.
  • docker run -i alpine sh will give you an empty line. If you type a command like ls you get an output, but you will not get a prompt or colored output.

In the last two cases, you get this behavior because mysql as well as shell were not treating the input as a tty and thus did not use tty specific behavior like masking the input or coloring the output.

This answer helped me to wrap my head around:

  • by default (without neither -i nor -t options) a Docker container only sends its output to STDOUT,
  • with -i option comes connection to STDIN,
  • -t option pulls in a terminal interface driver, that works on top of STDIN/STDOUT. And when a terminal driver is pulled in, the communication with a container must conform to the terminal interface protocol. Piping a string does not.

A tty indicates you have a terminal, something that would be provided by xterm or one of the many linux command line interfaces. It needs a keyboard and text output interface associated with it. Typical reasons to want this is for color text output support, handling of various key combinations (like the arrow keys), and the ability to move the cursor around the screen.

When you pipe a command into docker like your echo example shows, that pipe is the input, and that pipe does not have a tty interface, it's just a stream of text. Attempting to create a tty with that will fail as the error message indicates.

Can I start tty within the docker? I have some app that stops working the I do not run the docker with -t, but I can not modify the docker start command in production. So I need to make the app think it was started with -t.

It’s not redundant, docker can create a TTY without attaching anything to it. Your output would have characters for color, etc, but your terminal output would not be piped into the input of the container. So the characters you type would be queued up for the next command you run after the docker command exits.