Skip to content

Latest commit

 

History

History
245 lines (179 loc) · 22.1 KB

layout.md

File metadata and controls

245 lines (179 loc) · 22.1 KB

Layout and ownership: where the Flisk project lives when services are in several repos

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.

Short answer

  • One Flisk project is one repository tree. flisk finds services by walking the project directory for service.json files. 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 a service.json there. 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.json inside 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.paths points at service.json files 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).

A service outside the project root

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.

Layout A: one project in a platform repo

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.

Layout B: one project per service repo

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).

Layout C: multi-repository project with declared service repos (repos in .flisk/project.json)

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"
    }
  ]
}

Cloning and startup behavior

  • Automatic clone on startup: flisk up checks declared repos before resource discovery. Any missing service repository is cloned using structured Git arguments and existing credentials (SSH or credential helper), and discovery automatically includes newly cloned services.
  • Explicit clone command: Run flisk repos clone to clone missing repositories on demand. Pass --json for 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.

Doctor verification

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.

Ownership and CODEOWNERS

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.json manifests.
  • In .gitignore of 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.

Name and port clashes

  • 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>:dev and the migrator image <project>_<stack>-db-migrator:dev, so two projects with a people stack no longer remove each other's containers or overwrite each other's images. The unit and Starlark tests cover the names; a two-project flisk up run 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 resources printed both, and flisk up --dry-run, flisk doctor and flisk config regenerate raised no error. flisk doctor now fails with Duplicate resource names, naming the service.json paths. The generated Compose and Traefik files build the service and router names from appName (read in runtime/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 4000 refused a second bring-your-own resource with 4000. flisk doctor now warns (not fails) with Duplicate resource ports when two backend, mcp or routed bring-your-own resources share a port: 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 a port are not counted. Running two services on one port through flisk up was 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_app here); an earlier version of data.md said per service and was corrected in #584. Services of one stack share a database. Check the database names in your own project.

Ownership and CODEOWNERS

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.

Layouts for about 5, 20 and 50 services

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).

About 5 services

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.

About 20 services

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.

About 50 services

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 many service.json files (the threshold in runtime/discovery/constants.star is 110; runtime/discovery/AGENTS.md says 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.

Not checked

  • Any flisk up of these layouts: containers never started on the test machine (see the top of the page).
  • Building the image from a buildContext outside the project, and Tilt watching it.
  • Pulling a registry image (public or private) through flisk up.
  • Two projects running at once, including whether the shared app-autogenerated Compose project clashes under flisk up.
  • Service-to-service calls between projects, and between stacks in one project.
  • flisk up <stack> and --only on a project this large.
  • CODEOWNERS behaviour on GitHub.
  • NATS, Infisical and other optional infrastructure in a shared setup.

Migrating containers started before the project-scoped names

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-orphans

Only 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.