Local Development
Stable, named URLs for Ruby development — https://<app>.localhost instead of a memorized port. yamine gives every app its own hostname, works across git worktrees, and stays trusted with per-host TLS.
gem "yamine"
gem install yamine
cd ~/code/myapp && yamine
# -> https://myapp.localhost
Requires only Ruby stdlib (openssl, socket) — no extra dependencies.
How it works
yamine initwritesconfig/local.yml— the source of truth for the app name, its processes, and their environment (it migrates an existing Procfile). Booting without it fails with the fix, not a guess.- Boots the app — managed Rack apps on a unix socket (zero TCP ports), everything else via
PORT. - Serves
https://*.localhoston port 443 with per-host certs from a local CA, routing byHostheader.
.localhost resolves to loopback natively in Chrome, Firefox, and Edge. Safari, custom TLDs, and resolvers that read only /etc/hosts (CGO-disabled Go binaries are the common case) need yamine hosts sync. Doctor keeps the two apart: a name only file-only resolvers cannot see is a warning, a name nothing resolves is a failure.
config/local.yml
service: myapp
proxy:
tld: localhost # or host: myapp.local.example.com
subdomains: false # opt in to answering *.myapp.localhost
processes:
web:
cmd: bin/rails server -p $PORT
proxy: true
healthcheck: { path: /up, timeout: 30 }
worker:
cmd: bundle exec sidekiq
proxy: false # background process — spawned, no route
env:
clear:
RAILS_ENV: development
secret: [RAILS_MASTER_KEY] # values come from gitignored config/local.secrets
Top-level db: false opts out of the per-worktree database. yamine start --wait (the default) blocks until every healthcheck passes; --json streams one machine-readable event per phase for agents.
Pick a hostname
{variant}.{service}.{app}.{tld}
| Axis | Example | Source |
|---|---|---|
| app | myapp | service: in config/local.yml (yamine init infers it) |
| service | api.myapp | a non-web process name (every proxy: true process but web) |
| variant | fix-ui.myapp | --variant, YAMINE_VARIANT, linked worktree branch |
| tld | myapp.preview.example.com | --tld (default localhost) |
Linked git worktrees get a branch prefix automatically (fix-ui.myapp.localhost); the main checkout keeps the bare name.
yamine # -> https://myapp.localhost
yamine --variant demo # -> https://demo.myapp.localhost
yamine --tld preview.example.com # your own domain (OAuth parity)
Child processes receive YAMINE_URL (the stable URL — use it for OAuth callbacks, mailer hosts, webhook URLs), PORT, and HOST.
Worktrees
Linked git worktrees get a branch prefix automatically (feature-login.myapp.localhost); the main checkout keeps the bare name. Each worktree also gets its own database — injected as DATABASE_URL — so concurrent agents never share tables or migrations.
yamine owns the whole lifecycle:
yamine worktree add feature/login # worktree + config + database, ready to boot
yamine worktree list # every worktree: db, dirty, merged
yamine worktree remove feature/login # stop, drop db, remove worktree
yamine worktree clean # tear down everything already merged
add lands the worktree beside the repo, copies the gitignored per-checkout config (config/local.yml, config/local.secrets), runs bundle install, and pre-creates the database — the next step is just yamine start in it. clean is the done-and-merged sweep: it never touches uncommitted work, and unmerged branches survive everything except remove --force (git branch -d refuses what git has not seen merged). --dry-run prints the plan before anything happens.
Commands
| Command | What it does |
|---|---|
yamine | Boot every process in config/local.yml behind the proxy |
yamine get <name> | Print the URL for cross-service wiring |
yamine alias <name> <port> | Static route (e.g. a Docker container) |
yamine list | Show active routes and liveness |
yamine doctor | Health checks (state, proxy, routes, DNS, CA) |
yamine open | Open the app URL in a browser |
yamine log -f | Tail the backend log |
yamine trust | Add the local CA to the system trust store |
yamine stop | Stop this app’s backends and routes (machine-readable exit codes) |
yamine status | Show the effective naming context here |
yamine db list\|create\|drop | Per-worktree databases |
yamine worktree add\|list\|remove\|clean | Worktree lifecycle — see Worktrees |
yamine hosts sync | Write the managed block to /etc/hosts (Safari, custom TLDs, file-only resolvers) |
yamine clean | Remove state and /etc/hosts entries |
Rails integration
Rails apps need no extra gem. yamine injects RAILS_DEVELOPMENT_HOSTS=<hostname> into each spawned process, so the proxied .localhost host is allowed automatically — no config.hosts patch, no initializer. Boot a Rails app the same way as anything else:
yamine init # detects Rails, writes config/local.yml with bundle exec puma
yamine # boots it behind https://<app>.localhost
Read the injected URL wherever app code needs its own address — never hardcode localhost:3000:
ENV.fetch("YAMINE_URL", "http://localhost:3000")
(The former yamine-rails gem is deprecated — its hosts patch became the env injection, and its remaining Cable-origins helper is an optional convenience.)
Agent skill
The gem ships a yamine skill under ask/skills/, auto-discovered by ask-skills. Agents boot with yamine start --json (one flushed event per phase, payload last, failures carrying the log tail), wire URLs via yamine get, stop via yamine stop’s exit codes, and finish worktree branches with yamine worktree clean --dry-run first.
Next steps
- yamine on GitHub — routing, TLS, and worktree details
- yamine-rails — deprecated (moved to deprecated/), superseded by
RAILS_DEVELOPMENT_HOSTSinjection