Back to Install

Command Reference

Interactive Mode

hetge

Launches interactive mode with a menu of all available actions. Best for day-to-day use: guides you through each step with prompts and confirmations.

Configuration

Every command reads project settings from hetge.jsonc in your project root. The $schema line gives editors autocomplete and inline validation. It is the only config location: a package.json / pyproject.toml config from before 1.0 is moved once with npx @hetge/cli@0.22 config:migrate, which also folds in .env.deploy.* files and then deletes them (run it on a clean git tree; it reads each file, never its .tpl, so a refusal prints the op inject line that creates a file from its template first), and a hetge.json is renamed to hetge.jsonc.

{
    "$schema": "https://cli.hetge.com/hetge.schema.json",
    "onepassword": {
        "items": [
            { "file": ".env", "item": "my-project" }
        ]
    }
}

Environment Commands

hetge env:pull [file] --force

Downloads .env files from 1Password using your .tpl template files. Templates define which secrets to fetch. Name one configured file to pull just that one (hetge env:pull .env.staging). Use --force to overwrite existing local files without prompting. A no at the overwrite question (or no terminal to ask) writes nothing for that file, and the other configured files are still pulled; Ctrl-C or Escape stops the whole run there. Either way it exits 130.

hetge env:push [file] --force

Uploads local .env files back to 1Password. Syncs any changes you've made locally to the vault. Name one configured file to push just that one (hetge env:push .env.staging). Use --force to overwrite vault items without prompting. A no at the update question (or no terminal to ask) writes nothing for that file, and the other configured files are still pushed; Ctrl-C or Escape stops the whole run there. Either way it exits 130. An empty variable (KEY=) is kept: 1Password holds no empty field, so the template carries it as KEY= and env:pull writes it back empty. Empty and missing are not the same where one env file overrides another.

hetge env:check --target <name> --json

Reports whether an env file, 1Password and Dokploy still agree. Three comparisons per target: the file against its .tpl, what a sync would upload against what Dokploy holds, and the template resolved through op against the local file. Every deployable target, or --target just one. It writes nothing and prints variable names only, never a value, and exits 2 when anything is out of step, so it works in a script. It exits 1 when a comparison it asked for failed, even beside drift: op inject erroring or timing out, Dokploy not answering, an id the server no longer has, a missing docker-compose.yml, and a synced target with no operator credentials, so a CI job missing its Dokploy secrets fails rather than passing unchecked. An edge that does not apply (no .tpl, a local-only target, never synced, the dokploy feature off) is named as not checked and is not an error. --json prints the same reports as one JSON document on stdout: clean is true when no edge failed and none drifted, complete is false when one failed, compared counts the edges actually compared, and an edge that compared nothing is {"unchecked": reason, "failed": true|false}. With nothing to compare at all, clean is true and compared is 0. This is the one command that always asks 1Password, so op not being signed in, or op whoami timing out, is an error here rather than a skipped line. In a terminal it runs op signin first, without asking; unattended it cannot, since op signin asks for a password or a fingerprint, so sign in before a script runs it. An op whoami that does not answer in time is not a missing session (1Password is usually waiting on an unlock): it says 1Password did not answer, exits 1, and runs no op signin.

hetge exec --target <name> -- <command...>

Runs a local command with a deploy target's environment variables set, for example hetge exec --target staging -- pnpm prisma migrate deploy. Everything after -- is passed through verbatim, the working directory is the project root, and the command's exit code is propagated. Without the -- nothing runs: it prints the line with the -- in front of the program, whose words (flags such as -v or --target included) stay the program's. Schema-rewriting commands (migrate dev, migrate reset, db push) are refused against a target that describes a remote environment.

Local Development

hetge dev:up

Starts one shared Postgres 18 / Redis 8 / RustFS stack for every app on the machine, then provisions this project from its dev env file (dev.env, default .env.development.local): the POSTGRES_* trio becomes a role and a database, every S3_*BUCKET* variable a bucket. A file pointing anywhere but the local stack (a tunnel port, a production S3 URL) is refused, naming the variable. Works outside a project too, where it only starts the stack.

hetge dev:status

Whether the stack is running, and whether this project's database and buckets exist.

hetge dev:pull --yes

Replaces this project's local database with a copy of staging's: pg_dump streams from the staging container over the ssh alias (dev.pull) straight into the local database. No tunnel, no local Postgres tools, no password over the wire. Asks first; --yes answers.

hetge dev:reset --yes

Drops and recreates this project's local database and buckets. Other apps' data is untouched.

hetge dev:down --volumes --yes

Stops the stack and keeps the data. --volumes also deletes the stack's volumes, which is every app's local data, so it asks first.

