{"slug":"multi-stage-builds-for-monorepos","url":"https://tako.sh/blog/multi-stage-builds-for-monorepos/","canonical":"https://tako.sh/blog/multi-stage-builds-for-monorepos/","title":"Multi-Stage Builds for Monorepos","date":"2026-04-09T04:14","description":"How Tako's build stages let you deploy monorepo apps with shared packages — no Docker, no CI pipeline, just TOML.","author":null,"image":"dab463c4c310","imageAlt":null,"headings":[{"depth":2,"slug":"a-real-example","text":"A real example"},{"depth":2,"slug":"how-it-works","text":"How it works"},{"depth":2,"slug":"what-about-caching","text":"What about caching?"},{"depth":2,"slug":"stages-vs-single-build","text":"Stages vs single build"},{"depth":2,"slug":"no-docker-no-ci-just-deploy","text":"No Docker, no CI, just deploy"}],"markdown":"You have a monorepo. A shared UI library in `packages/ui`, an API app in `apps/api`, a web frontend in `apps/web`. Everything shares types, components, maybe a design system. It works great locally.\n\nThen you try to deploy, and the fun stops.\n\nMost deploy tools treat your repo as a single app with a single build command. Monorepos don't work that way. You need to build the shared library first, then the app that depends on it — in the right order, from the right directories. With tools like [Kamal](https://github.com/basecamp/kamal) or [Dokku](https://github.com/dokku/dokku), you end up writing a wrapper script or offloading the whole thing to CI. The deploy tool becomes a dumb uploader.\n\nTako handles this natively with [`[[build_stages]]`](/docs/tako-toml).\n\n## A real example\n\nSay your monorepo looks like this:\n\n```\npackages/ui/        # shared component library\napps/web/           # TanStack Start frontend\n  tako.toml\n```\n\nYour `apps/web/tako.toml`:\n\n```toml\nname = \"web\"\npreset = \"tanstack-start\"\nruntime = \"bun\"\n\n[[build_stages]]\nname = \"shared-ui\"\ncwd = \"../packages/ui\"\ninstall = \"bun install\"\nrun = \"bun run build\"\nexclude = [\"**/*.map\"]\n\n[[build_stages]]\nname = \"web-app\"\ninstall = \"bun install\"\nrun = \"vinxi build\"\nexclude = [\"**/*.map\", \"src/**/*.test.ts\"]\n\n[envs.production]\nroute = \"app.example.com\"\nservers = [\"prod\"]\n```\n\nThat's it. Run [`tako deploy`](/docs/deployment) and both stages execute in order — shared library first, then the app. No wrapper script, no Makefile, no CI pipeline glue.\n\n## How it works\n\n```d2\ndirection: right\n\nstages: Build Stages {\n  s1: \"shared-ui\\n(packages/ui)\" {\n    style.fill: \"#9BC4B6\"\n  }\n  s2: \"web-app\\n(apps/web)\" {\n    style.fill: \"#E88783\"\n  }\n  s1 -> s2: \"in order\"\n}\n\nartifact: \"Deploy\\nArtifact\" {\n  style.fill: \"#FFF9F4\"\n  style.stroke: \"#2F2A44\"\n}\n\nserver: \"tako-server\" {\n  style.fill: \"#2F2A44\"\n  style.font-color: \"#FFF9F4\"\n}\n\nstages.s2 -> artifact: \"exclude\\npatterns\"\nartifact -> server: \"SFTP\"\n```\n\nEach stage runs sequentially in declaration order. For every stage, Tako:\n\n1. Resolves the working directory (`cwd` relative to your app root — `..` is allowed for reaching sibling packages)\n2. Runs `install` if specified\n3. Runs `run`\n4. Collects `exclude` patterns, auto-prefixed with the stage's `cwd`\n\nAfter all stages complete, Tako packages the artifact — respecting `.gitignore`, force-excluding `.git/`, `.env*`, and `node_modules/` — and ships it to your servers via SFTP. The server runs a production install and starts your app with [zero-downtime rolling updates](/blog/zero-downtime-deploys-without-a-container-in-sight).\n\nThe workspace root is guarded: `cwd` can go up with `..` to reach sibling packages, but it can't escape the project root. You get monorepo flexibility without security surprises.\n\n## What about caching?\n\nTako caches build artifacts locally under `.tako/artifacts/`. The cache key includes a source hash, so if nothing changed, your next deploy skips the build entirely. Cached artifacts are checksum-verified before reuse — corrupted caches are discarded and rebuilt automatically.\n\nThis means your second deploy of the same code to a different [environment](/docs/tako-toml) is near-instant. Build once, ship to staging and production from the same artifact.\n\n## Stages vs single build\n\n|                        | `[build]`                  | `[[build_stages]]`                  |\n| ---------------------- | -------------------------- | ----------------------------------- |\n| **Use case**           | Single app, one build step | Monorepo or multi-step builds       |\n| **Working directory**  | One `cwd`                  | Per-stage `cwd` with `..` traversal |\n| **Exclude patterns**   | Top-level `exclude`        | Per-stage `exclude` (auto-prefixed) |\n| **Install step**       | One `install`              | Per-stage `install`                 |\n| **Mutual exclusivity** | Can't combine with stages  | Can't combine with `[build].run`    |\n\nThey're mutually exclusive by design. If you have `[[build_stages]]`, don't set `[build].run` — Tako will tell you if you try.\n\n## No Docker, no CI, just deploy\n\nThe monorepo deploy problem exists because most tools assume \"one repo = one container = one build.\" Tako doesn't use containers, so it doesn't inherit that assumption. Your build stages run directly on your machine, in order, with full access to your monorepo's dependency graph.\n\nCombined with [presets](/docs/presets) for framework-specific defaults and [multi-server environments](/blog/one-config-many-servers) for routing, you get a deploy workflow that actually fits how modern TypeScript monorepos work — not how Docker wishes they worked.\n\nCheck out the [full config reference](/docs/tako-toml) or the [deployment guide](/docs/deployment) to get started."}