Back to Install

Ship It

Everything you build goes to staging first. Only a named release reaches production, and only by promoting the image staging ran, or a production build of the same commit. That is an app with a staging target and production pinned to stable; an app with no staging target ships differently.

Overview

Two speeds

The whole system rests on one idea: building and releasing are different verbs. Confusing them is what made production scary.

Iterate · many times a day

Build → staging

  • Run it whenever you like, even with uncommitted changes.
  • Goes to staging only. It cannot reach production.
  • Every build gets a tag naming its commit: sha-8b966e9 from a clean tree, dev-20260825T0924-8b966e9 (with the build time) from a dirty one.
  • Neither is a version: no version number is claimed.

Release · when something is ready

Stage → release → promote

  • Needs a clean tree on main and the staged image built from it.
  • Bumps the version, writes notes, creates a git tag and a GitHub Release.
  • Names the image staging already ran, and deploys nothing. It builds no new app image, except when the app is not promote-clean (its build args differ between staging and production): then one extra <version>-production build comes first, from the same commit, before the bump.
  • Only a named release is promoted to production (promote --allow-unreleased is the escape hatch, and says so loudly).
✓

The rule that makes this safe

On an app with a staging target, production runs whatever the stable tag points at, and only promote and rollback ever move it. So an ordinary build, however careless, physically cannot reach production.

Bare hetge opens a menu whose Ship section lists these steps in order.

Part One

Every day: build and test on staging

1

Write code, run it locally

If you have just cloned the repo, or need fresh secrets:

$ hetge env:pull # secrets from 1Password
$ hetge dev:up # the local database, Redis and S3

The first fills your .env files from 1Password using the committed templates. It asks before overwriting a file you already have: a no skips that file, and Ctrl-C stops the run. The second starts the shared local stack and creates what your app needs in it. Never point a local app at staging's database: how the stack works is its own page, Run It Locally.

2

Put it on staging

$ hetge stage

Builds the image, pushes it, and deploys it to staging, whatever the app's default target is. It takes no --target.

After the line naming the hetge version (hetge v1.0.0 · stage), it prints where it is going: the target and its env file, the image reference, and the Dokploy URL, project, environment, compose and domains. stage, deploy, sync, release, promote, and build with --deploy or --sync all show that card before they act, so the destination is on screen before anything lands; release and promote then ask before going on. rollback instead shows the image it is going back to, and asks.

Run hetge status if you are ever unsure what each target is running.

Your tree can be dirty. The image is tagged so that everyone can see it was: that is the point of staging.

3

Try it

Open the staging site: account.hetge.dev, flux.hetge.dev, and so on. Log in, click around, upload something.

Need to run a tool against staging's database or storage?

$ hetge exec --target staging -- pnpm prisma migrate status

Everything after -- runs with staging's environment variables.

4

If you changed compose, env, targets or domains

$ hetge sync --target staging --deploy

Uploads the compose file, environment and domains to Dokploy, then deploys. No rebuild: it ships the image that is already there. It asks before removing any variable only Dokploy holds, naming each one. To see what it would change first, run hetge sync --target staging --dry-run: it writes nothing.

Name the target. Like build, deploy and exec, sync goes to the default target (deploy.default in hetge.jsonc) when you do not name one, and that is production unless you set it.

A plain hetge stage does not upload config. Once staging has been synced, it compares the env and compose file with what Dokploy holds, and when they differ it says so and offers to sync first (--yes accepts). It cannot see a change to targets or domains in hetge.jsonc, so after editing those run this, or hetge stage --sync to do both at once.

Repeat as often as you like. This loop has no ceremony and touches nothing that customers can see.

Part Two

Shipping to production

Stage a commit, test it, then two commands. Never edit anything in Dokploy by hand.

1

Commit, then stage that commit

Release names only a clean build of HEAD, so what you test has to be staged from a commit. If you changed an env file, push it first: env:push rewrites the committed template (.tpl), so it belongs in the commit.

$ hetge env:push # only if you changed an env file
$ hetge stage

