Running Multiple Instances
Running Multiple Instances
Several clones or jj workspaces of this project on one machine compete for the same ports. The
recommended pattern is to give each clone explicit, distinct ports in its two local env files,
rather than leaning on defaults or automatic port selection — then nothing depends on which instance
you happened to start first. Neither file is committed:
| File | Read by | Sets |
|---|---|---|
.env.dev.local |
make dev, make mailpit |
dev server port, mailpit’s host ports, email |
.env.test.local |
make test, make test-e2e |
email (opt-in, see Running tests) |
Start mailpit with make mailpit rather than a bare docker compose up mailpit: mailpit is defined
in docker-compose.dev.yml (not in the docker-compose.yml self-hosters copy), and on its own
compose reads only a file literally named .env, so the make target passes both -f and
--env-file .env.dev.local. That keeps each clone’s ports in one place.
Example: two clones side by side
Clone A in ~/src/sb-a keeps the defaults:
# .env.dev.local
DATABASE_URL=file:./data.db
AUTH_SECRET=<openssl rand -base64 32>
PORT=3000
SITE_URL=http://localhost:3000
COMPOSE_PROJECT_NAME=sb-a
MAILPIT_SMTP_PORT=1025
MAILPIT_UI_PORT=8025
SMTP_FROM=dev@example.test
SMTP_URL=smtp://localhost:1025
# .env.test.local
SMTP_URL=smtp://localhost:1025
MAILPIT_API_URL=http://localhost:8025
SMTP_FROM='Test <mailer-test@test.example>'
Clone B in ~/src/sb-b shifts every port by one:
# .env.dev.local
DATABASE_URL=file:./data.db
AUTH_SECRET=<openssl rand -base64 32>
PORT=3001
SITE_URL=http://localhost:3001
COMPOSE_PROJECT_NAME=sb-b
MAILPIT_SMTP_PORT=1026
MAILPIT_UI_PORT=8026
SMTP_FROM=dev@example.test
SMTP_URL=smtp://localhost:1026
# .env.test.local
SMTP_URL=smtp://localhost:1026
MAILPIT_API_URL=http://localhost:8026
SMTP_FROM='Test <mailer-test@test.example>'
Each clone can now run make mailpit, make dev, make test, and make test-e2e at the same time
as the other. Not every line is always needed: COMPOSE_PROJECT_NAME, MAILPIT_SMTP_PORT, and
MAILPIT_UI_PORT are consumed by docker compose rather than the app, and the SMTP_*/SITE_URL
lines only matter if you want the dev server itself to send email — PORT alone is enough
otherwise.
What each variable is for
Mailpit publishes two host ports, and the two app-side variables that point at them are unrelated to each other — each tracks a different one:
| Compose variable | Default | Mailpit port for | App-side variable to match |
|---|---|---|---|
MAILPIT_SMTP_PORT |
1025 |
receiving mail over SMTP | SMTP_URL |
MAILPIT_UI_PORT |
8025 |
the web UI and its REST API | MAILPIT_API_URL |
SMTP_URL(utils/mailer.ts) is how schellingboard sends mail: an ordinary SMTP connection string, with mailpit merely one possible server behind it.MAILPIT_API_URL(tests/helpers/mailpit.ts) is how the test suite reads mail back out of mailpit to assert on it. It is test-only and mailpit-specific; the app itself never reads it.
PORT is the port make dev serves the SPA on; the server runs on the next one. Set it for a
second instance, or the second make dev fails on the taken port. SITE_URL must agree with
PORT, since it’s the base URL emails use to link back to the site; a stale value sends readers to
the other instance.
COMPOSE_PROJECT_NAME only matters when two clones share a directory name — compose derives the
project from the directory, so ~/a/sb and ~/b/sb would otherwise fight over a single mailpit
container. Setting it is cheap insurance. Note that mailpit lives in its own compose file, so a bare
docker compose down won’t stop it; use the same two flags make mailpit passes —
docker compose -f docker-compose.dev.yml --env-file .env.dev.local down — since without the env
file compose looks for the container under the directory-derived project name instead. (Or just
Ctrl-C the foreground make mailpit.)
Some things need no attention: DATABASE_URL and SB_UPLOADS_DIR are relative paths, so each clone
already gets its own database and uploads. E2E runs pick a free port per run and override SITE_URL
to match (playwright.config.ts), so don’t set SITE_URL in .env.test.local expecting it to
apply — only SMTP_URL and MAILPIT_API_URL matter there. make test binds no port at all.