One Docker image with every senior-DevOps CLI baked in, plus PowerShell wrappers
so az, kubectl, terraform, flux, helm, kustomize and azd run inside
the container but feel local. Nothing to install on the host except Docker Desktop,
which you already have. Credentials and config persist in a named volume, so you
log in once.
Tools included: Azure CLI, azd, kubectl, kubelogin, Helm, Kustomize, Terraform, Flux CLI, AWS CLI v2, GitHub CLI (gh), kubectx/kubens, plus jq, yq, git, ssh, make.
- Docker Desktop running (WSL2 or Hyper-V backend — either is fine).
- PowerShell (Windows PowerShell 5.1 or PowerShell 7+).
- No admin rights or other installs required.
Open PowerShell in this folder and run:
powershell -NoProfile -ExecutionPolicy Bypass -File .\setup.ps1setup.ps1 runs every command from the steps below, in order, and then checks the tools:
- checks that Docker Desktop is running with Linux containers
- creates
.envfrom.env.example(an existing.envis kept) - builds the image (
docker compose build) - adds the
Import-Moduleline to your PowerShell profile (a line left over from a moved folder is updated, and a.bakcopy of the profile is saved first) - loads the wrappers
- runs
az,kubectl,terraform,helm,flux,kustomize,azd,gh,kubectxandkubensonce each, in the container, to prove they work
Then it offers the Azure device-code login and the Docker Desktop Kubernetes import. It is safe to run again whenever you like: every step checks before it changes anything.
| Option | What it does |
|---|---|
-SkipBuild |
the image is already built |
-SkipProfile |
leave your PowerShell profile alone |
-SkipVerify |
skip the tool checks |
-Login |
run az login --use-device-code at the end without asking |
-ImportKube |
import Docker Desktop's Kubernetes context at the end without asking |
-NoPrompt |
never ask a question (for automation) |
-ProfilePath <file> |
edit this profile instead of your all-hosts profile |
Exit code: 0 all good, 1 setup could not finish, 2 setup finished but a tool check failed.
That command runs the script in its own PowerShell process, so open a new PowerShell window
afterwards: your profile loads the tools there. (If you start it from your own prompt as
.\setup.ps1, after Set-ExecutionPolicy -Scope Process Bypass, the tools are loaded in that
window too.) If Group Policy blocks scripts altogether, or PowerShell runs in Constrained
Language Mode, the wrappers cannot work there: build with docker compose build and use
docker compose run --rm dev for a shell inside the toolbox.
To undo all of this later, run uninstall.ps1 (see Stop and remove it).
The manual steps, one at a time:
cd devtool
copy .env.example .env # optional; edit versions / proxy / repos path
docker compose build # or: docker build -t devtools:latest .First build pulls a lot (Azure CLI, Terraform, AWS CLI, etc.) and takes a few minutes. Rebuilds are cached and fast.
On a corporate network with TLS interception, the build will fail on certificate errors during the HTTPS downloads. See Troubleshooting.
Add one line to your profile so the wrappers load in every new shell:
if (!(Test-Path $PROFILE)) { New-Item -ItemType File -Path $PROFILE -Force }
notepad $PROFILEAdd (adjust the path to where this folder lives):
Import-Module 'C:\path\to\devtool\Devtools.psm1' -ForceReopen PowerShell. Now az, kubectl, terraform, etc. are the container
versions. Because there's no local az on this machine, these functions simply
become the default — no PATH changes needed. Check what they point at:
Get-DevToolsInfoaz login --use-device-codeOpens a code + URL; complete it in any browser. The token lands in ~/.azure
on the persistent volume, so it survives across every future command and shell.
az account show
terraform init # runs against the folder you're standing in
terraform plan -out tf.plan
kubectl get pods -A
helm upgrade --install myapp ./chart
flux get kustomizationsYour current directory is mounted at /work inside the container, so file-based
commands (terraform, kubectl apply -f, helm ./chart) act on the folder
you're in. For a full interactive shell in the toolbox:
dev # drops you into bash with the same mountsaz aks get-credentials --name <cluster> -g <rg> --overwrite-existing
kubectl get nodes # kubelogin handles the AAD token automaticallyBoth az and kubectl share the same /root volume, so the kubeconfig written
by get-credentials and the AAD token cache from kubelogin are right there for
every later kubectl call.
Instead of AKS, point the toolbox at the cluster built into Docker Desktop.
- Docker Desktop > Settings > Kubernetes > Enable Kubernetes > Apply. Wait until it shows Running. (If the option is missing or greyed out, an admin policy has it locked.)
- Rebuild once so the image has the helper scripts. Only the last layers change:
docker compose build
- Reload the module and import the context:
It ends with
Import-Module 'C:\path\to\devtool\Devtools.psm1' -Force Import-DockerDesktopKube
Connected to Kubernetes v1.xxwhen the container can reach the cluster. - Use it like any other context:
(Prefer another name?
kubectx docker-desktop kubectl get nodes kubenskubectl config rename-context docker-desktop local.)
Why a helper instead of just copying the kubeconfig? Two things break inside a container:
- The toolbox keeps its own kubeconfig on the volume and can't see the Windows one.
- Docker Desktop's context points at
kubernetes.docker.internal/ localhost, which inside a container is the container itself.Import-DockerDesktopKubecopies only thedocker-desktopcontext (the rest of your host kubeconfig is never read into the volume), repoints it athost.docker.internal, and keeps TLS verification on by verifying against the original name (tls-server-name).
Behind a corporate proxy there is a third trap: kubectl would send host.docker.internal
to the proxy, which can't reach your laptop. The image entrypoint appends the Docker Desktop
host names to NO_PROXY (existing entries are kept), so this just works.
Re-run Import-DockerDesktopKube after Reset Kubernetes Cluster (the certificates change).
It is safe to re-run any time; it replaces its own entries and leaves everything else
(AKS contexts, etc.) alone. A backup of the previous file is kept as ~/.kube/config.bak
on the volume.
If the import says the API server did not answer, it prints a direct probe result:
HTTP 000: nothing reachable. Check Kubernetes shows Running, and any VPN / firewall.- Any other code: the host is reachable, so the kubectl error above it is TLS or a proxy.
Edit the # ---- EXTRA TOOLS ---- block at the bottom of the Dockerfile
(k9s, terragrunt, tflint, trivy examples are there, commented), then:
docker compose buildIt's at the bottom on purpose — everything above stays cached, so adding a tool
is a quick rebuild. To upgrade a pinned tool, bump its version in .env
(or the ARG default) and rebuild.
docker compose run --rm dev # one-off shell
# or keep it up and exec in repeatedly:
docker compose up -d dev
docker compose exec dev bashSet REPOS_ROOT in .env to an absolute path (e.g. C:/Users/you/repos) to
mount your real code at /repos; otherwise the project's ./repos folder is used.
This project is already a git repo with an initial commit. Since you can't install
gh on the host, use the one baked into the toolbox. From the project folder:
docker compose build # if you haven't already
gh auth login # choose "Paste an authentication token" (a PAT) — no browser needed
gh repo create devtool --private --source . --remote origin --pushThat creates the repo under your account and pushes main in one shot. gh prints
the URL. Auth persists in the volume, so you only log in once.
Plain-git alternative (if you'd rather create the empty repo on github.com first):
dev # bash shell in the toolbox, project mounted at /work
# inside the container:
git remote add origin https://github.com/<you>/devtool.git
git push -u origin main # use a PAT as the password when prompteduninstall.ps1 is the undo for setup.ps1. In the PowerShell window you use the tools in:
.\uninstall.ps1If scripts are blocked, run powershell -NoProfile -ExecutionPolicy Bypass -File .\uninstall.ps1
instead, then Remove-Module Devtools in the window you were using. It does this, in order:
- removes the
Import-Moduleline from your PowerShell profile (a.bakcopy is saved first). It looks in the profiles of Windows PowerShell 5.1 and of PowerShell 7, which keep theirs in different folders - unloads the wrappers from the window it runs in, so
az,kubectland the rest are the programs installed on the PC again (or "not recognized" when there are none) - stops and removes the toolbox containers:
docker compose down, then anything still running from the image, such as adevshell open in another window - only when you ask: deletes the image and your saved logins
The image takes minutes to build and the volume holds your Azure, GitHub and Kubernetes logins,
so both are kept unless you ask. Your project folder and .env are never touched. It works
with Docker Desktop stopped (the profile and the window are cleaned first) and is safe to run again.
| Option | What it does |
|---|---|
-RemoveImage |
also delete the image devtools:latest (setup.ps1 builds it again) |
-RemoveVolume |
also delete the volume with your saved logins. Asks first |
-Force |
do not ask before deleting the volume |
-SkipProfile |
leave your PowerShell profile alone |
-NoPrompt |
never ask a question (for automation); the volume is then deleted only with -Force |
-ProfilePath <file> |
clean this profile instead of looking for yours |
Exit code: 0 done, 1 could not run (Constrained Language Mode), 2 finished but something
above failed. For everything at once: .\uninstall.ps1 -RemoveImage -RemoveVolume.
PowerShell windows that were already open keep the wrappers until you close them or run
Remove-Module Devtools in them. Only the default names are handled: the image devtools:latest
and the volumes devtools-home (used by the wrappers) and devtools_devtools-home (the one
docker compose run creates, because Compose prefixes volume names with the project name). A name
you chose with DEVTOOLS_IMAGE or DEVTOOLS_VOLUME is left alone.
Every push runs .github/workflows/ci.yml on GitHub's runners (the badge at the top shows the
latest result). It runs three jobs:
- Lint and PowerShell tests (Linux): hadolint on the Dockerfile, shellcheck on every shell
script,
docker compose config, and three PowerShell test scripts that run against a fakedocker:tests/Test-Devtools.ps1runs the realDevtools.psm1and checks the exactdocker runline each wrapper produces (mounts, working directory,-out/-opass-through, exit codes, env overrides).tests/Test-Setup.ps1runssetup.ps1in a scratch copy of the project, once per situation it has to handle: first run, second run, project folder moved, Docker not running, Windows containers, docker missing, wrong folder, build failure, nodocker compose, a broken tool, and each option. It checks the exit code, the messages, the docker calls and the profile file.tests/Test-Uninstall.ps1does the same foruninstall.ps1, and also runssetup.ps1first to prove the profile comes back byte for byte as it was. It covers odd profile lines, a locked profile, wrappers loaded in the session, containers that will not go, a missing or stopped Docker, and each option including the question before the volume is deleted.
- PowerShell on Windows: all three test scripts in Windows PowerShell 5.1 and in PowerShell 7 on
a Windows runner, with a fake
docker.exe. Windows runners cannot run Linux containers, so this covers the PowerShell side only: 5.1 syntax, parameter binding, native argument passing and Windows paths. The runner is thrown away after the job, so it is also where the real default profile folders of both PowerShell editions are tested (and put back afterwards). - Build the image and test it (Linux): a real
docker build, thentests/smoke.shruns inside the image: every tool starts and reports the version pinned in the Dockerfile, the entrypoint extendsNO_PROXY, the Terraform plugin cache is set up.tests/e2e-import.shstarts a fake Kubernetes API on the host with a certificate valid only forkubernetes.docker.internal, then checks that the container reaches it with TLS verified, that only thedocker-desktopcontext is imported (a second context's secret never reaches the volume), that kubectx and kubens work against it, that a re-import is idempotent, and that a dead corporate proxy cannot swallow the host traffic.setup.ps1runs for real against the Docker engine of the runner (build, profile, every tool in its own container), and then once more to prove a second run changes nothing.uninstall.ps1runs for real against the same engine, with a container still running from the image and both volumes present: it stops the container and takes the profile line out, keeps the image and the logins, and then with-RemoveImage -RemoveVolume -Forcedeletes them.
Run the PowerShell tests on your own machine (no Docker needed, and your own profile is not touched):
powershell -NoProfile -ExecutionPolicy Bypass -File tests\Test-Devtools.ps1 # Windows PowerShell 5.1
powershell -NoProfile -ExecutionPolicy Bypass -File tests\Test-Setup.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests\Test-Uninstall.ps1
pwsh -NoProfile -File tests/Test-Devtools.ps1 # PowerShell 7, any OS
pwsh -NoProfile -File tests/Test-Setup.ps1
pwsh -NoProfile -File tests/Test-Uninstall.ps1The image tests need Linux, macOS or WSL with Docker:
docker build -t devtools:ci .
docker run --rm -v "$PWD/tests:/tests:ro" devtools:ci bash /tests/smoke.sh
tests/e2e-import.sh devtools:ci # needs openssl and python3What CI cannot prove: how your corporate proxy and TLS inspection behave, and Docker Desktop's
real Kubernetes (the end-to-end test uses a fake API server on Linux). Run
Import-DockerDesktopKube on the laptop for that last mile.
Build fails with x509 / certificate errors (corporate proxy or TLS inspection). Two things, usually both:
- Drop your corporate root CA into
certs/as a*.crt(PEM). The Dockerfile trusts it before any download. Seecerts/README.md. - Set the proxy — in
.env(HTTP_PROXY/HTTPS_PROXY) and in Docker Desktop under Settings → Resources → Proxies. Thendocker compose build.
Import-Module fails with "running scripts is disabled on this system" (or "not digitally
signed"). PowerShell's execution policy is blocking the module. For your user only, no admin
needed:
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
Unblock-File 'C:\path\to\devtool\Devtools.psm1' # needed if the folder came from a downloaded ZIPIf a Group Policy enforces the policy, PowerShell will refuse the change. The module can't load
then; use docker compose run --rm dev instead, which gives you the same toolbox in a shell.
Garbled characters (â€", Γöé, broken box lines) in Terraform or gh output. Windows
PowerShell decodes native output with the legacy console code page, but the container prints
UTF-8. Add this line to your $PROFILE:
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8Quotes in an argument (JSON for kubectl patch -p, az --tags, terraform -var). Type them
naturally, e.g. kubectl patch deploy web -p '{"spec":{"replicas":3}}'. Windows PowerShell 5.1
normally strips the double quotes from native command arguments; the wrappers escape them for
you, so don't add backslashes. CI checks this in both Windows PowerShell 5.1 and PowerShell 7.
az login opens nothing / hangs. Use az login --use-device-code. The
container has no browser, so the normal interactive login can't work.
Output looks garbled, or piping/redirection breaks a command. The wrappers
only allocate a TTY (-t) for genuine interactive use and drop it when output is
piped or captured, which is correct. If you hit an edge case, run the command
inside dev instead.
A version pin 404s during build (e.g. a kubectl patch that doesn't exist).
Pick a valid version and set it in .env, then rebuild.
docker was not found on PATH from a wrapper. Docker Desktop isn't running,
or docker doesn't resolve in a plain shell. Start Docker Desktop and retry.
Bind mount path issues on Windows. docker compose handles Windows paths in
REPOS_ROOT. For the wrappers, the current directory is auto-detected; if a
drive-letter path ever misbehaves, cd into the folder and run from there (the
wrappers mount $PWD).
devtool/
├─ Dockerfile # the image; EXTRA TOOLS block at the bottom
├─ docker-compose.yml # build + persistent volume + dev service
├─ .env.example # versions, proxy, repos path (copy to .env)
├─ setup.ps1 # one command: check Docker, build, wire the profile, verify every tool
├─ uninstall.ps1 # the undo: stop the containers, remove the wrappers (image and logins only on request)
├─ Devtools.psm1 # PowerShell wrappers (az/kubectl/terraform/... + dev + Import-DockerDesktopKube)
├─ scripts/
│ ├─ devtools-entrypoint.sh # keeps Docker Desktop host traffic off the proxy
│ └─ import-docker-desktop-kube.sh # copies the docker-desktop kube context into the toolbox
├─ tests/ # smoke test, importer end-to-end test, PowerShell tests (wrappers, setup.ps1, uninstall.ps1)
├─ .github/workflows/ # CI: lint, build the image, run the tests above
├─ certs/ # drop corporate root CA here (optional)
└─ repos/ # default mount point if REPOS_ROOT is unset