Commit everything between the two. If Dokploy's staging env or compose file then differs from yours, stage offers to sync it first; release refuses until they agree.

2

Check what is running now

$ hetge status

Shows what staging, the release and production each run, and whether there is anything to promote. Safe to run any time: it changes nothing.

3

Test what is on staging

This is the step that matters, and the only one no tool can do for you. The image you are about to release is the one now running on staging, so whatever you check here is what customers get.

Release names whatever staging's tag (latest) points at, and every build moves latest. If you ran a plain hetge build after staging, stage again before you release, so the image you tested is the one you name.

4

Cut the release

Pick one:

$ hetge release patch # 1.0.1 to 1.0.2, fixes (the default)
$ hetge release minor # 1.0.1 to 1.1.0, new things
$ hetge release major # 1.0.1 to 2.0.0, breaking changes
$ hetge release 1.4.0 # or say it exactly

It shows you the version, the tag, the release notes and the image it is about to name, and waits for you to confirm. Nothing has happened yet at that point: pressing Ctrl-C, or answering no, is completely safe.

After you confirm it bumps the version, commits, tags, pushes, creates the GitHub Release, and tags the image staging is running as the release. It builds no new app image; an app that is not promote-clean gets one extra <version>-production build first, from the same commit, before the bump. With a staging target it deploys nothing; with none, it deploys production once (see No staging target).

If a step after the confirmation fails, nothing is undone for you: the release says what was written and prints what to run next, whether that undoes a bump that was never committed or finishes a release whose tag exists. Once the release commit and tag exist, running hetge release again is refused (there is nothing new to release), so the printed lines are how it gets finished.

5

Promote it

$ hetge promote

It shows the image it is about to ship and asks first. Then it copies the released image onto stable, keeping the old one as stable-previous (what rollback goes back to), and deploys production.

Promote never rebuilds. Production gets the image staging ran, not a fresh one that happens to come from the same code. The one exception is an app that is not promote-clean: there it ships the <version>-production build that release made from the same commit.

If it says stable moved but production was not deployed, finish with hetge deploy --target production.

!

Release will refuse you, on purpose

It says no if the tree has uncommitted changes or you are not on main (release.branch), if staging is not running an image built from HEAD, if that image came from a dirty tree or is already released, if there is nothing new since the last version tag, or if the staging env is out of step with its template, 1Password or Dokploy (the refusal says whether to push and sync, or to stage again). Each refusal names its reason, and most name the next step.

!

Promote will refuse you, on purpose

It says no if the app has no staging target or production does not pin stable, if nothing has been released yet, if stable already points at the released image (it then names hetge deploy --target production, for when production is not running it yet), if the build settings differ between staging and production and there is no production flavour from the same clean commit to ship instead, or if the production env is out of step with its template, 1Password or Dokploy (again, the refusal says whether to push and sync, or to stage again). Each refusal names its reason. Treat a refusal as information, not an obstacle: it is the check that keeps untested code out of production.

i

What a script sees

A refusal for env drift exits 2, and --allow-drift goes ahead anyway, printing a warning that names what it overrode. Any other refusal exits 1. Answering no at the confirmation exits 130, and so does a run with no terminal and no --yes. Nothing has been written in any of those cases.

Part Three

When production is broken

$ hetge rollback

Puts production back on the image it ran before the last promote, and redeploys. It shows the image it is going back to and asks first; in a script, add --yes (with no terminal and no --yes it stops with exit 130 and changes nothing). Do this first and investigate afterwards: it takes seconds and there is no prize for diagnosing a live outage.

Going further back: hetge rollback --tag 1.2.6.

Rollback swaps stable and stable-previous, so running it a second time puts production back on the image you just left. If it says stable moved but production was not deployed (or the deploy request failed), finish with hetge deploy --target production, not another rollback.

Rollback never touches the release: hetge status will keep showing production as behind it. Do not promote that release again: fix forward, then release and promote the fix.

i

Careful with databases

Rolling back the app does not roll back the database. Migrations run when a version deploys, and older code may not understand the newer schema. If the release included a migration, say so to whoever is helping before rolling back.

Part Four