Build & Deploy Commands

hetge build --target <name> --deploy --sync --no-cache --yes --allow-env-removals

Builds a Docker image and pushes it to GitHub Container Registry (GHCR). Pushes latest plus, when git information is available, an immutable sha-… (or dev-… for uncommitted changes) tag. No build ever writes a version tag: that is hetge release's job, onto an image that already exists. Add --deploy to automatically trigger deployment after a successful build. Requires a running Docker daemon (with buildx) and a GitHub login with write:packages; both are checked automatically, and the CLI will prompt for gh auth login if needed. --target picks a deploy target declared in hetge.jsonc; without it the configured default applies. With --deploy and no --sync, a build whose target is out of step with Dokploy is offered the fix first (Dokploy (production) is out of step. Sync it now, before the build?); yes syncs and carries on, no just prints the command. --sync uploads the compose, env and domain config before deploying, and --no-cache rebuilds without the Docker layer cache.

hetge stage --sync --no-cache --yes --allow-env-removals

Builds, pushes and deploys to the staging target, whatever deploy.default says. This is the first rung of the ship workflow: hetge release names the image staging runs, so production ends up on the exact bytes that were tested. A dirty tree is allowed, because that is what staging is for. --sync uploads the compose, env and domain config before deploying; --no-cache rebuilds without the Docker layer cache. An app with no staging target is refused before anything else runs, naming hetge build --deploy as its equivalent. When staging's Dokploy config is out of step, it offers to sync before building (Dokploy (staging) is out of step. Sync it now, before the build?) rather than only printing the command; --yes answers yes, so a scripted stage syncs first. With --sync the offer is not made, since that run syncs anyway.

hetge deploy --target <name> --yes --allow-env-removals

Triggers a deployment on Dokploy using the existing image on GHCR. No rebuild occurs: this just deploys what's already been pushed. --target chooses which deploy target is deployed. When the target's Dokploy config is out of step, it offers to sync first (Dokploy (staging) is out of step. Sync it now, before the deploy?); declining deploys against the config Dokploy already has, as before.

hetge sync --target <name> --deploy --dry-run --yes --allow-env-removals

Creates or updates your project configuration on Dokploy. Sets up the project, environment, compose file, and domain. Run this first when setting up a new project. Add --deploy to trigger deployment after syncing. --target uploads that target's env file, appends the managed IMAGE_TAG / CONTAINER_NAME block, and creates or reuses the target's own Dokploy environment. On a Dokploy application (dokploy.application), it writes the name, description, env and domains, keeps the build args and secrets as they are, and never creates the application. When Dokploy holds variables the upload does not, it stops and names them (Dokploy has 2 variables local does not: W, Z. Uploading removes them. Continue?) before anything is sent. The default is no; --yes answers yes, and a run with no terminal and no --yes cancels (exit 130). That is usually somebody's edit in the Dokploy UI, which the upload would otherwise delete without a word. The names are printed even under --yes. Two uploads are refused outright, because they look like a wrong or empty file rather than an edit: one that would remove variables and leave the app none, and one removing more than half of the app variables Dokploy holds (from two removals up; on a compose, hetge's own IMAGE_TAG, CONTAINER_NAME and HETGE_VERSION count on neither side, though a stale one is still named; an application has no such names, and all of its variables count). An empty upload that removes nothing goes ahead. --allow-env-removals is the deliberate way through, on sync, deploy, stage and build. --dry-run prints what a sync would change (the env names it would add, change or remove, the compose file, the name and description, the domains, what it would create and the id it would record) and writes nothing, not even the recorded id. It makes no offers on the way, and exits 1 when the sync itself would refuse or stop on an error. A Dokploy that cannot be reached is offered a retry at a terminal only; --yes never answers it.

hetge release [patch|minor|major|x.y.z] --no-github-release --allow-drift --yes

