Docker login works for stage: build but fails for stage: deploy in the same pipeline

I am experiencing an issue “Error: Cannot perform an interactive login from a non TTY device” in a GitLab CI/CD pipeline. The pipeline has four stages build, test, review, and deploy, however, “$ docker login -u $REGISTRY_USER -p $REGISTRY_PASSWORD $CI_REGISTRY” works fine for ‘stage: build’, but fails for ‘stage: deploy’. Found this article so the ‘master’ branch is protected, but it did not help.

build_job:
   stage: build
   image: docker:20.10.16
   services:
     - name: docker:20.10.16-dind
       alias: docker
   variables:
     CONTAINER_TEST_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
     DOCKER_HOST: tcp://docker:2375
     DOCKER_DRIVER: overlay2
     DOCKER_TLS_CERTDIR: ""
   script:
     - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
     - docker build -t $CONTAINER_TEST_IMAGE .
     - docker push $CONTAINER_TEST_IMAGE


# LOGS: 
$ docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
deploy_job:
  stage: deploy
  image: docker:20.10.16
  services:
    - name: docker:20.10.16-dind
      alias: docker
  variables:
    CONTAINER_RELEASE_IMAGE: $CI_REGISTRY_IMAGE:latest
    DOCKER_HOST: tcp://docker:2375
    DOCKER_DRIVER: overlay2
    DOCKER_TLS_CERTDIR: ""
  script:
    - docker login -u $REGISTRY_USER -p $REGISTRY_PASSWORD $CI_REGISTRY
    - docker run -d -p 3000:3000 $CONTAINER_RELEASE_IMAGE
  environment: 
    name: production/$CI_COMMIT_REF_SLUG
    url: https://$CI_ENVIRONMENT_SLUG.example.com


#LOGS:
$ docker login -u $REGISTRY_USER -p $REGISTRY_PASSWORD $CI_REGISTRY
Error: Cannot perform an interactive login from a non TTY device
Cleaning up project directory and file based variables
00:00
ERROR: Job failed: exit code 1

Any help, please? Thanks!

The second revision of my initial reply to your question on Stackoverflow regarding variable interpolation during Docker hub or Docker registry logins in Gitlab CI/CD scripts for the opportunity to have it answered here in the forums, too, in an improved revision:


TL;DR: Provide the right credentials, otherwise the error message might not tell you what went wrong.

Upfront summary of recommendations:

  1. Always quote the variables, otherwise it is easy to miss if they are unset as they will magically disappear.
  2. Provide the password via standard input (stdin), this is a better fit for CI/CD pipelines.
  3. Handle all the errors, this is builtin. (2nd. Revision Bonus)
    script:
     - echo "${REGISTRY_PASSWORD:?}" | docker login --password-stdin -u "${REGISTRY_USER:?}" -- "${CI_REGISTRY:?}"
     - docker build -t "${CONTAINER_TEST_IMAGE:?}" .
     - docker push "$CONTAINER_TEST_IMAGE"

The

Error: Cannot perform an interactive login from a non TTY device

emitted by the docker(1) utility for a docker-login(1) command like yours in the Gitlab CI/CD build_job job scripts’ first line:

docker login -u $REGISTRY_USER -p $REGISTRY_PASSWORD $CI_REGISTRY

The message is a bit misleading because docker-login is quite lax here. Even you specified to read the password as string from the command-line (-p or --password option), if the password is missing or wrong, docker login will still ask for it nevertheless.

As this makes no sense within a Gitlab CI/CD automation - there is no interactive terminal (TTY) to obtain the password input from the user - the docker login command is at least clever enough to realize that and quits with the bespoken error message.

So if you get that error, you were not able to properly provide the password.

The Missing Credentials Docker Login Situation in Gitlab CI/CD

You can change the docker login command to make it a bit more robust, especially within CI runs, I would recommend two changes:

  1. Always quote "$VARIABLES"
  2. Provide the password via standard input (stdin)

Let us shortly review the usage and better understand the problem to then make these two suggestions to that it becomes clearer why I suggest them.

From docker login --help:

$ docker login --help

Usage:  docker login [OPTIONS] [SERVER]

Log in to a registry.
If no server is specified, the default is defined by the daemon.

Options:
  -p, --password string   Password
      --password-stdin    Take the password from stdin
  -u, --username string   Username

Given your original command-line from the build_job:script:

docker login -u $REGISTRY_USER -p $REGISTRY_PASSWORD $CI_REGISTRY

and considering an error and all those three Gitlab CI/CD variables are empty, the command-line would look like the following:

docker login -u -p

That is login with the username “-p” to the default server.

You can simulate that on your own shell by running this command with standard-input closed:

$ 0<&- docker login -u -p
Error: Cannot perform an interactive login from a non TTY device

Exactly that error message.

Always quote "$VARIABLES"

Looking closer at the actual command-line, the error message is a reminder to always quote the variables. Also for explicitness use the options delimiter --:

docker login -u "$REGISTRY_USER" -p "$REGISTRY_PASSWORD" -- "$CI_REGISTRY"