No staging target

Some apps have only production. There is nothing to stage to or promote from, so hetge stage and hetge promote refuse, and so does hetge rollback: production cannot pin stable without a staging target, so it runs latest by default.

$ hetge build --deploy # try HEAD on production
$ hetge release patch # name it, then deploy production

hetge build --deploy builds what you have checked out and deploys it to production. Every build moves latest, the tag production runs, so even a plain hetge build is what production's next deploy ships.

hetge release then names the image production runs. When production runs latest but not HEAD's clean, unreleased build, release builds HEAD first, before the bump. Either way it deploys production once, so the app reports the new version. The summary says so, and there is no second question, also under --yes: never follow it with hetge deploy.

In the menu, the Ship section shows these two as Build → Push → Deploy and Release.

There is no rollback here. To go back, check out the last good commit and run hetge build --deploy.

Part Five

Setting up a new machine

$ op signin # sign in to 1Password
$ hetge login # everything else

Install hetge first (it needs Node.js 24 or newer): see the install page.

hetge login signs you into GitHub, logs Docker into the image registry, pulls the credentials the whole team shares into ~/.hetge/env, and sets up ~/.npmrc so pnpm install can fetch our private packages. In a project that names its own operator profile, it fills ~/.hetge/env.<profile> instead (hetge login --profile <name> works from anywhere).

If a command ever complains about a missing HETGE_DOKPLOY_API_KEY or similar, the answer is hetge login, not editing a file in the repo. Those credentials deliberately live outside every project, and when they are missing or out of date hetge offers to run login for you.

Reference

The shipping commands, briefly

Every flag of every command is in the Command Reference.

Command What it does Touches production?
hetge status Shows what each environment runs (--json for scripts) No, read only
hetge env:pull Fills your .env files from 1Password, asking before it overwrites one No
hetge env:push Saves your .env changes back to 1Password No, but affects teammates
hetge env:check Reports whether an env file, its template, 1Password and Dokploy agree; exits 2 on drift, 1 when a comparison failed No, read only
hetge build Builds and pushes an image, moving latest, the tag release names No with a staging target. Without one, latest is the tag production runs, so production's next deploy ships this build
hetge build --deploy Builds, pushes, deploys to the default target (deploy.default) Yes when that target is production. With no staging target, this is how a build reaches production. With one, production stays on stable but is redeployed
hetge stage Builds, pushes, deploys to staging; refuses with no staging target No
hetge sync --deploy Uploads config, then deploys, with no rebuild (--dry-run shows what it would change and writes nothing) Yes when the target is production: with --target production, or with no --target when deploy.default is production
hetge deploy --target production Redeploys what production's tag points at, with no rebuild Yes
hetge exec --target … -- Runs a local command with that target's env Depends what you run
hetge release … Version, tag, notes, GitHub Release; names the image staging runs (with no staging target: production's, building HEAD first when production does not run it) Not with a staging target, though it pushes to git. With no staging target, yes: it deploys production
hetge promote Sends the released image to production, keeping the old one as stable-previous Yes
hetge rollback Puts production back on the previous image (a swap: running it twice goes back) Yes
hetge login Sets up this machine No

Summary

Rules of thumb

Do

  • Stage as often as you want, dirty tree and all.
  • Run hetge status before anything that touches production.
  • Push env changes, then commit, then stage what you will release: release refuses otherwise.
  • Use what is on staging before you release: that image is the one production gets.
  • Roll back first, diagnose second.

Don't

  • Change images, environment variables or domains in the Dokploy UI. The next sync overwrites them, and nobody can see what you did.
  • Use --target production to skip a promote.
  • Run a plain hetge build between testing staging and releasing: it moves latest, which is what release names.
  • Add --yes unless a script is running unattended: it says yes for you, including to a sync that removes variables only Dokploy holds. It prints their names, and still refuses to leave the app with no variables, or to remove more than half of them, without --allow-env-removals.
  • Put a shared credential in a repo. They live in 1Password.

The staging environment exists so that production stops being an experiment. Getting the app running on your own machine is the other page: Run It Locally.