# Build detection and builders
URL: https://synquic.tech/docs/guides/build-detection.md

How Synq decides how to build your code (the Dockerfile-first rule, Compose, framework detection) and how to choose another builder per project, Dockerfile with a custom path and context, Nixpacks, Railpack or Cloud Native Buildpacks.

import { Step, Steps } from 'fumadocs-ui/components/steps';

Every source that contains code (a GitHub repository, a [Git URL](/docs/guides/deploy-from-git-url), an
uploaded folder or archive) goes through the same two questions:

1. **What is this?** Synq scans the files and detects a Dockerfile, a Compose file, or a framework.
2. **How is the image built?** The project's **Builder** setting: **Auto** (Synq's own detection, the
   default) or a tool you choose.

Everything after the build works the same whatever you pick: ports, environment variables, domains,
zero-downtime deploys and rollbacks.

## How Synq decides what to build

Synq looks at the app's folder (the repository root, or the **Root directory** you set for a monorepo app)
and uses the **first** rule that matches:

| Order | If the folder has | Synq builds it as |
|---|---|---|
| 1 | A `Dockerfile` at the repository root (and no Compose file that adds settings, see below) | **Dockerfile**: your Dockerfile is built as is. |
| 2 | A recognized framework | That framework, with generated install, build and start commands. |
| 3 | `docker-compose.yml`, `docker-compose.yaml`, `compose.yml` or `compose.yaml` | **Compose**: one service per Compose service. |
| 4 | A `Dockerfile` (in a subfolder app) | **Dockerfile**. |
| 5 | An `index.html` and no `package.json` | A **static site**, served by the edge. |
| 6 | A `package.json`, `server.js`, `app.js` or `index.js` | A generic **Node.js** app. |

### The Dockerfile-first rule