In the error case, those variables are empty the command then is:

docker login -u "" -p "" -- ""

The behaviour is still the same thought, the docker login command still tries hard, and it is not yet giving a good message.

NOTE: It’s not solving any of the problems incl. the messaging but we at least handle the case of empty or undefined variables, thanks to the quoting they do not disappear any longer:

0<&- docker login -u "" -p "" -- ""
Error: Cannot perform an interactive login from a non TTY device

This is important, always quote "$VARIABLES", especially when used as parameters in scripts. Use double-quotes².².³ (") to enclose the dollar sign and variable name.

Quote, Substitute and Handle Error

More complex substitutions may require quoting the variable name as well, e.g. if it is ambiguous, substitution requires it²..², or you feel it helps readability, then use curly brackets ({/}) for it. Some examples:

Environment Parameter Name Ambiguity

"${REGISTRY}_USER"

Ambiguity: Same as "$REGISTRY"_USER, "$REGISTRY_USER" would look for the parameter REGISTRY_USER, not REGISTRY.

Environment Parameter Error Handling (Bonus)

"${REGISTRY_USER:?error}"

Error handling: If parameter REGISTRY_USER is empty or unset, the expansion of error (or a message indicating it is unset if error is omitted) shall be written to standard error and the shell exits with a non-zero exit status. Otherwise, the value of parameter REGISTRY_USER shall be substituted.

This handling is a kind of magic within Gitlab CI/CD variable usage, because the runner will mark the job as failed, regardless of any other error handling (or overrides of it). Lets docker login YOLO style:

    - docker login -u "$REGISTRY_USER" -p "$REGISTRY_PASSWORD" -- "$CI_REGISTRY" || echo "YOLO!"
$ docker login -u "$REGISTRY_USER" -p "$REGISTRY_PASSWORD" -- "$CI_REGISTRY" || echo "YOLO!"
Error: Cannot perform an interactive login from a non TTY device
YOLO!
Job succeeded

And now the same YOLO style but with error checking that the input parameters are non-empty:

    - docker login -u "${REGISTRY_USER:?}" -p "${REGISTRY_PASSWORD:?}" -- "${CI_REGISTRY:?}" || echo "YOLO!"
$ docker login -u "${REGISTRY_USER:?}" -p "${REGISTRY_PASSWORD:?}" -- "${CI_REGISTRY:?}" || echo "YOLO!"
/bin/bash: line 64: REGISTRY_USER: parameter null or not set
ERROR: Job failed: exit code 1

(bash shell in debian based docker image used in gitlab-runner)

$ docker login -u "${REGISTRY_USER:?}" -p "${REGISTRY_PASSWORD:?}" -- "${CI_REGISTRY:?}" || echo "YOLO!"
/bin/sh: eval: line 64: REGISTRY_USER: parameter not set or null
ERROR: Job failed: exit code 2

(sh shell in busybox based docker image used in gitlab-runner)

Ignoring the difference of the number but confirming it is always non-zero, the Gitlab runner marks the job as failed despite error-exiting for the command has been disabled.

This shows you can have the error handling on the configuration apart from error handling with individual build commands.

The --password-stdin option

Therefore, within a non-interactive session (like the gitlab-runner) it is strongly recommended:

Use the --password-stdin option. Not only to prevent the warning in the case of a successful login but to make docker login to actually complain about the empty username in the first place! Also, it allows specifying it before the username argument option to remove another ambiguity:

echo "$REGISTRY_PASSWORD" | docker login --password-stdin -u "$REGISTRY_USER" -- "$CI_REGISTRY"

Now even if the variables are missing, the command still fails in a good way:

echo "" | docker login --password-stdin -u "" -- ""
Must provide --username with --password-stdin

And with error handling:

echo "${REGISTRY_PASSWORD:?}" | docker login --password-stdin -u "${REGISTRY_USER:?}" -- "${CI_REGISTRY:?}"
$ echo "${REGISTRY_PASSWORD:?}" | docker login --password-stdin -u "${REGISTRY_USER:?}" -- "${CI_REGISTRY:?}"
/bin/sh: eval: line 64: REGISTRY_PASSWORD: parameter not set or null
/bin/sh: eval: line 64: REGISTRY_USER: parameter not set or null
ERROR: Job failed: exit code 2

Catches even two unset/empty configuration parameters as empty and stops before executing the command. Note here that this is a shell without the pipefail option. This is real error handling on the variables/parameters.

  1. Always quote the variables, otherwise it is easy to miss if they are unset as they will magically disappear.
  2. Provide the password via standard input (stdin), this is a better fit for CI/CD pipelines.
  3. Handle all the errors, this is builtin.

@hakre, again a very detailed and clear answer, which helps a lot. Thank you!

Thanks @frdante , personally the best takeaway for me from this second edition is the error handling as in "${REGISTRY_PASSWORD:?}", can’t be easier for trouble-shooting Gitlab CI/CD variables in sh/bash containers (no clue about powershell). Thanks for your prompt feedback again!