BLOG/DEVELOPER DAILY
THEME
DEVELOPER DAILY

OrchShell: the ad-hoc command that already knows your environment

You keep a folder of wrapper scripts just to run helm and kubectl with the right per-environment values. orchstep do renders any command with your structured, layered config — objects and all — and runs it. dotenv++ for applications, no orchstep.yml required.

Jul 3, 2026 OrchStep Team 7 minROLE: Site Reliability EngineerSCALE: Any

Every team has the folder. scripts/deploy-prod-eu.sh, scripts/deploy-prod-us.sh, scripts/scale-staging.sh — a dozen near-identical wrappers whose only real job is to remember the right values for one environment and shove them into one helm or kubectl command. They drift. Someone updates the EU host and forgets US. The "config" is smeared across bash.

The root cause is that dotenv only hands your process flat strings. The moment your app config has structure — resources: { cpu, memory }, a list of hosts, nested ingress settings — you flatten it into RESOURCES_CPU=…, RESOURCES_MEM=… and reassemble it by hand downstream. The wrappers are that reassembly.

dotenv++ for applications

orchstep do runs any shell command or script with your OrchStep context already filled in: {{ vars.X }} and {{ env.X }} resolved from your defaults, your environment groups/environments (--env), and your dotenv — then it executes the result and passes the real exit code through. Pointed at a folder of environment files, that's OrchShell: dotenv++ for applications.

Classic dotenv hands flat KEY=value strings; OrchShell hands a command structured, per-environment, layered config — objects and lists — plus the flat env for free

The difference is the right side of that picture. Your environment config stays structured and layered (defaults → group → environment), and your command references it directly:

orchstep do --env prod-eu 'helm upgrade checkout ./chart \
  --set replicas={{ vars.replicas }} \
  --set resources.cpu={{ vars.resources.cpu }} \
  --set ingress.host={{ vars.ingress.host }}'

Switch the whole thing to another region by changing one flag — --env prod-us — and the same command resolves that environment's values. The dozen wrapper scripts collapse into one command plus a folder of environments/*.yml.

No orchstep.yml? Still works

Point orchstep do at a directory with no workflow file and it discovers the ambient context: a conventional environments/ directory (rich per-environment vars, including whole objects) and the dotenv your project config declares. So a repo that just holds config can still drive context-aware commands:

orchstep do --env prod-eu 'echo deploying to {{ vars.ingress.host }}'
# deploying to eu.checkout.example.com

(A bare .env is not auto-loaded — dotenv must be declared, so a directory you didn't write can't slip env vars into the commands you run.)

Four ways to hand it a command

The same engine, four shapes — pick by the moment you're in:

  1. Inline — the one-off: orchstep do 'kubectl set image deploy/app app={{ vars.image }}'.

  2. Stdin / pipe — render and run a whole script: cat deploy.sh | orchstep do --env prod.

  3. Script file with a shebang — make a runbook context-aware:

    #!/usr/bin/env -S orchstep do --script
    echo "Deploying v{{ vars.version }} to {{ vars.region }}"
    kubectl set image deploy/app app={{ vars.image }}

    chmod +x deploy && ./deploy — teammates run it and it pulls the right values from the env config automatically.

  4. Interactive REPLorchstep do --env prod drops you into a stateful shell where {{ vars.db_host }}, {{ vars.namespace }} and friends are already correct, and cd/export/multi-line constructs persist across lines like a real shell.

Look before you leap: --render

For anything destructive, render first — a context-aware envsubst that shows exactly what will run without running it:

orchstep do --render --env prod 'flyctl deploy --image {{ vars.image }}'
# flyctl deploy --image registry/app:1.2.3

This is the habit that replaces "comment out the command, echo it, eyeball it, uncomment." You see the fully-resolved command, then you run it.

The SRE case: prod inspection without the wrong context

The cost of the wrapper-script folder isn't just maintenance — it's the 2am incident where someone runs a command against prod with staging's namespace because they grabbed the wrong script. orchstep do --env prod gives you a shell where the namespace, hosts, and image tags are already the prod ones, structured and layered from the same files your deploys use. One source of truth for "what does prod look like," usable interactively, in a script, in a shebang, or in CI:

orchstep do -f ci.orchstep.yml 'docker push {{ vars.registry }}/app:{{ vars.version }}'

Secrets, by the way, resolve into the environment (referenced as $X), not rendered into the command text — same model as a normal step, so --render won't print them.

Takeaways

  • The wrapper-script folder exists because dotenv is flat. Keep your config structured and layered, and the wrappers disappear.
  • orchstep do renders any command/script with your real per-environment context and runs it — bare output, real exit code.
  • It works with no orchstep.yml at all, discovering ambient environments/ + a declared dotenv.
  • --render first for anything you can't take back.

Full reference: Ad-hoc Commands and OrchShell.

#ORCHSTEP-DO#ORCHSHELL#DOTENV#ENVIRONMENTS#DEVELOPER-EXPERIENCE
Try it in two minutes — one binary, no signup.
curl -fsSL https://orchstep.dev/install.sh | sh

RELATED — DEVELOPER DAILY