With a staging target it names the image staging already runs, and builds no main image and deploys nothing. On an app with no staging target it releases HEAD, and deploys production when hetge deploys the app: it reuses the image production runs when that is HEAD's build, and otherwise, on production's latest, builds HEAD first. Bumps the version in package.json (or pyproject.toml), commits it as v1.2.7, writes release notes from the commits since the last tag, tags that commit, pushes, creates a GitHub Release, and only then points the 1.2.7 and released tags at the staged digest, so released never names a digest whose release does not exist. Refuses, reporting every problem at once before writing anything, on a dirty tree, off the release branch, when gh is not authenticated (only for a run that creates a GitHub Release), and on the staged image itself: not in the registry (run hetge stage first), not built from HEAD, built from a dirty tree, already released, or nothing to release because HEAD is the last version tag. A failure from the push on is never rolled back: it prints what is still owed (the push, gh release create, the registry tags, and the production deploy on an app with no staging target), since a second release refuses once the tag exists. An app that is not promote-clean also gets a <version>-production flavour, built from HEAD before the bump. --no-github-release skips the GitHub Release for that run. Before any of that it checks the environment behind the image being named, against its .tpl, against Dokploy and against 1Password, and refuses when they disagree: Refusing to release: the staging env is not in step (.env.staging). The drifted variables are grouped by what fixes them, under Stage again (baked into the image as build args, so only a rebuild helps; Build again and hetge build --deploy on an app with no staging target) or Push and sync, each with its commands. --allow-drift goes ahead anyway and prints one line naming what it overrode; it is command mode only, since the interactive menu cannot pass a flag, so a release refused from the menu returns to the menu. When 1Password has no session, a terminal run offers to create one rather than skipping that edge; unattended, and under --yes, the offer is not made and the vault edge is skipped as before.

hetge promote --allow-unreleased --allow-drift --yes

Ships the released image to production: nothing is rebuilt, so production receives the exact digest staging ran. It refuses, listing every failed check at once, when no staging target is declared, when nothing has been released (run hetge release first, or --allow-unreleased to ship what staging runs), when the app is not promote-clean and no released-production flavour exists, when that flavour was built from a different commit or a dirty tree, when stable already points at that digest, or when the production target does not pin stable. A registry that does not answer is said to be unreadable, never taken for nothing released or no stable yet. On agreement it points stable-previous at whatever stable was, moves stable to the promoted digest, and deploys production. If that deploy fails, stable stays moved and the error names hetge deploy --target production to finish. --allow-unreleased means ship what the staging target runs, bypassing released, and says so loudly. Production's environment is checked first, the same three ways hetge release checks staging's, and a refusal there is reported on its own before the promotion checks are even reached. Like release, a terminal run with no 1Password session offers to create one rather than skipping the vault edge; unattended, and under --yes, that edge is skipped as before. --allow-drift lifts that one refusal and nothing else, and only from the command line: the interactive menu cannot pass it, so a promote refused there returns to the menu.

hetge rollback --tag <tag> --yes

Swaps stable and stable-previous, then deploys production, so a bad promote is one command away from undone and the rollback is itself reversible. It never touches released or a version tag. --tag names a different image to become the new stable, keeping the current one as previous. Refuses a rollback that would not change what runs, naming hetge deploy --target production for when stable is right but production is not. If the deploy fails after the tags moved, it says so and names that same deploy: do not run rollback again then, since it would swap the tags back.

hetge status --json

One row per deployable target: version, commit, branch, clean or dirty, released or not, build time, and (promoted) for the target running stable when its image could be read. Between staging and production sits a released row saying nothing released yet: hetge release, production behind: hetge promote or production runs it (on a production-only app, production runs unreleased work: hetge release, since only a later build --deploy moves production off what its release deployed). Then the local HEAD, marked (release commit of v1.2.7) when HEAD is a release's bump commit, and a not promote-clean line when the staging image's build args differ between the targets' env files. The version comes from the tags at the image's digest, the bare semver tag hetge release wrote, never from a label, and a 1.2.7-production flavour reads as the release it was built for, so production on it runs the release; when gh cannot list the package's tags it reads - and one line says why. Last comes an env line about the files rather than the images: per target, whether the env file is in step with its template and with what Dokploy holds, with the command that puts it right. 1Password is not consulted here, because status only reads; hetge env:check is where that question is asked. --json prints the same facts as one JSON document on stdout: each image with its branch and whether it is the checked-out commit (atHead), the released row's verdict, releasedAhead on production when there is something to promote, the release commit, the build args that are not promote-clean, and the env reports. A registry that does not answer is not an image that was never pushed: the row says could not read (readError in the JSON) and the command exits 1 after printing the rest.

hetge fleet:status --json

status for every project below the current directory: every hetge.jsonc one or two levels down, one row per deployable target, with version, image state, released, synced (checked against the server when it can be reached), env drift against Dokploy, the last deploy's outcome, and whether the image is the checked-out HEAD. Run it from the directory that holds the checkouts, not inside one. --json prints the rows, with the reasons behind each cell. An image the registry could not be read for is unreadable, never missing, and the command exits 1 after printing the rest. A Dokploy application reads by dokploy: Dokploy builds it from git, so there is no image to judge. A checkout that does not load, one still configured in a pre-1.0 location included, is named under the table rather than left out.

hetge server:status --profile <name> --json

