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:
- Passing secrets via
--password-stdininstead of the insecure-pflag. - Using a here‑document (
<<EOF) to feed a block of commands tossh. - Leveraging
echoand 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 auxor/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
<<EOFtells the shell: “Everything that follows, up to a line that contains onlyEOF, 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
echooutputs the password (as a single line) to its standard output.- The pipe (
|) redirects that output to the standard input ofdocker login. - 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 CIscript: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
- Always mask secret variables in GitLab (
Settings > CI/CD > Variables). - Prefer
--password-stdinfor any Docker authentication. - Limit SSH access – use key‑based auth, restrict the key to the required commands, and consider using a bastion host.
- Enable job timeout and resource limits on runners to avoid runaway processes.
- Audit your
.gitlab-ci.ymlfor accidental exposure of secrets (e.g., strayecho $SECRETwithout 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.