For contributing to ETLauncher, editing a gem locally, or the internals
behind the day-to-day commands in the main README.md.
Run app commands against the running services (they use each service's own installed gems/packages):
docker compose exec etengine bin/rails console
./bin/test etmodel # full suite; add a path/line to narrow it
docker compose logs -f etmodel
docker compose exec database mysql -uroot -pdev # the shared DB (all apps)./bin/test <service> [rspec args] wraps docker compose exec -e RAILS_ENV=test <service> bundle exec rspec. RAILS_ENV=test is required: the containers set
RAILS_ENV=development, so spec/rails_helper.rb's ENV['RAILS_ENV'] ||= 'test' never
applies. Without it the test-group gems are never required (uninitialized constant FactoryBot) and any spec that did load would run against the development database.
There is no host-level access to the database - database has no port
mapped to the host, only to the other containers.
The bin/ scripts are host orchestration (they shell out to docker compose
and docker inspect), so they're terminal-only - Docker Desktop can't run
them. But once the stack exists you can do most day-to-day work from the GUI:
- First run is terminal:
./bin/uponce (builds, seeds, creates the project). Theetlauncherproject then appears under Containers. - Lifecycle: start / stop / restart the project or a single service from
the Containers view - no script needed for up/down. Use the Logs tab
instead of
docker compose logs. - Per-container tasks - Containers → service → Exec:
etengine→bin/rails db:migrate,bin/rails consoleetmodel→bundle exec rspecdatabase→mysql -uroot -pdev(password isDB_ROOT_PASSWORD)
- Arrow keys not working in Exec? Docker Desktop's Exec opens the
container's basic
/bin/sh(dash), which has no command history or line editing. Typebashfor a full shell - every app image (etengine,etmodel,my-etm,database) has it, but none of them shipzsh. For a richer shell (history, oh-my-zsh), use a real terminal againstworkspaceinstead, which does havezsh:docker compose exec workspace zsh. - Reset everything (destructive): delete the
etlauncher_db_datavolume (Volumes tab), or delete the project with its volumes from the Containers view, then run./bin/uponce to rebuild and reseed. Same asdocker compose down -v && ./bin/up. - Stays terminal-only:
./bin/up,./bin/update,./bin/seed-users,./bin/seed-oauth,./bin/db-dump,./bin/db-restore.
- Rails apps (etengine, etmodel, myetm):
app/code, views, and routes reload on the next request (Zeitwerk in development). Restart the service for initializer orsettings.ymlchanges:docker compose restart etengine. - Collections (Next.js): runs
next dev- components hot-reload in the browser instantly. - ETModel JS: no dev server -
config/shakapacker.ymlhascompile: true, so packs recompile on the next request. No live-reload; reload the page after a JS change. - Translations (i18n-js JS bundle): i18n-js's Sprockets integration
doesn't track
config/localesas a dependency of the compiled JS it embeds translations into, so an edit alone won't invalidate the cached compile - restart the service:docker compose restart etmodel.tmp/cache/assetsistmpfsfor this reason (seecompose.yaml), so the restart always recompiles from cold rather than reusing a stale cache. - ETSource: switching branches in your
etsource/checkout is picked up automatically by etengine's file watcher; make any request and it reloads from the new branch.
./bin/update exists because gems and yarn packages aren't baked into the
Docker image - they install from the Gemfile/package.json in the
bind-mounted repo when the container boots (see bin/app-boot.sh), into a
named volume that persists between runs. So a changed Gemfile.lock is picked
up by a container restart (a few seconds), not an image rebuild (slow).
./bin/update # Gemfile/lock or package.json changed
./bin/update --build # also rebuild images - only for Dockerfile / system-package changesSafe to run anytime. Nothing reinstalls if the pull changed no dependencies, but the app
containers are always recreated - expect roughly a minute before every app serves again.
It also runs db:prepare for each app (MyETM → ETEngine → ETModel), so a migration
pulled in with the new code is applied automatically - no separate migrate step.
Yes. All app databases live in one MySQL container backed by the
single db_data Docker named volume (mirroring prod's one MySQL per
server), independent of the container:
| Command | DB data |
|---|---|
docker compose stop / down, reboot, ./bin/update |
persists |
docker compose down -v |
destroyed (volume removed) |
To snapshot and roll back without destroying anything (e.g. before risky
data work), use the dump/restore helpers - they write to the git-ignored
dumps/:
./bin/db-dump # dumps all databases to dumps/database-<timestamp>.sql
# ... experiment ...
./bin/db-restore # restores the latest dump
./bin/db-restore dumps/database-20260629-120000.sql # or a specific fileTo start completely clean (destroy data, rebuild, reseed):
docker compose down -v && ./bin/up.
The workspace service is the central toolchain - Ruby, Node, yarn, Python,
uv, pyetm, mysql-client, git - with every repo bind-mounted under
/workspace. It has no dependencies, so it runs on its own without the app
stack (you only need the stack up to talk to your local model).
Open a shell any of three ways:
# Terminal - stack down: starts a throwaway container, exits on Ctrl-D
docker compose run --rm workspace zsh
# Terminal - stack up: attach to the running container
docker compose exec workspace zsh- VS Code: open the
etlauncherfolder and choose "Reopen in Container". This starts only the workspace (not the whole stack); the integrated terminal opens inside it. Start the apps separately with./bin/upwhen needed. - Docker Desktop: Containers →
etlauncher-workspace→ Exec.
pyetm is installed system-wide - import pyetm works with no venv:
python3 -c "import pyetm; print(pyetm.__version__)"In VS Code, open a .ipynb and pick the container's Python interpreter
(/usr/local/bin/python) as the kernel. The bundled ipykernel + Jupyter
extension run it with no setup.
The workspace ships a stock oh-my-zsh shell with no baked team config - bring your own dotfiles:
- VS Code dev container: point the dotfiles feature at your repo (it's
cloned and installed into the container). Add to your user VS Code
settings.json:"dotfiles.repository": "your-github-user/dotfiles"
- Terminal-only: bind-mount your own
.zshrc, e.g.docker compose run --rm -v ~/.zshrc:/root/.zshrc workspace zsh.
By default the apps use the Quintel gems (merit, atlas, fever, refinery,
turbine, identity) pinned to a GitHub ref: in their Gemfile - matching CI
and production. (turbine-graph is the one exception: it floats on a
RubyGems version, >=0.1, not a GitHub ref.) To edit a gem locally without
pushing and bumping the ref, opt in per run with DEV_GEMS:
DEV_GEMS=merit,atlas ./bin/up # or ./bin/updateClone the gems you need as flat siblings, same level as the app repos:
git clone git@github.com:quintel/merit.git
git clone git@github.com:quintel/atlas.git
git clone git@github.com:quintel/fever.git
git clone git@github.com:quintel/rubel.git
git clone git@github.com:quintel/osmosis.git
git clone git@github.com:quintel/refinery.git
git clone git@github.com:quintel/turbine.git
git clone git@github.com:quintel/etplugin.git
git clone git@github.com:quintel/identity_rails.git~/Github/etm/
├── etengine/
├── etmodel/
├── etsource/
├── multi-year-charts/
├── my-etm/
├── etlauncher/
├── merit/
├── atlas/
├── fever/
├── rubel/
├── osmosis/
├── refinery/
├── turbine/
├── etplugin/
└── identity_rails/
This mounts the parent directory into the app containers at /workspace and
rewrites the listed gems to path: overrides at boot, so edits in
../merit, ../atlas, … are live in the running app. Unset DEV_GEMS and
re-run ./bin/up to return to the pinned refs.
Checkout not a flat sibling? Give its path relative to the parent
directory: DEV_GEMS=atlas=some/other/place. The path must resolve under the
parent - that's the only directory mounted into the containers; anything
outside it doesn't exist as far as the container is concerned.
A missing or unresolvable checkout fails the boot rather than silently falling back to the pinned ref - better to find out immediately than debug why an edit isn't showing up.
Verify the override took:
docker compose exec -e BUNDLE_GEMFILE=/tmp/dev-bundle/Gemfile etengine \
bundle info quintel_merit # → Path: /workspace/meritThe BUNDLE_GEMFILE matters: app-boot.sh exports it only for the server process it
execs, so a plain docker compose exec reads the unmodified /app/Gemfile and reports
the pinned git ref - making a working override look broken.
Notes:
- Values are sibling repo dir names -
merit,atlas,fever,refinery,turbine,identity_rails,etplugin,rubel,osmosis- mapped internally to the gem's name in the Gemfile. - The local checkout should be on a branch whose history contains the
ref:SHA pinned in the consuming app'sGemfile, so Bundler can still resolve the gem's own dependencies. Checking out the branch you're developing is enough. - A gem only overrides in apps that actually declare it:
identityapplies to etengine + etmodel;merit/atlas/fever/refinery/turbineto etengine;etpluginto etmodel only.
By default Collections runs next dev for instant hot reload. Staging and
production don't: both serve the quintel/collections image built from
../multi-year-charts/Dockerfile (next build + standalone node server.js). To run that same image locally, opt in per run:
COLLECTIONS_PROD=1 ./bin/upThis layers compose.collections-prod.yaml over the base compose file. Use
it to verify a change behaves under a production build before deploying -
it's the only local path that exercises the build the deploy uses.
Notes:
- No hot reload. The image is built once; code changes need a rebuild:
COLLECTIONS_PROD=1 ./bin/update --build. NEXT_PUBLIC_*URLs are baked in at build time, not read at runtime (Next inlines them into the client bundle). The overlay passes them as build args from your.env(ETENGINE_URL,ETMODEL_URL,MYETM_URL). Change a URL → rebuild.- Server-only secrets (
NEXTAUTH_SECRET,AUTH_CLIENT_SECRET, …) stay runtime env, so they do not require a rebuild.
- Creates
.envfrom.env.exampleif missing; warns (doesn't overwrite) if an existing.envis missing a variable.env.examplenow has. Checks ports 3000/3001/3002/3005 are free and the sibling checkouts exist - if the stack is already running, this fails fast here without touching anything; use./bin/updateto pick up changes on a running stack instead. - Reports Docker's disk usage, then removes ETLauncher's own stopped
containers and leftover untagged images - filtered by the
com.docker.compose.project=etlauncherlabel, so other projects are never touched. Builds images and starts the shareddatabase(waits until healthy). db:preparefor MyETM → ETEngine → ETModel (MyETM first - it is the OIDC issuer and its seed creates the admin user; all DBs live in the shareddatabase)../bin/seed-users+./bin/seed-oauth- idempotently create the dev admin/user and the Doorkeeper OAuth applications, with the fixed dev credentials from.env, so SSO works with no manual MyETM clicks.- Starts the whole stack.
- Docker disk filling up:
bin/upandbin/updateonly prune resources labelled as ETLauncher's, so a machine-wide clean-up is yours to run -docker system prune(add-ato drop unused tagged images too). Neither script does this for you because it reaches every other project on the machine and shrinks the shared build cache. NoScenariosMigratedduringbin/update: an ETEngine migration updates existing scenarios and yours has none it applies to. Nothing is wrong with your database - re-run withSKIP_SCENARIO_CHECK=1 ./bin/updateto record the migration as applied and move on. Leave the flag off when you are testing a scenario migration you wrote, where the error is important.- Python packages changed (e.g. pyetm version bump): rebuild the
workspace image -
docker compose build workspace- then reconnect. Named volumes for Ruby/Node deps are unaffected. - ETModel JS schema errors after a deps change: the named
etmodel_node_modulesvolume can hold stale packages. Refresh it withdocker compose run --rm --no-deps etmodel yarn install, thendocker compose up -d --force-recreate etmodel. - Config overrides:
overrides/holds container-correctsettings.local.ymlfiles mounted read-only over each Rails app, so the orchestrated values win regardless of any personalsettings.local.ymlin your checkout (your host files are never modified). - Edited an override or
.env? Recreate the app. Compose doesn't detect bind-mounted file-content changes, and Rails reads settings only at boot, so a plaindocker compose up -dwon't pick them up - rundocker compose up -d --force-recreate <service>. (Symptom of a stale app: an OIDC "redirect uri doesn't match" error after changing aclient_uri.)