How to structure your repository

The short answer: however you already have it. TalariCloud reads what is there rather than asking you to rearrange it. This page is for when you want to know why it made the choice it made — and for the two layouts where a little structure saves you a lot of configuration.

The three layouts

Nearly every repository is one of these. The platform handles all three, and only the third one asks anything of you.

Three repository layouts and what each one deploys to A single application in the repository root becomes one service. A repository with api and web folders becomes two services from one repository. A repository declaring services in talaricloud.yaml becomes exactly the services the file names. ONE APP my-app/ manage.py requirements.txt myapp/ 1 service nothing to configure MONOREPO my-app/ api/ requirements.txt web/ package.json 2 services found, not typed DECLARED my-app/ talaricloud.yaml api/ web/ worker/ jobs/ admin/ … exactly what it says no limit, no guessing
You do not have to pick one. A repository that starts as the first and grows into the second needs no migration.

One application in the root

Nothing to do. Put your manifest where it belongs and the platform finds it — requirements.txt, package.json, composer.json, go.mod, Cargo.toml, Gemfile or mix.exs.

my-app/
├── manage.py
├── requirements.txt
└── myapp/
    ├── settings.py
    └── wsgi.py

One rule worth knowing: the manifest has to be committed. A requirements.txt in .gitignore, or a package-lock.json that never got added, is the most common reason a repository that runs on your laptop is not recognised at all.

A frontend and a backend in one repository

Put each one in its own folder with its own manifest. That is the whole requirement.

my-app/
├── api/
│   ├── requirements.txt
│   └── main.py
└── web/
    ├── package.json
    └── src/

Connect the repository once and the platform reads it, finds both, and proposes a service for each — with the runtime, build command and start command already filled in. They become one application: one plan, one bill, one page in the dashboard. A frontend and a backend are not two subscriptions.

What it will and will not create for you

  • At most four services from one repository. Not a technical limit — a deliberate one. A service you have to add yourself is one button; a service invented for you is a monthly charge on a plan you never picked, discovered on the bill.
  • Only web services and static sites. A worker and a cron job look identical to a web service from the outside — both are "a program that runs" — so the platform proposes only what it can be certain of, and you change the type afterwards.
  • Some folders are skipped on purpose, and each skip is reported with its reason. A React Native app has a package.json and a start script and looks exactly like a Node service; deployed, it builds, starts, listens on nothing, fails its health check forever, and bills you for it. Shared libraries are skipped for the same reason.

It scans the root and up to fifteen folders, two levels deep. A service in a folder deeper than that is still one click away — point a new service's root directory at it.

Root directory: the one setting that matters

A service's root directory is the folder the build runs in. Set it to api and the build installs api/requirements.txt and starts in api/ — as though that folder were the whole repository.

This is what makes a monorepo work, and it is also the setting people get wrong. Two consequences worth holding in mind:

  • Paths in your build and start commands are relative to it, not to the repository root.
  • Code outside it is not part of the build. A shared packages/ui that web/ imports will not be there when web/ builds alone.

If you share code between services, the honest options are to publish it as a package, or to set the root directory to the repository root and let the build command reach into the subfolder:

# root directory: .  (the whole repository)
npm ci && npm run build --workspace web

Declaring it yourself: talaricloud.yaml

Discovery is a guess, however good. A file is a decision — and a decision is worth writing down once your repository holds more than a handful of services, or when the next person to deploy it should not have to reconstruct what you clicked.

Put talaricloud.yaml in the repository root:

services:
  - name: beamline-api
    type: web
    runtime: python
    rootDir: api
    buildCommand: pip install -r requirements.txt
    startCommand: gunicorn app.wsgi:application --bind 0.0.0.0:$PORT
    preDeployCommand: python manage.py migrate --noinput
    healthCheckPath: /healthz
    envVars:
      - key: DJANGO_SETTINGS_MODULE
        value: app.settings.production
      - key: SECRET_KEY
        generateValue: true

  - name: beamline-web
    type: static
    rootDir: web
    buildCommand: npm ci && npm run build
    staticPublishPath: dist

  - name: beamline-worker
    type: worker
    runtime: python
    rootDir: api
    startCommand: celery -A app worker -l info

databases:
  - name: beamline-db

A blueprint overrides the four-service cap entirely. A file declaring twelve services gets twelve, because somebody wrote them down — which is the whole difference between a declaration and a guess.

generateValue: true asks the platform to invent a secret and keep it, so a SECRET_KEY nobody should have to think of is never one somebody has to commit.

Coming from Render? A render.yaml is read as-is. Nothing to rename, nothing to rewrite — the keys above are Render's keys.

One address for a frontend and a backend

Two services normally mean two addresses, and a browser calling one from the other means CORS, cookie domains, and an afternoon you will not get back.

Instead, route a path on one service's address to another: /api on your frontend's address reaches your backend, and the browser only ever sees one origin. No CORS to configure, cookies simply work, and there is one address to share.

myapp.talaricloud.com/        → web    (your frontend)
myapp.talaricloud.com/api/*   → api    (your backend)

This is set on the frontend service, after both are deployed. Structurally it means one useful thing for your repository: your frontend can call /api/... with a relative path, in development and in production, and never needs to know a hostname at all.

Things that quietly cost you a deploy

  • The manifest is gitignored. If it is not committed, it does not exist as far as the platform is concerned.
  • The start command binds a fixed port. It must use $PORT and bind 0.0.0.0. Binding 127.0.0.1 or a hard-coded 8000 means nothing outside the container can reach it, and the health check fails on an app that looks perfectly healthy in the logs.
  • Secrets committed to the repository. Use environment variables; they are encrypted at rest and masked until you ask. A key in git history is a key you have to rotate, not delete.
  • A build that needs a file outside the root directory. See above — it will not be there.
  • Two services sharing a folder and fighting over it. An API and a worker should share a root directory — same code, different start command. That is correct and supported. Two different applications in one folder is not.

A checklist

  • Every deployable app has its own folder and its own committed manifest.
  • The start command binds 0.0.0.0:$PORT.
  • No secrets in the repository.
  • Migrations are a pre-deploy command, not something your app does on startup.
  • More than four services, or you want the layout written down? Add talaricloud.yaml.

Deploy it

Connect the repository and let the platform read it. Nothing to rearrange first.

Request an invitation → How deploying works