Back to Install

Run It Locally

Every app on your machine shares one local Postgres, Redis and S3. Your app's env file says what it needs, and hetge dev:up creates it, refusing a file whose Postgres, Redis or S3 settings point anywhere but this stack.

Overview

One stack, every app

The whole thing rests on one idea: the stack is shared, your data is not. There is one hetge-dev on the machine, managed only by the CLI, and each app gets its own slice of it.

Shared · started once

The stack

  • Postgres 18, Redis 8 and RustFS (S3), pinned by digest, on ports no hetGE app publishes.
  • One Docker compose project, hetge-dev, written to ~/.hetge/dev/compose.yml by the CLI.
  • Starts with the first hetge dev:up on the machine, from any directory. dev:down removes the containers and keeps the data; only dev:down --volumes deletes it.
  • Root credentials are deliberately plain: everything binds to 127.0.0.1 only.

Yours · per project

Your slice

  • A Postgres role and database, from POSTGRES_USER, POSTGRES_PASSWORD and POSTGRES_DB in your dev env file (.env.development.local, or whatever dev.env in hetge.jsonc names). The role is a superuser, so a staging dump's extensions restore.
  • One bucket for each name an S3_*BUCKET* variable in that file gives; the ones dev.publicBuckets lists get anonymous read.
  • Nothing to create for Redis: the app picks its own index (REDIS_DB), as in production.
  • dev:reset and dev:pull touch only this. Other apps never notice.
Service Address Credentials
Postgres 18 localhost:55432 root postgres / postgres; your app uses the role its env file names
Redis 8 localhost:56379 password hetge
RustFS (S3) http://localhost:59000 hetgedev / hetgedev123
RustFS console http://localhost:59001 the same, in a browser
✓

The rule that makes this safe

Development should never run against a remote database. hetge dev:up reads your dev env file before it starts anything and refuses, naming the variable, if the Postgres, Redis or S3 settings in it point anywhere but the stack: the old SSH tunnel port, a production S3 URL. It checks only what that file says. A setting the file leaves out is not checked, and your app may take it from .env instead, so keep every Postgres, Redis and S3 setting the app uses in the dev env file. When you need real data, hetge dev:pull copies staging's down. The data comes to you; you never go to it.

Keep tests off the stack: an app's pnpm test and pnpm e2e should use their own throwaway databases, never hetge-dev or your data.

Part One

Every day: the loop

1

Get your env files

$ newgrp onepassword-cli # Linux, if op cannot reach the 1Password app
$ hetge env:pull

Fills the env files hetge.jsonc lists (onepassword.items) from 1Password, using their committed templates. When the project lists its dev env file there (.env.development.local unless dev.env names another), that one is filled too, already pointing at the stack. If you have just cloned, this is the first thing to run, and after that only when the secrets change.

A file you already have is overwritten only after op2env asks: a no skips that file, Ctrl-C stops the run, and either one ends with exit 130. --force overwrites without asking, and hetge env:pull .env.development.local pulls just the dev file. The newgrp line is for Linux, when the 1Password app integration is on and the shell lacks its group.

2

Start the stack and provision your app

$ hetge dev:up

Starts Postgres, Redis and RustFS if they are not already running (the first time downloads the images), then creates your app's role, database and buckets. Run it as often as you like: it creates only what is missing, sets your role's password back to the one in the env file, drops nothing, and finishes by printing where everything is.

A dev env file that is missing or names no database and no bucket gets the stack alone: dev:up says "nothing to provision" and succeeds. If you expected a database, check that the file is there (hetge env:pull).

Apps wrap it as pnpm db:up. Outside a project it manages the stack only.

3

Generate, migrate, seed, run

$ pnpm db:generate # the app's own generate step, if it has one
$ pnpm db:migrate # the app's own migrations
$ pnpm db:seed # if the app has seeds
$ pnpm dev

The database exists but is empty; the app's own scripts fill it. hetge does not own migrations or seeds. To look at what is in a bucket, open the console at http://localhost:59001.

4

When something cannot connect

$ hetge dev:status

Shows each service's state and ports, and whether your database and each bucket exist. If the stack is not running it says so. Safe to run any time.

5

Done for the day

$ hetge dev:down

Stops and removes the containers, for every app on the machine, since the stack is shared. Every app's data stays in Docker volumes and is back at the next dev:up. Leaving the stack running is fine too: it is three small containers on loopback. It does not come back by itself after a reboot or a Docker restart: run hetge dev:up again.

That is the loop. Nothing in it needs a VPN, a tunnel, or a credential that is not already on your machine.

Part Two

Real data, and starting over

1

Copy staging's database down

$ hetge dev:pull

