How to Run a Cloud Provider CLI in a Docker Container: AWS, Azure, and GCP Setup Guide

How to Run a Cloud Provider CLI in a Docker Container

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:

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

Troubleshooting

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

Exit mobile version