I got tired, a long time ago, of installing the AWS CLI on one laptop, the Azure CLI on another, and then discovering version mismatches between what’s on my machine and what’s on a teammate’s machine when we’re debugging the same deployment. The fix I landed on, and the one most DevOps teams eventually land on too, is to stop installing cloud CLIs locally altogether and run them from containers instead.
This guide covers exactly how to do that for AWS, Azure, and GCP — including authentication, credential persistence, and building your own multi-cloud CLI image.
Why Run Cloud CLIs in Containers?
Before the how, the why:
- No local pollution. Python version conflicts, Node dependency clashes, and CLI tool version drift never touch your host machine.
- Reproducibility. Everyone on the team runs the exact same CLI version, pinned by image tag.
- Portability. The same container works identically on Linux, macOS, and Windows (via Docker Desktop).
- CI/CD friendliness. The same image you use locally can be dropped straight into a GitLab CI or GitHub Actions job.
- Easy version pinning. Need to test against an older CLI version for a legacy pipeline? Just pull a different tag.
AWS CLI in a Container
AWS publishes an official CLI image.
docker pull amazon/aws-cli:2.17.0
Running a Single Command
docker run --rm amazon/aws-cli:2.17.0 --version
Expected output:
aws-cli/2.17.0 Python/3.11.6 Linux/6.1.0 exe/x86_64.docker
Passing Credentials
The cleanest way is to mount your existing ~/.aws directory as read-only:
docker run --rm -it \
-v ~/.aws:/root/.aws:ro \
amazon/aws-cli:2.17.0 s3 ls
Expected output (assuming you have buckets):
2026-01-15 09:32:11 my-app-logs
2026-03-02 14:11:47 terraform-state-prod
Alternatively, pass credentials as environment variables — useful in CI where you don’t want a mounted config directory:
docker run --rm \
-e AWS_ACCESS_KEY_ID=AKIA... \
-e AWS_SECRET_ACCESS_KEY=... \
-e AWS_DEFAULT_REGION=us-east-1 \
amazon/aws-cli:2.17.0 ec2 describe-instances
Creating a Shell Alias
To make the container feel like a native binary, add this to your .bashrc or .zshrc:
alias aws='docker run --rm -it -v ~/.aws:/root/.aws:ro -v "$PWD":/aws amazon/aws-cli:2.17.0'
Now aws s3 ls works exactly like the local binary would, transparently running inside a container.
Azure CLI in a Container
Microsoft publishes an official Azure CLI image.
docker pull mcr.microsoft.com/azure-cli:2.63.0
Interactive Login
docker run -it mcr.microsoft.com/azure-cli:2.63.0 az login
This prints a device-code URL and code:
To sign in, use a web browser to open the page https://microsoft.com/devicelogin
and enter the code ABCD1234 to authenticate.
Persisting Login Across Container Runs
By default, once the container exits, your login session is gone. Persist the ~/.azure config directory with a named volume:
docker volume create azure-cli-config
docker run -it \
-v azure-cli-config:/root/.azure \
mcr.microsoft.com/azure-cli:2.63.0 az login
Subsequent runs reuse that same volume and stay authenticated:
docker run --rm \
-v azure-cli-config:/root/.azure \
mcr.microsoft.com/azure-cli:2.63.0 az account show
Expected output:
{
"environmentName": "AzureCloud",
"id": "b1234567-89ab-cdef-0123-456789abcdef",
"isDefault": true,
"name": "Pay-As-You-Go",
"user": {
"name": "you@example.com",
"type": "user"
}
}
Service Principal Login (Non-Interactive, CI-Friendly)
docker run --rm mcr.microsoft.com/azure-cli:2.63.0 az login \
--service-principal \
-u $AZURE_CLIENT_ID \
-p $AZURE_CLIENT_SECRET \
--tenant $AZURE_TENANT_ID
Google Cloud CLI (gcloud) in a Container
Google publishes official images under gcr.io/google.com/cloudsdktool/google-cloud-cli.
docker pull gcr.io/google.com/cloudsdktool/google-cloud-cli:latest
Authenticating
docker run -it \
-v gcloud-config:/root/.config/gcloud \
gcr.io/google.com/cloudsdktool/google-cloud-cli:latest \
gcloud auth login
Using a Service Account Key
For automation, mount a service account JSON key file and activate it:
docker run --rm \
-v /path/to/key.json:/tmp/key.json:ro \
gcr.io/google.com/cloudsdktool/google-cloud-cli:latest \
gcloud auth activate-service-account --key-file=/tmp/key.json
Listing Compute Instances
docker run --rm \
-v gcloud-config:/root/.config/gcloud \
gcr.io/google.com/cloudsdktool/google-cloud-cli:latest \
gcloud compute instances list
Expected output:
NAME ZONE MACHINE_TYPE STATUS
web-server us-central1-a e2-medium RUNNING
Building a Single “Multi-Cloud” CLI Image
Rather than switching between three separate images, many teams build one image with all three CLIs installed, so a single container can talk to AWS, Azure, and GCP in the same script.
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y \
curl unzip python3 python3-pip apt-transport-https \
lsb-release gnupg ca-certificates && \
rm -rf /var/lib/apt/lists/*
# AWS CLI v2
RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \
unzip awscliv2.zip && ./aws/install && rm -rf awscliv2.zip aws
# Azure CLI
RUN curl -sL https://aka.ms/InstallAzureCLIDeb | bash
# Google Cloud CLI
RUN curl -sSL https://sdk.cloud.google.com | bash -s -- --disable-prompts && \
ln -s /root/google-cloud-sdk/bin/gcloud /usr/local/bin/gcloud
WORKDIR /workspace
ENTRYPOINT ["/bin/bash"]
Build and run it:
docker build -t multicloud-cli:1.0 .
docker run --rm -it multicloud-cli:1.0
Inside the container, verify all three:
aws --version
az --version
gcloud --version
Using Compose to Manage Credential Volumes
If you regularly switch between clouds, a docker-compose.yml makes this cleaner than remembering long docker run commands:
services:
aws-cli:
image: amazon/aws-cli:2.17.0
volumes:
- ~/.aws:/root/.aws:ro
entrypoint: aws
azure-cli:
image: mcr.microsoft.com/azure-cli:2.63.0
volumes:
- azure-config:/root/.azure
gcloud-cli:
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:latest
volumes:
- gcloud-config:/root/.config/gcloud
volumes:
azure-config:
gcloud-config:
Run any of them with:
docker compose run --rm aws-cli s3 ls
docker compose run --rm azure-cli az group list
docker compose run --rm gcloud-cli gcloud projects list
Understanding What’s Actually Happening Under the Hood
It’s worth being clear about what containerizing a CLI does and doesn’t change. The aws, az, and gcloud binaries are ordinary programs — running them inside a container doesn’t make them faster or give them special access to anything. What changes is isolation: the container gets its own filesystem layer, its own set of installed dependencies, and (unless you mount your credential directory in) no access to your host’s files at all by default. That last point is exactly why the -v ~/.aws:/root/.aws:ro pattern matters — without it, the CLI inside the container has no way to find your credentials, because containers don’t inherit anything from the host filesystem unless you explicitly mount it.
This also explains why --rm shows up in almost every example above: without it, Docker leaves a stopped container behind after every single command, and running dozens of aws s3 ls calls a day would otherwise leave dozens of dead containers cluttering docker ps -a.
Handling Output Files and Local Directories
A common need is running a CLI command that writes output to a local file — for example, downloading an S3 object or exporting a Terraform plan. Since the container has its own isolated filesystem, you need to mount your working directory in as well as your credentials:
docker run --rm \
-v ~/.aws:/root/.aws:ro \
-v "$PWD":/workspace \
-w /workspace \
amazon/aws-cli:2.17.0 s3 cp s3://my-bucket/report.csv ./report.csv
Expected output:
download: s3://my-bucket/report.csv to ./report.csv
The -w /workspace flag sets the container’s working directory to match the mounted volume, so relative paths in the command behave the way you’d expect.
Integrating into CI/CD Pipelines
The exact same images work as pipeline steps. A GitHub Actions example using the AWS CLI container directly (rather than the aws-actions/configure-aws-credentials action) looks like this:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Sync to S3
run: |
docker run --rm \
-e AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }} \
-e AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }} \
-e AWS_DEFAULT_REGION=us-east-1 \
-v "$PWD":/workspace -w /workspace \
amazon/aws-cli:2.17.0 s3 sync ./dist s3://my-app-bucket
The advantage over installing the CLI directly in the CI runner is version pinning — the pipeline behaves identically regardless of what’s baked into the runner image, and upgrading the CLI version is a one-line tag change.
Performance Considerations
Containerized CLIs add a small amount of overhead versus a natively installed binary — mainly image pull time on first use (subsequent runs use the cached image) and a few hundred milliseconds of container startup per invocation. For interactive use this is imperceptible; for scripts issuing hundreds of CLI calls in a tight loop, consider starting a single long-lived container and issuing multiple commands inside it via docker exec, rather than paying the startup cost per call:
docker run -d --name aws-session --entrypoint sleep \
-v ~/.aws:/root/.aws:ro amazon/aws-cli:2.17.0 infinity
docker exec aws-session aws s3 ls
docker exec aws-session aws ec2 describe-instances
docker stop aws-session && docker rm aws-session
Security Considerations
- Never bake credentials into an image. Layers are cached and can leak secrets even after deletion from later layers.
- Prefer named volumes or bind mounts for credential storage over environment variables when running interactively, since environment variables are visible via
docker inspectand process listings on the host. - Use short-lived tokens where possible — AWS STS assume-role sessions, Azure managed identities, and GCP workload identity federation all reduce the blast radius of a leaked credential.
- Mount credential directories read-only (
:ro) whenever the CLI doesn’t need to write back to them. - Rotate CI service principal / service account credentials regularly, and scope their IAM permissions to the minimum required for the pipeline.
Troubleshooting
- “Unable to locate credentials” (AWS) — double check the mount path matches the CLI’s expected config location (
/root/.awsfor therootuser inside the container, not/home/user/.awsunless you changed the container’s user). - Azure login opens a browser but the container has no browser — this is expected; use the device code flow (
az loginwithout--service-principal) shown above, or use a service principal in headless environments. - gcloud commands fail with
PERMISSION_DENIED— verify the active account/project withgcloud config listbefore assuming the credentials themselves are bad. - Volume permissions errors on Linux hosts — if the container runs as a non-root user internally, you may need to
chownthe mounted directory or pass--user $(id -u):$(id -g)to match host file ownership.
Summary
Running cloud CLIs inside Docker containers removes an entire category of “works on my machine” problems from multi-cloud DevOps workflows. Official images exist for AWS, Azure, and GCP, authentication can be persisted across runs using named volumes, and it’s straightforward to combine all three into a single multi-cloud utility image for scripts that need to touch more than one provider. The same pattern scales cleanly into CI/CD, where these exact images can be reused as pipeline steps.
References
- AWS CLI Docker image: https://hub.docker.com/r/amazon/aws-cli
- Azure CLI Docker image: https://learn.microsoft.com/en-us/cli/azure/run-azure-cli-docker
- Google Cloud CLI Docker image: https://cloud.google.com/sdk/docs/downloads-docker
- Docker Compose file reference: https://docs.docker.com/reference/compose-file/
