The question this page answers: our services are spread over several repositories and teams, so where does the Flisk project go, who reviews changes to the shared parts, and what breaks if two teams pick names that clash?
How this page was written: the CLI (Flisk 1.3.86, Tilt 0.37.7, Docker Compose 5.5.1) was run in scratch directories, and the generated files and the Tilt resource list were read. Where something was only read in the source or the docs, it says so. flisk up could not be completed for these checks. Docker on the test machine had no free address pools (all predefined address pools have been fully subnetted), so docker network create failed and the project's Postgres and Traefik did not start. No service was started through flisk up for this page. Since #589 the init-networks step fails with Docker's own message when a network cannot be created; at the time of these checks it did not say so (read in the changelog, not re-run, because Docker was not used for this sweep). Everything below about what runs is therefore about what flisk and Tilt generate and load, not about containers that were observed answering requests. The Tilt resource lists were read with tilt alpha tiltfile-result -f .flisk/.flisk-out/Tiltfile, which evaluates the generated Tiltfile without starting anything.
- One Flisk project is one repository tree.
fliskfinds services by walking the project directory forservice.jsonfiles. A service in a sibling directory is not part of the project. See what happens with a service outside the project root. - Several repos, one place to run everything: a platform repository holds the one
.flisk/project.json, and each service gets aservice.jsonthere. Services whose code lives elsewhere are wrapped as bring-your-own resources, either from a registry image (image) or from a source checkout beside the platform repo (buildContext). Both loaded in Tilt in the checks here; neither was started. - A
service.jsoninside each service repo, each repo its own project: this is the ordinary single-repo flow, and it is the layout that keeps each team's files in their own repository. The cost is one Postgres and one Traefik per project, on the same default host ports. - Not supported: one project whose
discovery.pathspoints atservice.jsonfiles in other repositories. It looks as if it should work and fails; the details are below. - Two projects can coexist on one Docker daemon if their project names differ, with one clash that depends on stack and service names (names).
Three attempts, each in a project whose root is l565-hub/, with the other repo checked out beside it as l565-ext/.
| Attempt | What was observed |
|---|---|
flisk resource out --type bring-your-own --path ../l565-ext/out |
Refused: Path must be within the project directory |
service.json written by hand in ../l565-ext/outer/, and "../l565-ext/*" added to discovery.paths in .flisk/project.json |
flisk config verify and flisk doctor pass (doctor prints All resources are covered by discovery.paths). flisk resources, flisk stacks and flisk up --dry-run do not list the service. Tilt's own discovery does load it (`Loaded: outer |
A symlink inside the project (services/ext/linked) to a directory with a service.json in the other repo |
Not found by flisk resources or by Tilt. Symlinked directories are not followed |
Consequence: do not put service.json outside the project root. The CLI and Tilt disagree about it, and the generator will not write its files there.
A service.json that is inside the project may build from outside it. This was checked with "buildContext": "../../../../l565-ext/outer-src" and "dockerfile": "Dockerfile" on a bring-your-own service in services/ext/ctxsvc/. flisk resources lists it, the Tiltfile loads it (ctxsvc, ctxsvc-yaml, ctxsvc-run-only and so on), and the generated Compose file has context: ../../../services/ext/ctxsvc/../../../../l565-ext/outer-src. The image was not built, and whether Tilt watches files in that outside directory was not checked.
One more fact from this experiment: a hand-written service.json in a stack that is not listed in a phase's enabledStacks (in .flisk/project.json) was not loaded by the Tiltfile until the stack was added. flisk resource added app there itself in one of the projects used here, but not in another, and why it differed was not tracked down. If you write service.json files by hand, check that flisk up --dry-run and tilt alpha tiltfile-result list them. Whether flisk up <stack> makes up for a missing entry was not checked.
platform-repo/ owned by the platform team
.flisk/project.json one project: name, stacks, discovery.paths
services/
platform/database-management/ shared Postgres (generated Compose file)
orders/ stack "orders"
orders-api/service.json bring-your-own, image: registry/orders-api:dev
orders-worker/service.json bring-your-own, buildContext: ../../../../orders-repo
billing/
billing-api/service.json
shared-platform-engineering/ generated helper scripts
orders-repo/ cloned next to platform-repo only if you need its source
How a developer starts only what they need without cloning every repo: services that use image need no source checkout; the image has to exist in a registry the machine can pull from. Pulling from a private registry is not documented and not tested (#506). Services that use buildContext need that repo cloned at the path the service.json names. flisk up <stack> and flisk up --only <service> start a subset; --dry-run printed the expected selection (tilt up ... -- --focus=<stack> <service>), and neither was run for real here.
Cost: every team's service definition lives in the platform repo, so a team changes its own port, route or image through a pull request to that repo. See ownership.
orders-repo/ owned by the orders team
.flisk/project.json name: orders-repo
services/orders/orders-api/service.json
(own Postgres: orders_repo_postgres, own Traefik)
billing-repo/ owned by the billing team
.flisk/project.json name: billing-repo
services/billing/billing-api/service.json
(own Postgres: billing_repo_postgres, own Traefik)
What was observed for two such projects (l565-svc-a and l565-svc-b, same stack app, same service names api and api2), by diffing the generated files:
| Generated thing | Differs between the two projects? |
|---|---|
| Postgres container, user, database and volume names | Yes, prefixed with the project name (l565_svc_a_postgres, database l565_svc_a_app) |
| Docker networks and the Traefik container | Yes, prefixed (l565_svc_a_traefik-public) |
| Traefik host rules | Yes (api.l565-svc-a.localhost and api.l565-svc-b.localhost) |
| The Compose project that runs the services of a stack | No: app-autogenerated in both |
| The Compose project of Postgres and of Traefik | No (database-management, flisk-out), but their container names are prefixed |
The project name is project.name in .flisk/project.json, and flisk project took it from the directory name. flisk project --yes --config-file <path> copied the file including its name, so a project.json shared between repos this way gives every repo the same name and the same network and database names. Give each repo its own name.
Services in different projects have different networks, so a service in orders-repo cannot reach one in billing-repo by name. That was concluded from the generated network names, not tested at runtime.
Host ports: each project asks for Traefik on 80/443 and Postgres on 15432 (not 5432, which local databases often hold). When they are taken, Flisk picks the next free port from a range of about a hundred ports for each of the three (8080 to 8180, 8443 to 8543, 15432 to 15532 in host-port-plan.ts); see the FAQ. Running more than two projects at once is not documented or tested (#509).
platform-hub/ hub / orchestrator repository
.flisk/project.json declares repos: [{ url, dir, branch }]
services/
shop/
orders-api/ cloned from git@github.com:acme/orders-api.git
service.json discovered automatically once cloned
billing-api/ cloned from git@github.com:acme/billing-api.git
service.json discovered automatically once cloned
When services are developed in separate Git repositories but run together as a local platform, Layout C allows .flisk/project.json to declare the repositories that compose the workspace. The block below is a fragment: merge its repos array into the existing .flisk/project.json, which must also keep the required version, project, phases, optional_infra, and discovery fields.
{
"repos": [
{
"url": "git@github.com:acme/orders-api.git",
"dir": "services/shop/orders-api",
"branch": "main"
},
{
"url": "git@github.com:acme/billing-api.git",
"dir": "services/shop/billing-api",
"branch": "main"
}
]
}- Automatic clone on startup:
flisk upchecks declaredreposbefore resource discovery. Any missing service repository is cloned using structured Git arguments and existing credentials (SSHor credential helper), and discovery automatically includes newly cloned services. - Explicit clone command: Run
flisk repos cloneto clone missing repositories on demand. Pass--jsonfor machine-readable output:flisk repos clone --json. - Safe idempotency: Existing directories are never overwritten, pulled, or checked out. If a destination directory already exists, it is left untouched.
- Strict path validation: Target paths must be relative to the project root. Absolute paths, directory traversal (
..), symlink escapes outside the project root, and reserved directories (.git,.flisk,node_modules) are rejected with actionable errors. - Clean abort on failure: If a clone fails partway through, Flisk cleans up only the specific target directory created by that failed attempt, preserving any pre-existing files.
flisk doctor includes read-only checks for declared repositories:
- Verifies that all declared target directories exist.
- Checks whether targets are valid Git repositories.
- Reports warning if an expected branch is configured but another branch is currently checked out.
- Reports warning if local uncommitted changes exist.
- Doctor remains strictly read-only and never performs network fetches, checkouts, or resets.
In Layout C:
- The platform hub repository contains
.flisk/project.json, shared Compose infrastructure, and stack wiring. - Individual service repositories maintain their own commits, branches, PRs, and
service.jsonmanifests. - In
.gitignoreof the hub repo, teams typically ignore the cloned service directories (e.g.,services/shop/*/) so child Git repositories are not tracked as nested git repositories.
- Same stack and service name in two projects. Fixed in the engine: the Compose project is
<project>-<stack>-autogenerated, the container<project>-<stack>-autogenerated-<service>-1, the app image<project>_<stack>_<service>:devand the migrator image<project>_<stack>-db-migrator:dev, so two projects with apeoplestack no longer remove each other's containers or overwrite each other's images. The unit and Starlark tests cover the names; a two-projectflisk uprun was not run yet (see the migration note for containers started before this change). - Same name twice inside one project (two directories, both
appName: "api"):flisk resourcesprinted both, andflisk up --dry-run,flisk doctorandflisk config regenerateraised no error.flisk doctornow fails withDuplicate resource names, naming theservice.jsonpaths. The generated Compose and Traefik files build the service and router names fromappName(read inruntime/engine/topologies/platform/docker/), which is why it fails. What Tilt does with two resources of one name was not checked. - Same container port. Service ports are not published on the host in the generated Compose files (only Traefik and Postgres are), and each service has its own container. A backend and a bring-your-own service in one project both ended up with
"port": 4000, and nothing warned.flisk resource --port 4000refused a second bring-your-own resource with 4000.flisk doctornow warns (not fails) withDuplicate resource portswhen two backend, mcp or routed bring-your-own resources share aport: from the Compose files two containers can listen on 4000 at once, so it is not a certain clash. Frontends, workers, libraries and resources without aportare not counted. Running two services on one port throughflisk upwas not run. - One Postgres per project. Each project has one Postgres container shared by all its services (local data). The database is per stack,
<project>_<stack>(l565_svc_b_apphere); an earlier version ofdata.mdsaid per service and was corrected in #584. Services of one stack share a database. Check the database names in your own project.
What in a project is shared, and where it is defined (read from the files flisk project and flisk resource wrote):
| File | What it controls | Committed? |
|---|---|---|
.flisk/project.json |
Project name (so network and database names), stacks per phase, discovery.paths, optional infrastructure |
Yes |
services/<stack>/<name>/service.json |
One service: port, health path, image or build context, route settings | Yes |
services/platform/database-management/docker-compose.yml |
The shared Postgres | In a git repository flisk up adds it to .gitignore (block below); delete its line to commit it (gradual adoption) |
shared-platform-engineering/docker-templates/ |
Helper shell scripts used by generated builds | Same as above |
.env and .flisk/.flisk-out/ |
Generated passwords, secrets and output | No, git-ignored |
Generated files in the repository root (#3030). In a git work tree, flisk up and flisk config regenerate add the root files Flisk generates to .gitignore once, in a block that starts with # >>> Flisk generated files in the repository root (#3030): the workspace package.json and the lockfiles beside it (only when package.json is the one Flisk wrote, with no dependencies of its own), .tiltignore (only with the Flisk header), shared-platform-engineering/ and services/platform/database-management/docker-compose.yml. A path git already tracks is never ignored, and a package.json or .tiltignore your team wrote is never ignored or overwritten. Flisk does not touch an existing block, so to commit one of these files delete its line. services/platform/messaging/ is yours and is never listed. Moving these files under .flisk/ was not done: the Tiltfile reads .tiltignore from the root and generated builds use the root paths.
Traefik routes are not a separate shared file. They appear as labels in the Compose file generated from each service.json (for example traefik.http.routers.api-project.rule=Host(...)), so the team that owns a service.json owns its route.
An example CODEOWNERS for Layout A. The team names are placeholders. It was written from the table above; it was not run against GitHub, and enforcement needs "Require review from Code Owners" turned on in branch protection, which is GitHub's feature and was not tested here.
# Fallback: anything not listed below goes to the platform team
* @acme/platform-team
# Project-wide settings: project name, stacks, discovery paths
/.flisk/project.json @acme/platform-team
# Shared infrastructure, if you commit it
/services/platform/ @acme/platform-team
/shared-platform-engineering/ @acme/platform-team
# Each team owns its stack's service.json files (ports, routes, images)
/services/orders/ @acme/orders-team
/services/billing/ @acme/billing-team
The catch-all is first on purpose: as GitHub documents it, the last matching pattern wins, so the specific paths below it override it. That rule was read, not tested here.
To catch a service.json that was edited without regenerating, run flisk config verify in CI (FAQ). It passed on the scratch projects and listed out-of-date files after an edit. Port clashes between teams are not detected by any command checked here, so put a review on service.json ports, as above.
These are recommendations built from the checks above, not measured results. The only scale numbers Flisk has are in the claims registry and the scale benchmarks: generated /health stubs, with images already built, 10 services healthy after 8 s, 50 after 24 s (829 MiB in total) and 100 after 112 s on a 16 GB machine. A real application is larger. Memory, CPU and start time for real services are unmeasured, and so is how many real services fit on a developer machine (#508).
one repo or one platform repo
.flisk/project.json 1 project
services/<stack>/<name>/ 5 service.json
One project, one owner. If the five services are in five repos, use Layout A with image or buildContext, or Layout B if each repo is developed alone. No cross-team review problem to solve yet; one CODEOWNERS line for .flisk/project.json is enough.
platform-repo
.flisk/project.json 1 project, one stack per team
services/orders/ (team A, 6 services)
services/billing/ (team B, 7 services)
services/search/ (team C, 7 services)
CODEOWNERS path per team (above)
One project, one stack per team, flisk up <stack> for the stack you work on. This relies on flisk up <stack> pulling in what the stack depends on, which is described in its help text and was not run here. The shared Postgres and Traefik are one set for everyone. Nothing here was measured at this size.
platform-repo (one project) or per-domain projects
.flisk/project.json commerce-platform/ (20 services)
services/<team>/ x 8-10 stacks payments-platform/ (15 services)
CODEOWNERS per stack each its own name, ports and Postgres
Two routes, both unmeasured:
- One project. The fixture for this size is 50 stubs healthy in 24 s with images built; the CI fixture built 100 stubs from scratch in 472 s. The source has a warning,
Large root: ... has N manifests, for a discovery root with manyservice.jsonfiles (the threshold inruntime/discovery/constants.staris 110;runtime/discovery/AGENTS.mdsays 50). It was not triggered here. You would run only a few stacks at a time, since starting 50 real services at once is not something anyone has measured. - Several projects by domain. Each has its own Postgres and network set, which limits blast radius but multiplies host-port use, and running more than two projects at once is not documented or tested. Give each project a distinct name and distinct stack and service names (name clashes).
Which of the two is better at 50 services is not known; this page does not recommend one.
- Any
flisk upof these layouts: containers never started on the test machine (see the top of the page). - Building the image from a
buildContextoutside the project, and Tilt watching it. - Pulling a registry
image(public or private) throughflisk up. - Two projects running at once, including whether the shared
app-autogeneratedCompose project clashes underflisk up. - Service-to-service calls between projects, and between stacks in one project.
flisk up <stack>and--onlyon a project this large.- CODEOWNERS behaviour on GitHub.
- NATS, Infisical and other optional infrastructure in a shared setup.
Containers started by an earlier Flisk keep the names <stack>-autogenerated-<service>-1. The new engine does not know them: flisk down and Tilt leave them running, and they keep the old image tag <stack>_<service>:dev. Stop them once per stack from the project root, using the old Compose project name:
docker compose -p people-autogenerated -f services/people/.autogenerated/docker-compose.app.autogenerated.yml down --remove-orphansOnly do this when the old project name belongs to this project: earlier Flisk versions could not tell two projects apart, so check docker ps -a --filter name=people-autogenerated and its com.docker.compose.project.working_dir label first. The old image tags people_<service>:dev are not removed; docker rmi them when you are done.