How to add a new service type, database type, companion type, or migration tool. Each one is a single map entry in LocalDevStackCli's companion object plus a generator implementation; the supported-list error strings auto-derive from the map.
For external contributors, the high-level PR checklist in CONTRIBUTING.md → Before you submit a PR is the entry point; the per-extension steps below give the full detail. Every checklist below ends with "run the integration sweep" — see integration-sweep.md for how.
- Implement
ServiceGenerator(includeoverride val runCommand). - Subclass
DockerfileGenerator— only overrideprotected fun dockerfile(): String. The base class handles the rest. - Add one entry to the
SERVICESmap inLocalDevStackCli's companion object:"<type>" to ServiceSpec(::YourServiceGenerator, ::YourDockerfileGenerator, listOf(".:/app", ...)). - Add parameterized rows in
AllServiceGeneratorsTestandAllDockerfileGeneratorsTest. - Update the supported-types tables in
README.mdandCLAUDE.md. - Run the integration sweep against your new service × all 8 databases before merging — see integration-sweep.md.
Patterns the 72-combo integration sweep surfaced repeatedly — check each before you submit:
- Native deps in the chosen runtime. If your service installs gems / pip wheels / npm packages with C extensions, the slim variant of the base image probably lacks
build-essential. Either pick the full image (e.g.ruby:3.2notruby:3.2-slim) orapt-get install build-essential— but the latter adds minutes to the first build on a cold network. Full image is usually the right tradeoff for dev. - Hot-reload tooling install cost. If the hot-reload tool installs from source (
cargo install,go install, source-compiled), the first build can blow past any reasonable timeout. Prefer a pinned pre-built binary from a GitHub release. - Database driver build deps. A single image is used against all 8 databases, so any database-specific extension (e.g.
pdo_pgsql) needs its build-time deps unconditionally even when the user's DB choice doesn't need them at runtime. - Project-name sanitization. If
projectNameis interpolated anywhere strict (C# namespaces, Go module paths, Rust crate names, Java packages), sanitize it explicitly — picocli accepts hyphens. - Lockfile assumptions. If your service template doesn't write a lockfile, the Dockerfile must use the package manager's lock-optional install (
npm install, notnpm ci;bundle installwithout--frozen). - Framework weight. The whole point is "single
docker compose upboots a healthy/health." Heavy frameworks (Laravel, Rails) need acomposer install/bundle installthat fights this. We've deliberately picked minimal frameworks for PHP (built-inphp -S) and Ruby (Sinatra) — don't upgrade these without re-running the sweep.
- Implement
DatabaseGenerator. The compose YAML must end with\nvolumes:\n <name>_data:soappendMigrateBlockToComposeandappendCompanionBlocksToComposecan splice their additions.MigrationComposeAppenderTestandCompanionComposeAppenderTestenforce this. - Add one entry to the
DATABASESmap in the companion object:"<type>" to DbSpec(::YourDatabaseGenerator, mapOf("<ENV_KEY>" to "<url>"), { DbConnectionInfo(it, jdbcUrl = "...", user = "...", password = "...") }). JDBC URL, env var, credentials all in one place. - Add the database to
SUPPORTED_MIGRATIONS(in the same companion object) with the compatible migration tools, or an empty list for "no migration support". The supported-databases error string is auto-derived. - Update the supported-types tables in
README.mdandCLAUDE.md.
- Implement
CompanionGenerator(interface ingenerator/CompanionGenerator.kt) —companionName(lowercase identifier, also the compose service name and--withtoken),composeServiceBlock()(YAML snippet starting with<name>:, ending in newline). Optional:envOverlay(),namedVolumes(). - Add one entry to the
COMPANIONSmap inLocalDevStackCli's companion object:"<name>" to CompanionSpec(::YourCompanionGenerator). The supported-list error and--namecollision check are auto-derived. - Add rows to
AllCompanionGeneratorsTest.companions()plus a CLI-level test inLocalDevStackCliTest(mirror the existing--with mailhog/--with miniotests). - Score the candidate against the five companion criteria before merging — universal need across personas, zero-config single container, drop-in for a real cloud service, visible UI, mature stable image. If criterion #2 fails, it does not belong behind
--with. - Add the companion to the integration sweep — at minimum, one combo of
--service <any> --database <any> --with <new-companion>that boots/health200 plus the companion's own health endpoint. - Update the companion table in
README.mdandCLAUDE.md.
- Implement
MigrationGenerator(interface ingenerator/MigrationGenerator.kt) —toolName,generateScaffold,composeServiceBlock(must includeprofiles: ["migrations"],restart: "no", anddepends_on.db.condition: service_healthy),createMigrationHint. - For SQL-based tools, reuse
identityColumnSql(databaseType)fromMigrationSqlHelpers.ktfor the example migration's primary-key column. - Add the tool to
LocalDevStackCli.resolveMigrationGenerator()(the inner factorywhen) and to theSUPPORTED_MIGRATIONSmap for each compatible DB. - Pin the tool image at major+minor (matching the project convention for DB images). For npm-distributed tools, generate a
Dockerfile.migrateingenerateScaffoldand pin the package version strictly. - Add a per-tool unit test in
src/test/kotlin/com/localdevstack/generator/, plus tuples inAllMigrationGeneratorsTest.allGenerators()(andsqlGenerators()if SQL-only). - Add valid/invalid
(database, tool)rows to the@CsvSourcematrices inLocalDevStackCliTest. - Update the migration tools table in
README.mdand the migration generators table inCLAUDE.md.