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
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
hetge-dev, written to
~/.hetge/dev/compose.yml by the CLI.hetge dev:up on the machine, from any directory.
dev:down removes the containers and keeps the data; only
dev:down --volumes deletes it.127.0.0.1 only.
Yours · per project
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.S3_*BUCKET* variable in that file gives; the
ones dev.publicBuckets lists get anonymous read.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
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.
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.
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.
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.
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
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.
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.
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
dev:up says nodev: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
docker compose logs command to read.
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.
docker compose -p hetge-dev -f
~/.hetge/dev/compose.yml down, then hetge dev:up again.
dev:up again.
Part Four
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.
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
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 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
Do
hetge env:pull and then hetge dev:up;
run dev:up again whenever the app cannot connect.dev:down stops it
for every app.dev:pull when a bug only shows on real data.Don't
docker-compose.dev.yml or start a Postgres, Redis or MinIO container
by hand. That is what this replaced..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.~/.hetge/dev/compose.yml. Every dev:up rewrites it.dev:down --volumes to fix one app. That is dev:reset.--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.