A `Dockerfile` at the **repository root** is your own build recipe, so it wins over framework detection.
A Next.js repository with a root Dockerfile is built from the Dockerfile, not as Next.js. The deploy
screen then shows **Dockerfile Detected** ("Your project will be built and deployed using its
Dockerfile") and asks only for the **Exposed Port**, which it reads from the Dockerfile's `EXPOSE` line.

Two exceptions:

- **A Compose file that adds settings turns the rule off.** If the root also has a Compose file that says
  anything the Dockerfile path would lose, the Dockerfile does not win and detection continues down the
  table (a framework, then the Compose file). "Anything" means: more than one service; a service that only
  names an `image:`; a `build` with another context, another Dockerfile, `args`, `target` or `secrets`;
  any `volumes`, `env_file`, `depends_on`, `secrets`, `configs`, `command` or `entrypoint`; or
  `environment` keys that the Dockerfile does not set with `ENV`. When Synq cannot read the Compose file's
  contents, it assumes the Compose file matters. A plain one-service wrapper (`build: .` and nothing else,
  the kind kept for `docker compose up --build`) does not count: the Dockerfile is used.
- **A monorepo app in a subfolder** does not get the Dockerfile-first rule. A sub-app's Dockerfile usually
  expects the whole repository as its build context, so Synq detects the framework first and uses the
  Dockerfile only when no framework matches. To build a sub-app from its Dockerfile anyway, choose the
  **Dockerfile** builder and set the **Build context** (see [below](#dockerfile)).

To build a repository with a Dockerfile as a framework app instead, open **Framework** on the deploy screen
and press **Change**, or set it in [`synq.json`](/docs/guides/synq-json). **Use detected** goes back to
what Synq found. To deploy a repository as a Compose stack when a framework was detected (or when the
Compose file is not at the root), open **Compose file** on the deploy screen ("Deploying with Docker
Compose? Point to the file."), enter the file or its folder, and press **Scan**.

### Frameworks Synq detects

Frameworks are checked in this order, so a full-stack framework wins over a plain server library that is
also in its dependencies:

- **JavaScript and TypeScript**: Next.js, Nuxt, SvelteKit, Astro, Remix, TanStack Start, Angular, Gatsby,
  Vite, Create React App, Vue, NestJS, AdonisJS, Elysia, Hono, Fastify, Koa, Express.
- **Python**: Django, Flask, FastAPI, then any Python app.
- **Go**: Gin, Fiber, Echo, then any Go module.
- **Rust**: Actix, Axum, Rocket, then any Cargo project.
- **Ruby**: Rails, Sinatra. **PHP**: Laravel, Symfony. **JVM**: Spring Boot, Quarkus, Kotlin.
  **.NET**: Blazor, .NET. **Elixir**: Phoenix.

The deploy screen shows what was detected and the commands it will run. Change any of them there, or pin
them in [`synq.json`](/docs/reference/synq-json) so every deploy uses the same settings.

### How Synq picks the start command

For JavaScript and TypeScript apps, the start command is the app's **production** command. Many
`package.json` files use `start` for a development server: a new NestJS project has
`"start": "nest start"`, which recompiles the app every time it boots. Synq picks, in order:

1. A `start:prod` or `start:production` script (for NestJS, `npm run start:prod`).
2. The `start` script, when it is not a development server.
3. When `start` is a development server: a `prod` or `serve:prod` script, then the same file without the
   file watcher (`nodemon server.js` becomes `node server.js`), then the `main` field of `package.json`,
   then the framework's own production command (`node dist/main.js` for NestJS, `next start` for Next.js).

Synq treats these as development servers: `nest start` (with or without `--watch`), `nodemon`,
`ts-node-dev`, `tsx watch`, `node --watch`, `bun --watch` or `--hot`, `next dev`, `nuxt dev`, a plain `vite`,
`react-scripts start`, `ng serve`, `vue-cli-service serve`, `gatsby develop`, `remix dev`, `astro dev`,
`webpack serve`, `expo start` and `parcel`, also when they run through another script
(`"start": "npm run dev"`) or inside `concurrently`. Preview servers such as `vite preview` are not
development servers.

Scripts run with the project's package manager: `npm run start:prod`, `yarn start:prod`,
`pnpm start:prod` or `bun run start:prod`.

**Projects created before Synq 1.2.3** may have `npm run start` saved as their start command. Nothing needs
changing: when a build step runs and the repository's `start` script is a development server, the build uses
the production command instead and says so in the build log, for example:

```text
[synq-build] start command "npm run start" runs a dev server ("nest start"); using "npm run start:prod" instead
```

This only replaces the plain `npm run start`, `npm start`, `yarn start`, `pnpm start` or `bun run start`, and
only with a production script, the file a watcher runs, or the framework's compiled entry. Any other start
command is used as written. To run something else, set the start command with **Edit** on the project's
**Configuration** tab, or in `synq.json`.

## Choose a builder

The **Builder** setting decides which tool turns the source into a container image:

| Builder | What it does | Configure it with |
|---|---|---|
| **Auto** (default) | Synq detects the framework and writes the build. A Dockerfile in the repository is used when there is one (the rules above). | The deploy screen, or `synq.json`. |
| **Dockerfile** | Builds the repository's own Dockerfile. Its `CMD` starts the app. | **Dockerfile path** and **Build context**. |
| **Nixpacks** | Railway's Nixpacks detects the language and builds with Nix packages. | `nixpacks.toml`, or `NIXPACKS_*` environment variables. |
| **Railpack** | Railway's successor to Nixpacks, built on BuildKit. | `railpack.json`, or `RAILPACK_*` environment variables. |
| **Buildpacks** | Cloud Native Buildpacks from Heroku or Paketo. The `Procfile`, or the language default, starts the app. | **Builder image**, a `Procfile`, and buildpack environment variables. |

Nixpacks, Railpack and Buildpacks use the image's own start command, and the project's environment
variables are passed to the build.

### Where to set it

- **Before the first deploy**: on the deploy screen, open the **Builder** section (it is collapsed and on
  **Auto** unless you open it). It is offered for single-app projects on your own servers, not for Compose
  or multi-service projects.
- **Later**: open the project, then the **Configuration** tab, and expand the **Builder** card ("How this
  app's image is built on the build server"). Pick a builder, fill in its fields, and press
  **Save builder**. The change "Applies on the next deploy": press **Deploy** or **Redeploy** afterwards.

Projects that deploy a prebuilt image ([Docker image](/docs/guides/deploy-docker-image) projects and
catalog apps that ship as a release) have nothing to build, so they have no Builder card.

### Dockerfile

- **Dockerfile path**: relative to the build context. Leave it empty for `Dockerfile`. Example:
  `docker/Dockerfile.prod`.
- **Build context**: the directory sent to the build, relative to the repository root. Leave it empty for
  the app's root directory. Example: `.` for the whole repository, when a monorepo sub-app's Dockerfile
  copies shared packages.

Both must be paths inside the repository: no leading `/`, no `..`. Switching a project to **Dockerfile**
sets it up the way a Dockerfile import would: the framework becomes Docker, install and build commands are
cleared, the app runs as a web container, and the port comes from the Dockerfile's `EXPOSE` line (3000
when there is none).

### Nixpacks and Railpack

Put `nixpacks.toml` or `railpack.json` in the app's folder, or set `NIXPACKS_*` or `RAILPACK_*`
variables on the project's **Environment** tab. For example, `NIXPACKS_NODE_VERSION=22` or a
`[phases.build]` section in `nixpacks.toml`.

The card also takes extra system packages, so the common cases need no config file. Separate names with
spaces or commas; a name may use letters, digits and `. _ + @ -` (Mise names also `:` and `/`).

| Builder | Field | Becomes | Example |
|---|---|---|---|
| Nixpacks | **Nix packages** | `NIXPACKS_PKGS` | `ffmpeg imagemagick` |
| Nixpacks | **APT packages** | `NIXPACKS_APT_PKGS` | `libpq-dev` |
| Railpack | **Railpack version** | the Railpack release the build runs | `0.40.0` |
| Railpack | **Mise packages** | `RAILPACK_PACKAGES` | `node@22 pipx:httpie` |
| Railpack | **Build APT packages** | `RAILPACK_BUILD_APT_PACKAGES` | `build-essential` |
| Railpack | **Deploy APT packages** | `RAILPACK_DEPLOY_APT_PACKAGES` | `ffmpeg` |

A field wins over a project variable of the same name. **Railpack version** pins the whole tool (both
`railpack prepare` and its BuildKit frontend, `ghcr.io/railwayapp/railpack-frontend:v<version>`); leave it
empty to use the version Synq ships. A pin stays in place across Synq updates.

### Buildpacks

**Builder image** is one of:

| Builder image | Good for |
|---|---|
| `heroku/builder:24` (default) | Most apps. |
| `heroku/builder:22` | Apps that need the older Ubuntu 22.04 base. |
| `paketobuildpacks/builder-jammy-base` | Most apps, Paketo buildpacks. |
| `paketobuildpacks/builder-jammy-full` | Apps that need extra system libraries. |
| `paketobuildpacks/builder-jammy-tiny` | Small images without a shell. |

The list is fixed on purpose: a buildpacks builder gets access to the build server's Docker daemon, so
Synq only runs builders it knows. Add a `Procfile` (for example `web: node server.js`) to set the start
command.

### Pinned tool versions

The builders run as pinned containers on the build server's Docker daemon, never as programs installed on
the host. The Builder card shows the version, for example "Runs Nixpacks 1.41.0 as a pinned container on
the build server." The current pins are Nixpacks 1.41.0, Railpack 0.40.0 and `pack` 0.40.9. They change
only with a Synq update, except that a project can pin its own **Railpack version**.

### Static sites and other builders

Every builder except **Auto** produces a container. If your project is a static site, switching to another
builder turns it into a web app on the Docker runtime ("Every builder except Auto runs the app as a
container, so this static site becomes a web app on the Docker runtime").

### Switching back to Auto

When a project leaves **Auto**, Synq remembers its framework settings (framework, commands, output
directory, port, runtime). Choosing **Auto** again restores them, and the card says so ("Switching back to
Auto restores the Next.js build settings."). A project that was created from its Dockerfile has nothing to
restore, so Auto keeps building the Dockerfile. To build it as a framework app instead, use **Edit** in the
**Configuration** tab and pick a framework.

## The "This repo has a Dockerfile" banner

A project imported as a framework app (for example before Dockerfile-first detection existed, or because
someone picked the framework by hand) may still have a Dockerfile in its repository. When that happens, the
**Builder** card opens by itself and shows:

> **This repo has a Dockerfile. Build with it instead?**
> Dockerfile is in the repository, but this project builds as nextjs.

Press **Use Dockerfile** to switch in one step. Synq changes the builder to **Dockerfile**, sets the port
from the Dockerfile's `EXPOSE` line, and shows "Now building with the Dockerfile". The next deploy builds
the Dockerfile. To undo it, choose **Auto** and save: the previous framework settings come back.

Synq looks for the Dockerfile in the app's root directory first, then at the repository root. The banner
needs to read the repository, so it appears for GitHub projects. It does not appear for Git URL projects or uploads, where you can still pick **Dockerfile** by hand.

## API

- `GET /api/projects/:id/builder`: the saved builder, what the next deploy actually uses (`effective`),
  whether the repository has an unused Dockerfile (`suggestDockerfile`, with its `EXPOSE` port) and the
  pinned tool versions.
- `PATCH /api/projects/:id` with `builder`: for example
  `{ "builder": { "type": "dockerfile", "dockerfilePath": "docker/Dockerfile", "dockerContext": "." } }`,
  `{ "builder": { "type": "buildpacks", "buildpacksBuilder": "paketobuildpacks/builder-jammy-base" } }`,
  `{ "builder": { "type": "nixpacks", "nixpacksPackages": "ffmpeg", "nixpacksAptPackages": ["libpq-dev"] } }`
  (a package list is a string or an array; the API stores and returns an array), or
  `{ "builder": { "type": "auto" } }`. Applied on the next deploy.

## Related

<Cards>
  <Card title="Deploy from GitHub" href="/docs/guides/deploy-from-github" description="Import a repository and review the detected build." />
  <Card title="synq.json" href="/docs/guides/synq-json" description="Pin the framework, commands and port in the repository." />
  <Card title="Build on another server" href="/docs/guides/build-servers" description="Run the same builders on a separate build server and ship the image." />
  <Card title="Deployments troubleshooting" href="/docs/troubleshooting/deployments" description="Fix a build that fails or an app that does not start." />
</Cards>
