Home Technical Support Secure Docker Login and CI/CD Script Syntax in GitLab Pipelines

Secure Docker Login and CI/CD Script Syntax in GitLab Pipelines

Last updated on Jan 06, 2026

Secure Docker Login and CI/CD Script Syntax in GitLab Pipelines

Introduction

When you automate container builds and deployments with GitLab CI/CD, handling credentials safely is essential. A common pattern—echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY—illustrates three best‑practice concepts:

  1. Passing secrets via --password-stdin instead of the insecure -p flag.
  2. Using a here‑document (<<EOF) to feed a block of commands to ssh.
  3. Leveraging echo and the pipe (|) to stream the password into Docker.

This article breaks down each piece, explains why it matters, and shows a complete, production‑ready example you can copy into your own .gitlab-ci.yml file.


1. Why Prefer --password-stdin Over -p

The security problem with -p

docker login -u myuser -p mysecret registry.example.com
  • The password becomes part of the process command line.
  • Anyone with access to the host can see it with tools like ps aux or /proc/<pid>/cmdline.
  • The value may also be captured in shell history or CI job logs if the runner echoes the command.

How --password-stdin mitigates the risk

echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
  • The password is read from standard input, never appearing in the command line.
  • It is not stored in the runner’s process table, reducing exposure to other users.
  • The secret is still protected by GitLab’s built‑in masked variable feature, which prevents it from being printed in job logs.

Quick comparison

Feature -p (plain) --password-stdin
Visibility in ps ✅ Yes ❌ No
Stored in shell history ✅ Yes ❌ No
Works with masked CI variables ❌ May leak ✅ Safe
Recommended by Docker docs ❌ No ✅ Yes

2. Understanding the EOF (Here‑Document) Syntax

What is a here‑document?

A here‑document is a way to embed a multi‑line string directly in a shell script. The syntax looks like:

ssh user@host <<EOF
  docker pull "$CI_REGISTRY/image:latest"
  docker stop myapp || true
  docker rm myapp || true
  docker run -d --name myapp "$CI_REGISTRY/image:latest"
EOF
  • <<EOF tells the shell: “Everything that follows, up to a line that contains only EOF, should be fed to the preceding command’s standard input.”
  • The delimiter (EOF, EOT, END, etc.) can be any word you choose, as long as it matches the closing line.

Why use it with ssh?

  • Readability – You can write a full series of commands without escaping quotes or line‑breaks.
  • Atomic execution – The remote shell receives the entire block as one script, reducing the risk of partial execution.
  • No intermediate files – The commands are streamed directly, avoiding temporary scripts on the remote host.

Example in a GitLab CI job

deploy_production:
  stage: deploy
  image: alpine:latest
  script:
    - |
      echo "$CI_REGISTRY_PASSWORD" |
      docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
    - |
      ssh deploy@prod.example.com <<'EOF'
        set -e
        docker pull "$CI_REGISTRY/image:${CI_COMMIT_SHA}"
        docker stop web || true
        docker rm web || true
        docker run -d --name web -p 80:80 "$CI_REGISTRY/image:${CI_COMMIT_SHA}"
      EOF

Notice the single‑quoted <<'EOF' – this prevents variable expansion on the local machine, letting the remote side handle its own environment variables.


3. The Role of echo and the Pipe (|)

How the pipeline works

echo "$CI_REGISTRY_PASSWORD" | docker login ... --password-stdin
  1. echo outputs the password (as a single line) to its standard output.
  2. The pipe (|) redirects that output to the standard input of docker login.
  3. Docker reads the password from stdin, authenticates, and then discards the input.

Why not just write the password in the command?

  • Directly embedding the password (-p "$CI_REGISTRY_PASSWORD") would expose it in the process list.
  • Using echo + pipe keeps the secret out of the command line while still allowing a one‑liner that fits neatly into a GitLab CI script: array.

Alternative syntax (for completeness)

script:
  - docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" <<<"$CI_REGISTRY_PASSWORD"

The <<< here‑string does the same thing as echo … |, but echo | is more universally understood across POSIX shells.


4. Putting It All Together – A Full CI/CD Example

stages:
  - build
  - push
  - deploy

variables:
  IMAGE: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"

build:
  stage: build
  image: docker:latest
  services: [docker:dind]
  script:
    - docker build -t "$IMAGE" .

push:
  stage: push
  image: docker:latest
  services: [docker:dind]
  script:
    - echo "$CI_REGISTRY_PASSWORD" |
      docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
    - docker push "$IMAGE"

deploy:
  stage: deploy
  image: alpine:latest
  script:
    - |
      ssh deploy@prod.example.com <<'EOF'
        set -e
        docker pull "$IMAGE"
        docker stop app || true
        docker rm app || true
        docker run -d --name app -p 80:80 "$IMAGE"
      EOF
  • This pipeline builds an image, pushes it securely, and then deploys it on a remote host using a clean, readable here‑document.

Common Questions

Question Answer
Do masked variables still appear in job logs? No. GitLab masks them (replaces with *****) when they would otherwise be printed.
Can I use -p if I run the pipeline on a private runner? Technically possible, but Docker recommends --password-stdin for any environment where other users could inspect processes.
What if my password contains newlines? Use a masked variable that does not contain newlines, or store the credential in a Docker config file instead.
Is EOF case‑sensitive? Yes. The opening delimiter must match the closing one exactly (EOF, eof, END, etc.).
Do I need to escape $ inside the here‑document? If you want the remote host to expand the variable, leave it unescaped. Use a single‑quoted delimiter (<<'EOF') to prevent local expansion.

Tips for Secure CI/CD Pipelines

  1. Always mask secret variables in GitLab (Settings > CI/CD > Variables).
  2. Prefer --password-stdin for any Docker authentication.
  3. Limit SSH access – use key‑based auth, restrict the key to the required commands, and consider using a bastion host.
  4. Enable job timeout and resource limits on runners to avoid runaway processes.
  5. Audit your .gitlab-ci.yml for accidental exposure of secrets (e.g., stray echo $SECRET without piping).

Summary

Secure Docker login, clean multi‑line remote execution, and proper use of shell pipelines are foundational skills for reliable GitLab CI/CD pipelines. By:

  • Passing passwords via --password-stdin,
  • Embedding remote command blocks with a here‑document (<<EOF), and
  • Streaming secrets with echo … |,

you protect credentials, improve script readability, and keep your deployment process both safe and maintainable. Apply these patterns today and enjoy a smoother, more secure DevSecOps workflow.