One Docker image with every senior-DevOps CLI baked in, plus PowerShell wrappers
(and bash wrappers for Git Bash, WSL, Linux and macOS) 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, from either shell. Short commands (k, kgp, tfp, azl ...),
history suggestions and Tab completion come with it, in PowerShell, in bash and in the toolbox shell.
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+), or bash 4.2 or newer (Git Bash, WSL, Linux, macOS with a newer bash). The two work side by side and share the image and the logins.
- 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, the shortcuts, Tab completion and history suggestions (see
Shortcuts, history suggestions and Tab completion),
and makes
~/.devtools-aliases.ps1for your own shortcuts (never overwritten) - runs
az,kubectl,terraform,helm,flux,kustomize,azd,gh,kubectxandkubensonce each, in the container, to prove they work, and checks that the image has the Tab completion helper
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).
Prefer bash (Git Bash, WSL, Linux, macOS)? Run ./setup.sh instead. The
Git Bash walkthrough goes through it one step at a time.
A few helper commands are named differently in the two shells, and a command typed in the wrong
one fails: see PowerShell or bash: which commands go where.
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 kustomizationsThese lines are typed the same way in bash.
Your 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 zsh with the same mounts (dev bash for bash)az 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.
PowerShell and bash run the same toolbox: the same image, the same volume, the same logins. The
tool names (az, kubectl, terraform ...) and the shortcuts (k, kgp, tfp, azl ...) are
typed the same way in both. A few helper commands are named differently, and each shell only knows
its own. The prompt tells you which one you are in:
| PowerShell (Windows PowerShell 5.1 or 7) | bash (Git Bash, WSL, Linux, macOS) | |
|---|---|---|
| The prompt looks like | PS C:\Users\you> |
you@PC MINGW64 ~ in Git Bash, you@host:~$ elsewhere |
| Set it up, once | powershell -NoProfile -ExecutionPolicy Bypass -File .\setup.ps1 |
./setup.sh |
Is az the toolbox's? |
Get-Command az (CommandType says Function) |
type az (says az is a function) |
| Image, volume, tools | Get-DevToolsInfo |
devtools-info |
| List the shortcuts | Get-DevToolsAlias or Get-DevToolsAlias 'kg*' |
devtools-alias or devtools-alias 'kg*' |
| Import Docker Desktop's Kubernetes | Import-DockerDesktopKube |
devtools-import-kube |
| A shell inside the toolbox | dev |
dev |
| Your own shortcuts | ~/.devtools-aliases.ps1 |
~/.devtools-aliases.sh |
| Switch the tools off in this window | Remove-Module Devtools |
devtools-unload |
| Remove it for good | .\uninstall.ps1 |
./uninstall.sh |
A command from the other column fails with an error that looks like a broken install, but nothing
is broken.
devtools-alias typed in PowerShell says not recognized as the name of a cmdlet, and
Get-DevToolsAlias typed in bash says command not found. type az in PowerShell says Cannot find
path 'C:\Users\you\az', because there type is short for Get-Content and it looks for a file
called az. If Get-DevToolsAlias is not recognized in PowerShell either, see
Troubleshooting.
The same toolbox for bash. If Git for Windows is installed, Git Bash is the natural place for it on
this laptop. az, kubectl, terraform, flux, helm, kustomize, azd, gh, kubectx and
kubens become shell functions that run the container, so they win over any program of the same
name that is installed (command az still reaches the installed one). The image and the volume with
your logins are the ones PowerShell uses: log in from either, it shows in both.
Git Bash comes with Git for Windows. If neither Git nor WSL is installed on the PC, use the PowerShell version: nothing in the toolbox needs bash.
- Unzip the project, for example to
C:\Users\<you>\devtool(right-click the zip, Extract All). - Start Docker Desktop and wait until it says "Engine running". It has to use Linux containers, which is the default.
- Open Git Bash in the project folder. In File Explorer open the folder, right-click an empty
spot and choose Open Git Bash here (on Windows 11 it is under Show more options). Without
that menu entry, start Git Bash from the Start menu and
cdthere. In bashC:\is written/c/, and the slashes lean the other way:cd /c/Users/<you>/devtool
~is your Windows user folder, so a folder in Downloads iscd ~/Downloads/devtool. Put quotes around a path that has a space in it:cd "/c/Users/<you>/my tools/devtool". - Run the setup.
If that says "Permission denied", run
./setup.sh
bash setup.sh. The first build takes a few minutes. If you already ransetup.ps1the image exists and this step is quick. You should see something like this (the versions and the times differ):Near the end it asks two questions, eachDevOps toolbox setup (/c/Users/<you>/devtool) == 1/6 Docker OK Docker engine 28.0.4, Linux containers == 2/6 Settings (.env) OK created .env from .env.example (edit it to set a proxy or pin tool versions) == 3/6 Build the image (the first build takes a few minutes) OK image devtools:latest built in 148s == 4/6 bash startup file OK added the devtools.sh line to /c/Users/<you>/.bashrc OK /c/Users/<you>/.bash_profile now loads ~/.bashrc, so login shells (Git Bash windows, ...) get the toolbox too == 5/6 Load the wrappers OK az, kubectl, terraform, flux, helm, kustomize, azd, gh, kubectx, kubens and dev now run in the container OK 38 shortcuts: k, kgp, tfp, azl, aksx ... (devtools-alias lists them all) OK made a file for your own shortcuts: /c/Users/<you>/.devtools-aliases.sh OK Tab completion for az, kubectl, terraform, helm, flux ... (the real tool answers from the container, ...) OK history: type the start of a command, then Up or Down finds it == 6/6 Verify every tool (one container per tool) OK az azure-cli 2.91.0 OK kubectl Client Version: v1.31.4 OK terraform Terraform v1.16.5 ... OK complete Tab completion helper is in the image All set.[y/N](Enter means no): sign in to Azure now, and import Docker Desktop's Kubernetes context now. Both can wait: step 6 shows the sign-in, anddevtools-import-kubedoes the import whenever you like. To answer them up front, run./setup.sh --login --import-kube; to never be asked,./setup.sh --no-prompt. - Open a new Git Bash window. A script cannot load anything into the window it was started
from. The new window loads the toolbox from
~/.bashrc(setup also made sure Git Bash reads that file). To use it in the current window instead, run the line setup printed at the end, for example. '/c/Users/<you>/devtool/devtools.sh'. - Check it.
type az # az is a function devtools-info # the image, the volume, the tools devtools-alias # the shortcuts az login --use-device-code # sign in (the shortcut is azl); the token stays in the volume
type azhas to sayaz is a function. If it saysaz is /c/...ornot found, the toolbox is not loaded in this window: open a new one, or run the line from step 5.
In Git Bash ~ is your Windows user folder (echo ~ shows it; some company set-ups point it
elsewhere), so ~/.bashrc is C:\Users\<you>\.bashrc and your own shortcuts file,
~/.devtools-aliases.sh, is next to it. Your logins live in the Docker volume devtools-home,
which PowerShell uses too, so you can sign in from either shell.
Same steps as setup.ps1, in the same order: checks that Docker runs Linux containers, makes .env,
builds the image, adds one line to ~/.bashrc (a .bak copy is saved when it has to change an
older line), loads the wrappers, makes ~/.devtools-aliases.sh for your own shortcuts (never
overwritten) and runs every tool once. Git Bash windows and macOS Terminal start login shells,
which read ~/.bash_profile and not ~/.bashrc, so if your login file does not load ~/.bashrc
setup adds the two lines that do. It is safe to run again.
The line it adds to ~/.bashrc is . '/path/to/devtool/devtools.sh'. A script cannot load anything
into the terminal it was started from, so open a new terminal afterwards, or run that line.
| Option | What it does |
|---|---|
--skip-build |
the image is already built |
--skip-profile |
leave your startup files alone |
--skip-verify |
skip the tool checks |
--login |
run az login --use-device-code at the end without asking |
--import-kube |
import Docker Desktop's Kubernetes context at the end without asking |
--no-prompt |
never ask a question (for automation) |
--rc-file FILE |
edit this file instead of ~/.bashrc |
Exit code: 0 all good, 1 setup could not finish, 2 setup finished but something above failed.
The tools and the shortcuts are typed the same way. The helper commands have bash names:
devtools-info, devtools-alias, devtools-import-kube [CONTEXT] [KUBECONFIG] and
devtools-unload. They are listed next to their PowerShell names in
PowerShell or bash. devtools-alias 'kg*' filters
the list and devtools-alias --count counts it. dev opens zsh inside the toolbox (dev bash for
bash). Your own shortcuts go in ~/.devtools-aliases.sh: aliases or functions, read last, so it
wins.
The shortcuts are the same list (scripts/shell/aliases.sh is the file both the toolbox shell and
your bash read), and they are aliases, so kgp -n kube-system runs kubectl get pods -n kube-system
and kex web -- sh keeps its --. Aliases only expand in an interactive shell; in a script call the
tool itself, or run shopt -s expand_aliases first. Set AKS_RG and AKS_NAME once (for example
export AKS_RG=my-rg AKS_NAME=my-cluster in ~/.devtools-aliases.sh) for aksc, aksup and aksx.
(The shell inside the toolbox reads a ~/.devtools-aliases.sh of its own, kept on the volume. It is
not the file in your home folder.)
Two things bash does differently. History: bash cannot draw a grey suggestion while you type, so
the Up and Down arrows search the history for what you have typed instead (type kgp, press Up). Only
the default arrow bindings are replaced; a binding of your own is left alone. The shell inside the
toolbox (dev) has the grey suggestions. Tab completion: az, kubectl, terraform, helm,
flux, kustomize, azd and gh, and the shortcuts for them, complete through the real tool in a
short-lived container, about a second per press (a few for az), exactly as in PowerShell. The first
Tab lists the choices when there are several.
On Git Bash, devtools.sh stops Git Bash from rewriting /work, /subscriptions/... or any
argument that starts with a slash into a Windows path, and mounts the current folder in the form
Docker for Windows wants (C:/Users/you/project). In the plain Git Bash window (mintty) a program
that needs a terminal, such as az login, kubectl exec -it or dev, only works through winpty,
which Git for Windows ships; devtools.sh uses it there automatically. winpty rewrites every
argument that looks like a path on its own account (-w /work became
-w C:/Users/you/AppData/Local/Programs/Git/work) and cannot be told not to, so docker's arguments
never go past it as they are. They go as one string of hex digits to scripts/devtools-pty-run.sh,
which winpty starts, and which turns them back into arguments and runs docker.exe. That file has
to stay in the scripts folder next to devtools.sh. The Windows Terminal and the VS Code terminal
do not need winpty. In WSL, Docker Desktop must have WSL integration switched on for your
distribution; Docker Desktop's kubeconfig is the Windows one, so use
devtools-import-kube docker-desktop /mnt/c/Users/NAME/.kube/config there.
| Variable | Effect |
|---|---|
DEVTOOLS_IMAGE, DEVTOOLS_VOLUME |
the image and the volume (defaults devtools:latest, devtools-home) |
DEVTOOLS_BASH_ALIASES |
the file for your own shortcuts (default ~/.devtools-aliases.sh) |
DEVTOOLS_NO_READLINE |
1 leaves the arrow keys and readline settings alone |
DEVTOOLS_WINPTY |
auto (default), 1 or 0: whether docker runs through winpty in Git Bash |
AKS_RG, AKS_NAME |
your cluster, for aksc, aksup, aksx; dev hands them to the shell inside |
Set them before the line in ~/.bashrc that loads the toolbox. devtools.sh needs bash 4.2 or
newer: Git Bash and every current Linux have it; the bash that macOS ships is 3.2, so install a
newer one (brew install bash). zsh is not supported by the host script; dev gives you zsh inside
the toolbox. To undo it, run ./uninstall.sh (see Stop and remove it).
All of this loads with the module, so setup.ps1 turns it on and uninstall.ps1 turns it off.
The parts that run inside the toolbox come with the image, which setup.ps1 rebuilds. (This
section is about PowerShell; bash has the same shortcuts and completion, see above.)
Get-DevToolsAlias lists them. Whatever you type after a shortcut is added at the end, so
kgp -n kube-system runs kubectl get pods -n kube-system.
| Azure | Runs |
|---|---|
azl |
az login --use-device-code |
azwho |
az account show -o table |
azsubs |
az account list -o table |
azsub NAME |
az account set --subscription NAME |
azrg |
az group list -o table |
azres RG |
az resource list -o table -g RG |
acrls REGISTRY |
az acr repository list -o table -n REGISTRY |
| AKS and kubectl | Runs |
|---|---|
aksl |
az aks list -o table |
k |
kubectl |
kx, kn |
kubectx, kubens |
kgp, kgpa |
kubectl get pods, kubectl get pods -A |
kgn, kgd, kgs |
kubectl get nodes, deployments, services |
kd |
kubectl describe |
kl, klf |
kubectl logs, kubectl logs -f |
kex POD -- sh |
kubectl exec -it POD -- sh |
kaf FILE |
kubectl apply -f FILE |
krr deploy/NAME |
kubectl rollout restart deploy/NAME |
ktop |
kubectl top pods |
| Terraform | Runs |
|---|---|
tf |
terraform |
tfi, tfv, tff |
terraform init, validate, fmt -recursive |
tfp |
terraform plan -out=tfplan |
tfa |
terraform apply tfplan (applies exactly the plan you just read) |
tfo |
terraform output |
tfs, tfss ADDRESS |
terraform state list, state show ADDRESS |
tfw, tfws NAME |
terraform workspace list, workspace select NAME |
tfdestroy |
terraform destroy (it still asks you to confirm) |
There is no -auto-approve shortcut, on purpose.
Three AKS helpers take the resource group and the cluster name, either typed (aksc my-rg my-cluster)
or from two variables that you set once. They stay on your laptop and are not in this repository:
$env:AKS_RG = 'my-resource-group'; $env:AKS_NAME = 'my-cluster'| Helper | Runs |
|---|---|
aksc |
az aks get-credentials -g RG -n NAME --overwrite-existing |
aksup |
az aks get-upgrades -g RG -n NAME -o table |
aksx kubectl get nodes |
az aks command invoke -g RG -n NAME --command "kubectl get nodes", which reaches a private cluster through Azure |
PowerShell removes a bare -- before a function sees it. The wrappers put it back where you typed
it, so kubectl exec -it web -- ls -la and kex web -- sh work as written.
setup.ps1 makes ~/.devtools-aliases.ps1 once and never overwrites it. It is read after the
built-in shortcuts, so what you define there wins:
function kgj { kubectl get pods -o json @args }
function tfpl { terraform plan -out=tfplan @args }
$env:AKS_RG = 'my-resource-group'
$env:AKS_NAME = 'my-cluster'A mistake in the file shows as a warning and the toolbox still loads. Get-DevToolsAlias marks the
shortcuts that are yours. Keep the file somewhere else by setting $env:DEVTOOLS_ALIASES before the
import. Inside the toolbox shell the same list exists as shell aliases, and your own go in
~/.devtools-aliases.sh (that is on the volume, so it stays).
As you type, PowerShell shows the rest of an earlier command in grey; the Right arrow accepts it. That needs PSReadLine 2.1 or newer, which PowerShell 7 has. Windows PowerShell 5.1 ships 2.0, which cannot show grey text, so there the Up and Down arrows search your history for what you have typed instead. To get the grey text on 5.1 as well, update PSReadLine once (no admin rights; it needs the PowerShell Gallery, which some companies block), then open a new window:
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Install-Module PSReadLine -Scope CurrentUser -Force -SkipPublisherCheckTab opens a menu of the choices; arrows pick, Enter accepts. Only defaults are replaced: a Tab
binding or a suggestion source you set yourself is left alone, and everything is handed back when
the module is removed. $env:DEVTOOLS_NO_READLINE = '1' before the import skips all of it.
az, kubectl, terraform, helm, flux (and kustomize, azd, gh) complete their
commands and flags, and so do the shortcuts for them: kgp -n <Tab>, tf wor<Tab>. In PowerShell
the real tool answers from a short-lived container, so a Tab press takes about a second (a few for
az). Names that live in a system, such as pods or resource groups, are offered when the tool can
reach it and you are logged in. If Docker is not running, the image is older than this feature or
the tool takes too long, nothing is offered and file names complete as usual.
dev (and docker compose run --rm dev) opens zsh: grey suggestions from your history, colours
(green when the command exists, red when it does not), a Tab menu, and the shortcuts above.
dev bash opens bash with the same shortcuts and completion. The history is kept in the volume
(~/.zsh_history), so it survives between sessions.
| Variable | Effect |
|---|---|
DEVTOOLS_ALIASES |
the file for your own shortcuts (default ~/.devtools-aliases.ps1) |
DEVTOOLS_NO_READLINE |
1 leaves PSReadLine alone |
AKS_RG, AKS_NAME |
your cluster, for aksc, aksup, aksx; dev hands them to the shell inside |
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 zshSet 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, and neither
is your own shortcuts file (~/.devtools-aliases.ps1): it holds what you wrote, so the script
only tells you where it is. With the profile line gone, new windows have no shortcuts, Tab
completion or history settings from the toolbox. 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.
From bash it is ./uninstall.sh, with the same steps and the same exit codes. It takes the
devtools.sh line (and the login-shell line setup.sh added) out of ~/.bashrc, ~/.bash_profile,
~/.bash_login and ~/.profile, saving a .bak copy of each file it changes, and leaves the rest of
those files byte for byte as they were. Its options are --remove-image, --remove-volume,
--force, --skip-profile, --no-prompt and --rc-file FILE. A terminal that is open keeps the
functions until you close it or run devtools-unload in it. Your ~/.devtools-aliases.sh is kept.
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 wrapper tests (Linux): hadolint on the Dockerfile, shellcheck on every shell
script,
docker compose config, and the test scripts for the PowerShell and the bash wrappers, which 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), every shortcut against its table, the bare--that PowerShell removes, your own shortcuts file (including a broken one), Tab completion against canned answers, and what is done to PSReadLine (new and old versions) and handed back.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.tests/pty_shell_test.py pwshstarts an interactive PowerShell 7 in a pseudo terminal with the profile linesetup.ps1writes, then types like a person: the Tab menu, Tab completion through a fake docker, grey suggestions from the history,kex pod -- ls -la, and the keys coming back when the module is removed.tests/test-devtools-sh.shruns the realdevtools.shin a child bash and checks the exactdocker runline each function produces (volume, current folder, image,devand the AKS variables, arguments with quotes, spaces and slashes, exit codes), the shortcuts, your own shortcuts file (including a broken one), Tab completion against canned answers, and thatdevtools-unloadleaves nothing behind.tests/test-setup-sh.shandtests/test-uninstall-sh.shdo forsetup.shanduninstall.shwhat the PowerShell scripts above do for theirs, including that~/.bashrccomes back byte for byte after setup and uninstall.tests/pty_shell_test.py hostbashstarts an interactive bash in a pseudo terminal with the linesetup.shwrites and types like a person: Tab completion through a fake docker, the Up arrow finding earlier commands,-treaching docker at a terminal, and the keys coming back afterdevtools-unload. A third session plays the plain Git Bash window: a stand-inwinptyrewrites path arguments the way Git for Windows' does, and docker must still get-w /workand every other argument as typed.- The three bash test scripts run once more inside the official
bash:4.2andbash:4.3images, the oldest bash the scripts promise to work with.
- On Windows: all three PowerShell 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). The three bash test scripts run there too, in Git Bash against the fakedocker.exe, which is what proves that Git Bash does not rewrite/workor/subscriptions/...and that the folder is mounted in its Windows form. The runner also opens a hidden Git Bash window (mintty, which giveswinptythe terminal it needs), and in it the tests callkubectx,kubectl execand a few awkward argument lists through Git's realwinpty:docker.exemust get-w /workand every other argument as typed, and the exit code must come back. They also print whatwinptydid to a few arguments. - 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, and the interactive shells are in place (zsh and its plugins, the completion scripts, the aliases, and the answersdevtools-completegives for each tool).tests/pty_shell_test.pytypes into zsh and into bash inside the image, in a pseudo terminal: the aliases, the grey suggestion, the colours and Tab completion for flux, gh, terraform, kubectl, helm and az.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. So doessetup.sh, followed by a new bash that loads the line it wrote: the tools are the container's, standard input and the current folder reach the container, and Tab completion gets real answers from kubectl, terraform, helm and az.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.uninstall.shdoes the first part (container and startup file) the same way.
Run the bash tests on your own machine (no Docker needed, and your own ~/.bashrc is not touched;
Linux, macOS or Git Bash):
bash tests/test-devtools-sh.sh
bash tests/test-setup-sh.sh
bash tests/test-uninstall-sh.sh
python3 tests/pty_shell_test.py hostbash -- bash # Linux or macOSRun 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 python3
python3 tests/pty_shell_test.py zsh --full -- docker run --rm -it devtools:ci zsh
python3 tests/pty_shell_test.py bash --full -- docker run --rm -it devtools:ci bash
python3 tests/pty_shell_test.py pwsh -- pwsh -NoLogo # PowerShell 7 on Linux or macOSWhat CI cannot prove: how your corporate proxy and TLS inspection behave, Docker Desktop's
real Kubernetes (the end-to-end test uses a fake API server on Linux), and how Windows PowerShell
5.1 behaves at a live prompt (CI has no console there, so the grey suggestions, the Tab menu and
the Up and Down search are tested with PowerShell 7 and with stand-ins for PSReadLine 2.0). The same
goes for bash: it is tested on Linux, in Git Bash (also in a real mintty window with Git's real
winpty, but one that no person types into) and on bash 4.2 to 5.3, always against a fake docker.
A WSL distribution, a Mac and a live Git Bash window with Docker Desktop behind it follow the notes
in the bash section. Run Import-DockerDesktopKube (or devtools-import-kube) on the laptop for the
Kubernetes 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.
Build fails with "429 Too Many Requests", or Docker Hub is blocked. The image starts from
debian:bookworm-slim on Docker Hub. Put a mirror of the same image in .env and rebuild
(.\setup.ps1):
BASE_IMAGE=registry.corp.example/library/debian:bookworm-slim
It has to be Debian 12; the install steps use apt.
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.
Tab does nothing, or is slow, in PowerShell. Each Tab asks a short-lived container, so it takes
about a second, and the first one after Docker Desktop starts can take longer. Nothing at all means
the answer was empty or late: check that Docker Desktop is running and that the image is current
(.\setup.ps1 rebuilds it; Get-DevToolsInfo shows which image the wrappers use). Names that need
a login or a cluster (pods, resource groups) only complete when the tool can reach them.
No grey suggestions in Windows PowerShell 5.1. It ships PSReadLine 2.0, which cannot draw them.
The Up and Down arrows search the history instead; to get the grey text, update PSReadLine as shown
under History suggestions. The shell inside the toolbox (dev) has the grey
suggestions either way.
A warning about your shortcuts file when PowerShell starts. The module could not read
~/.devtools-aliases.ps1 (or the file named by DEVTOOLS_ALIASES) and went on without the rest of
it. The warning names the file and the error; fix that line, or move the file away.
az login opens nothing / hangs. Use az login --use-device-code. The
container has no browser, so the normal interactive login can't work.
In PowerShell, devtools-alias is "not recognized" and type az says "Cannot find path".
Those are bash commands. In PowerShell write Get-DevToolsAlias and Get-Command az (there type
is short for Get-Content, which looks for a file called az). Every pair is in
PowerShell or bash. If Get-DevToolsAlias is not
recognized either, your profile loads an older copy of the module, from before the shortcuts were
added. Run setup.ps1 again from the newest project folder and open a new PowerShell window.
In Git Bash, Get-DevToolsAlias or Get-Command says "command not found". The other way round:
those are PowerShell commands. Use devtools-alias and type az.
A new Git Bash (or macOS Terminal) window has no tools, but . ~/.bashrc fixes it. That window
starts a login shell, which reads ~/.bash_profile and not ~/.bashrc. setup.sh adds a line to
your login file that loads ~/.bashrc; if you have since replaced that file, put this in it:
[ -r ~/.bashrc ] && . ~/.bashrc.
./setup.sh says "Permission denied". Run it as bash setup.sh.
cd C:\Users\you\devtool says "No such file or directory" in Git Bash. Bash reads a backslash as
an escape and drops it. Write cd /c/Users/you/devtool, or put the Windows form in quotes with
forward slashes: cd "C:/Users/you/devtool".
In Git Bash, az login (or dev) says "the input device is not a TTY". The plain Git Bash
window (mintty) cannot hand its terminal to docker.exe. devtools.sh goes through winpty there
by itself; if that was switched off (DEVTOOLS_WINPTY=0) or winpty is missing, use the Windows
Terminal or the VS Code terminal, or set DEVTOOLS_WINPTY=1 and install winpty.
In Git Bash, a tool says the working directory 'C:/.../Git/work' is invalid, it needs to be an absolute path. winpty (see the bash section) rewrote /work into a Windows path before docker
saw it. Copies of devtools.sh from before this was fixed handed winpty docker's arguments as
they were. Update the whole project folder (extract the newest zip over it, so that
scripts/devtools-pty-run.sh is there too), open a new Git Bash window and try again.
devtools-info prints a Terminal line: winpty (...) in the plain Git Bash window and direct
anywhere else. If it still fails, open an issue with the output of devtools-info and of the failing
command, after taking your user name and any cluster names out of it.
In WSL, docker was not found on PATH. Docker Desktop > Settings > Resources > WSL integration:
switch on your distribution. A docker.exe from Windows is not used on purpose.
devtools.sh needs bash 4.2 or newer. macOS ships bash 3.2. Install a newer one
(brew install bash) and run setup.sh with it, or use dev for zsh inside the toolbox.
Tab does nothing, or is slow, in bash. Same as in PowerShell: each press asks a short-lived
container (about a second, longer for az). Nothing at all means the answer was empty or late, or
that the image is older than this feature (./setup.sh rebuilds it). Another completion for the
same command that you registered after the toolbox loaded replaces ours; load the toolbox last.
$'\r': command not found, or a ^M in a "bad interpreter" message. A script, or your shortcuts
file, was saved with Windows line endings (CR LF). Convert it with sed -i 's/\r$//' FILE, or in
VS Code click CRLF in the status bar and choose LF. The files in this repository are kept at LF,
also when git on Windows checks them out.
A shortcut such as kgp says "command not found" in a script. Aliases expand only in an
interactive shell. In a script write the command out, or put shopt -s expand_aliases before it.
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),
│ # shortcuts, Tab completion, history suggestions
├─ setup.sh # the same as setup.ps1, for bash (Git Bash, WSL, Linux, macOS)
├─ uninstall.sh # the same as uninstall.ps1, for bash
├─ devtools.sh # bash wrappers (the twin of Devtools.psm1): functions, shortcuts, Tab completion,
│ # history search; sourced from ~/.bashrc
├─ 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
│ ├─ devtools-complete.sh # answers the Tab presses of PowerShell and host bash with the real tool's completions
│ └─ shell/ # the toolbox shell: zshrc, bashrc, aliases.sh, az-completion.sh
├─ tests/ # smoke test, importer end-to-end test, terminal (pty) tests, PowerShell and bash tests
├─ .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