Drops your local database, recreates it, then streams a pg_dump of staging's database over ssh straight into it. No tunnel, no local Postgres tools, no dump file on disk, and no password leaves the server: the dump runs inside the staging container as its own superuser.

It asks first, and the default answer is yes: Enter replaces your local database. Before asking, it refuses and names the fix when this machine has no ssh, the project has no deployable staging target in hetge.jsonc, or the staging env file is not here (hetge env:pull) or has no POSTGRES_DB, which names the database it copies. The stack must be running (hetge dev:up), and your dev env file needs its POSTGRES_* block.

The ssh side is not tested until the copy starts, after your local database has been dropped. The hetge alias (dev.pull.ssh) must log in without a prompt and be allowed to run docker on the server, so check it first:

$ ssh -o BatchMode=yes hetge docker ps

If the copy fails, the message says so, and the database is empty or holds only part of the copy: run dev:pull again. For an empty schema instead, run hetge dev:reset (which also empties this project's buckets) and then pnpm db:migrate.

2

Start from nothing

$ hetge dev:reset

Drops and recreates this project's database and buckets. Other apps' data is untouched. It asks first. Then pnpm db:migrate and pnpm db:seed as usual.

3

Wipe the whole machine's local data

$ hetge dev:down --volumes

Deletes every app's local data, not just this project's. It asks first. Reach for dev:reset unless you really mean everyone.

!

Pull, reset and --volumes destroy data, on purpose

All three confirm before doing anything. dev:pull's question defaults to yes, the other two to no. None runs unattended without --yes: with no terminal to ask, the command says it needs one and stops. A no, Ctrl-C, or no terminal all change nothing and exit 130, so a script can tell a decline from an error (1). A pulled copy is real staging data: treat it with the care you would give staging itself, and never push it anywhere.

Part Three

When dev:up says no

dev:up reads your dev env file before it writes or starts anything, and refuses if any of it points away from the stack. Every problem is listed at once, variable by variable, and nothing is changed. dev:status, dev:reset and dev:pull check the same file the same way.

Variable Must be Checked when
POSTGRES_HOST localhost or 127.0.0.1 any of POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB has a value
POSTGRES_PORT 55432 same
POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB all three present; names use letters, digits, _ and - only same
REDIS_HOST, REDIS_PORT, REDIS_PASSWORD localhost or 127.0.0.1, 56379, hetge REDIS_HOST is present
S3_ENDPOINT_URL http://localhost:59000 or http://127.0.0.1:59000 any S3_*BUCKET* variable has a value
S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY hetgedev, hetgedev123 same
S3_INTERNAL_ENDPOINT_URL http://localhost:59000 or http://127.0.0.1:59000 present and not blank
S3_*BUCKET* values a valid bucket name: 3 to 63 characters of lowercase letters, digits, dots and hyphens, starting and ending with a letter or digit the variable has a value
dev.publicBuckets entries each one the value of an S3_*BUCKET* variable in the file always
✓

The two classic mistakes

POSTGRES_PORT=54320 is the old SSH tunnel: with it, pnpm dev was writing to a server. S3_INTERNAL_ENDPOINT_URL pointed at https://s3.hetge.com is a production URL that would send a local app's uploads into the real object store. Both used to be silent defaults; now they are errors that name the variable and the value the stack expects.

If 1Password already has the right values, hetge env:pull is the fix. Otherwise fix the value in the env file and run hetge env:push, so 1Password has it and every machine gets it at its next pull; commit the .tpl if the push changed it. A dev.publicBuckets entry that no bucket variable mentions is fixed in hetge.jsonc and committed. Never in the CLI.

A healthy dev env file looks like this. A file that names no database and no bucket is fine too: dev:up reports "nothing to provision" and succeeds. A file that does not exist reads the same way, so if you expected a database, check that the file is there (hetge env:pull).

.env.development.local

POSTGRES_HOST=localhost
POSTGRES_PORT=55432
POSTGRES_USER=courier
POSTGRES_PASSWORD=courier
POSTGRES_DB=courier

REDIS_HOST=localhost
REDIS_PORT=56379
REDIS_PASSWORD=hetge
# REDIS_DB is the app's own index, as in production

S3_ENDPOINT_URL=http://localhost:59000
S3_ACCESS_KEY_ID=hetgedev
S3_SECRET_ACCESS_KEY=hetgedev123
S3_BUCKET=courier

Other things it can say

→ The local stack did not come up healthy. A service did not start, or did not pass its healthcheck. The lines after it name any container holding one of the stack's ports and each service that is not healthy, and the last line gives the docker compose logs command to read.
→ port 55432 (postgres) is already used by <container> Another container holds the port, usually an old per-app one from before the shared stack. Stop it (docker stop <name>), and remove it once you know you no longer need it (docker rm <name>); its data was never the stack's. Then run dev:up again.
→ The stack reports healthy, but Docker is not actually publishing its ports The stack reports healthy, but its published ports are not reachable Either one, seen on WSL2. Do what the message says: docker compose -p hetge-dev -f ~/.hetge/dev/compose.yml down, then hetge dev:up again.
→ Failed while provisioning database … / provisioning buckets The step is named and nothing earlier is undone. Every step is safe to repeat, so fix the cause and run dev:up again.

Part Four

Configuration

Every setting hetge reads for a project lives in hetge.jsonc. The dev block is optional; these are the defaults:

hetge.jsonc

"dev": {
    "env": ".env.development.local",
    "publicBuckets": [],
    "pull": { "ssh": "hetge", "container": "hetge-postgres-staging" }
}

env names the file dev:up provisions from. publicBuckets lists buckets that get anonymous read, as they have in production; each must appear in the env file. pull says which ssh alias and which container dev:pull dumps from.

i

Still on an older config? Move it once with the last 0.x release

The dev block, and the staging target that dev:pull reads, exist only in hetge.jsonc, the one config file hetge 1.0 reads. A project whose hetge config is still in package.json or pyproject.toml, or that still carries .env.deploy.* files, moves over once with 0.22, before you use 1.0 on it:

$ npx @hetge/cli@0.22 config:migrate

It writes hetge.jsonc, strips the old field, folds the deploy files in and deletes them, with their .tpl, without asking: run it on a clean git tree, and copy any deploy file git does not track first. It reads the deploy file, never its .tpl: on a fresh clone, where only the template is tracked, create the file first (op inject -i .env.deploy.local.tpl -o .env.deploy.local), or it leaves those settings out (on a project already on hetge.jsonc it answers "Nothing to migrate"). It drops any credentials it finds with a note: those belong in ~/.hetge/env, which hetge login fills. Review the diff and commit it all together. A hetge.json is not moved by it: rename that to hetge.jsonc first. If hetge refuses to load a project and names this command, this is what it means.

Part Five

Setting up a new machine

Install hetge first (Install): it needs Node.js 24 or newer, which the install script sets up when it is missing. Then:

$ op signin # sign in to 1Password
$ hetge login # GitHub, GHCR, the shared credentials, ~/.npmrc
$ hetge dev:up # the stack, from anywhere

You need a Docker engine with the compose and buildx plugins: Docker Desktop, OrbStack on macOS, Colima, Rancher Desktop, or Docker Engine on Linux. dev:pull also needs ssh and the hetge alias in your ~/.ssh/config, logging in without a prompt. The CLI checks for Docker, both plugins and ssh before it runs and names whatever is missing; the alias is only tried when the copy starts.

Reference

Every command, briefly

Every flag of every command is in the Command Reference. In the menu (hetge on its own), these are under Local Development: Start local stack, Local stack status, Pull staging data, Reset local data, Stop local stack (which keeps the data). hetge guide run-it prints this page in short.

Command What it does Destroys local data?
hetge dev:up Starts the stack, creates your database and buckets No
hetge dev:status Shows the stack and whether your database and buckets exist No, read only
hetge dev:down Stops the stack No, data kept
hetge dev:down --volumes Stops the stack and deletes its volumes Yes: every app's
hetge dev:reset Drops and recreates this project's database and buckets Yes: this project's
hetge dev:pull Replaces this project's database with a copy of staging's Yes: this project's database
hetge env:pull Fills your env files from 1Password Overwrites env files, after asking
pnpm db:migrate / db:seed The app's own schema and seed data The app's business

Summary

Rules of thumb

Do

  • After a fresh clone, run hetge env:pull and then hetge dev:up; run dev:up again whenever the app cannot connect.
  • Leaving the stack running is fine: it is small and shared. dev:down stops it for every app.
  • Use dev:pull when a bug only shows on real data.
  • Read the refusal. It names the variable and the value it wants.

Don't

  • Write a docker-compose.dev.yml or start a Postgres, Redis or MinIO container by hand. That is what this replaced.
  • Point .env.development.local at the SSH tunnel or a remote S3. dev:up will refuse, and the app would be writing to a real server.
  • Edit ~/.hetge/dev/compose.yml. Every dev:up rewrites it.
  • Use dev:down --volumes to fix one app. That is dev:reset.
  • Add --yes unless a script is running unattended.

The shared stack exists so that no app's pnpm dev ever writes to a server again. Getting it to staging and production is the other page: Ship It.