Every project, environment and service on the Dokploy server, with its status, last deploy and domains. It reads the server rather than a project, so it works with no checkout; inside one it uses that project's operator profile, and says so when that project's config does not load and the shared credentials are read instead. --profile reads another organization's credentials (~/.hetge/env.<name>). --json prints { host, profile, rows }.

Setup & CLI

hetge init

Writes a hetge.jsonc through a short wizard: the vault and env files, which features are on, and the Dokploy project and service. The file goes beside the nearest package.json or pyproject.toml at or above the current directory, or at the repository root when there is none. Discovery stops at the git repository root, so a config above the repository is never adopted. It refuses wherever a config already exists, including one that does not load (a pre-1.0 location or an invalid hetge.jsonc), and prints the reason instead of writing over it.

hetge login --profile <name>

Refreshes GitHub authentication (with write:packages) and, when Docker is installed and running, forces a fresh docker login ghcr.io using the current gh token, whatever the project's features (without Docker it says so and skips that step). It works in any directory. Use this if your token expired or you switched GitHub accounts mid-session. It also pulls the shared operator credentials (the Dokploy URL and API key) into ~/.hetge/env, and the GitHub Packages token into ~/.npmrc, both readable by you alone. Inside a project with an operator profile it fills that profile's ~/.hetge/env.<profile> instead; --profile fills ~/.hetge/env.<name> from anywhere, which is the file server:status --profile <name> reads.

hetge update

Updates hetge to the latest version, with the package manager that installed it. When it cannot ask GitHub which version is the latest (gh missing or logged out, or no answer within a minute), it says so, names the command to run by hand, and exits 1, rather than calling the installed version the latest.

hetge version

Prints the version and its release date.

hetge help [command]

Lists every command with its flags and what each does. The same list drives argument checking, the interactive menu and shell completion, so it cannot fall behind. hetge help build (or hetge build --help) shows that one command: its usage, the values it takes and every flag with its description. A name that is not a command is refused, with the closest one suggested.

hetge guide [ship-it|run-it]

Prints a guide condensed for the terminal: ship-it (stage, release, promote) or run-it (the local stack). No topic means a picker, or the list when piped.

hetge completion [bash|zsh|fish|powershell] --install

Prints a shell completion script, generated from the same command list as this page. --install writes it to the shell's standard location. With no shell named, a terminal gets a guided setup; without one, it says how to name a shell and exits 130.

Typical Workflow

1
hetge sync
First time only: creates your project on Dokploy
2
hetge stage
Build, push to GHCR, and deploy to staging
3
hetge release
Bump, tag, notes, GitHub Release, then name the staged image
4
hetge promote
Ship that exact image to production

Notes

→ Exit codes, for scripts and CI: 0 done (for a check, in step), 1 an error, 2 drift (env:check found some, or release / promote refused because of it), 130 cancelled: Ctrl-C or Escape at a prompt, or a "no" to a confirmation that ends the command (with no terminal and no --yes, nobody can say yes, so it is a "no"; so do hetge init, bare hetge and hetge completion with no shell, which need a terminal to ask at all). A run stopped by a signal exits 128 plus its number, as a shell reports it: 130 for Ctrl-C, 143 for SIGTERM. hetge exec exits with its command's own code instead. Under --json stdout is the JSON and nothing else: warnings and errors go to stderr, and nothing asks a question, not even op signin (env:check --json without a session exits 1). A minor release may add JSON fields; removing or renaming one is a major release. A command given an argument it does not take is refused rather than ignored, and so is a switch given a value (--deploy=false: leave the flag out instead), an empty value (--tag=), or a flag given two different values (--target staging --target production)
→ Deploy settings (image, Dokploy project, compose, domains) live in hetge.jsonc; the Dokploy URL and API key live in ~/.hetge/env, shared by every project (or in ~/.hetge/env.<profile> for a project that names an operator profile), and hetge login fills them
→ Guides: Ship It walks through stage → release → promote; Run It Locally covers the shared dev stack. Both are condensed in the terminal too: hetge guide ship-it and hetge guide run-it
→ When .tpl templates are updated, run env:pull to fetch the latest secrets
→ After modifying .env files locally, run env:push to sync to 1Password, then sync to update Dokploy
→ When docker-compose.yml or a target's domains change, run sync to update Dokploy. Sync deletes any domain the config does not declare, so read the plan it prints
→ A service Dokploy builds from git declares dokploy.application instead of dokploy.compose, with features.build off: sync manages its env and domains, deploy rebuilds it from git, and no GitHub login is needed
→ Docker must be installed and running for build. The CLI verifies the daemon on every build and prints platform-specific hints (e.g. open -a Docker on macOS) if it's missing or stopped